TelegramMessenger / Telegram-iOS

Telegram-iOS
5.37k stars 1.44k forks source link

Scrolling issue with Telegram Web Apps for Bots #1070

Open NelepovDmitry opened 1 year ago

NelepovDmitry commented 1 year ago

Telegram Flutter scrolling bug Platforms - Android & iOS

Steps to reproduce

1.Create Flutter app with a simple ListView (like ListView.builder for example) 2.Deploy it on any host (or for test use https://todo.hubitproject.com/flutter_bug_ck) 3.Create Telegram bot or check test bot @hubit_test_flutter_bot 3.1. Open Telegram bot father @Botfather 3.2. Type /newbot 3.3. Enter a unique name 3.3. Enter a unique bot login 3.4. Type /setmenubutton 3.5. Enter a path to flutter app from step 2 (or for test use https://todo.hubitproject.com/flutter_bug_ck) 3.6. Enter button name, for example "Test Flutter" 4.Open bot from Mobile device 5.Tap button from step 3.6 6.Expand Web App to fullscreen by dragging topbar part of webapp

  1. Try scroll up (finger move up) and down (finger move down) Expected result Scroll works correctly

Actual result Scrolling down is close telegram web app

Complete code of app deployed at https://todo.hubitproject.com/flutter_bug_ck

import 'dart:math';

import 'package:flutter/material.dart';

void main() { runApp(const MyApp()); }

class MyApp extends StatelessWidget { const MyApp({super.key});

// This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomeScreen(), ); } }

class HomeScreen extends StatefulWidget { const HomeScreen({super.key});

@override State createState() => _HomeScreenState(); }

class _HomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( body: ListView.separated( itemBuilder: (context, index) { return Container( padding: const EdgeInsets.all(32), color: Color.fromARGB( 255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255), ), child: Text('Item $index'), ); }, separatorBuilder: (context, index) { return const SizedBox( height: 32, ); }, itemCount: 100), ); } }

flutter doctor -v result

[✓] Flutter (Channel stable, 3.3.10, on macOS 13.1 22C65 darwin-arm, locale en-RU) • Flutter version 3.3.10 on channel stable at /Users/rain/Dev/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 135454af32 (2 months ago), 2022-12-15 07:36:55 -0800 • Engine revision 3316dd8728 • Dart version 2.18.6 • DevTools version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) • Android SDK at /Users/rain/Library/Android/sdk • Platform android-33, build-tools 33.0.0 • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866) • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.2) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14C18 • CocoaPods version 1.11.3

[✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[✓] IntelliJ IDEA Community Edition (version 2022.3.2) • IntelliJ at /Applications/IntelliJ IDEA CE.app • Flutter plugin version 72.0.4 • Dart plugin version 223.8617.8

[✓] VS Code (version 1.75.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension can be installed from: 🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (2 available) • macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm • Chrome (web) • chrome • web-javascript • Google Chrome 109.0.5414.119

[✓] HTTP Host Availability • All required HTTP hosts are available

• No issues found!

NelepovDmitry commented 1 year ago

Give the option to disable close by gesture - this will solve all the problems

qahor0v commented 5 months ago

Give the option to disable close by gesture - this will solve all the problems

For example?

maybephilipp commented 2 months ago

I've investigated @tapswap_bot (props to them) source code (webkit debug) and found the solution. I've also made the scrolling working (their solution was breaking it).

So steps to implement:

  1. Document/body magic (without this touchmove step doesn't fix the problem for some reason):

(tailwind style) document: h-auto, overflow-hidden body: min-h-screen h-screen overflow-hidden

js code:

const overflow = 100
document.body.style.overflowY = 'hidden'
document.body.style.marginTop = `${overflow}px`
document.body.style.height = window.innerHeight + overflow + "px"
document.body.style.paddingBottom = `${overflow}px`
window.scrollTo(0, overflow)
  1. Block touchmove (block scroll on doc and body, because of the first step. Make body's child scrollable and then...):
    let ts: number | undefined
    const onTouchStart = (e: TouchEvent) => {
    ts = e.touches[0].clientY
    }
    const onTouchMove = (e: TouchEvent) => {
    if (scrollableEl) {
    const scroll = scrollableEl.scrollTop
    const te = e.changedTouches[0].clientY
    if (scroll <= 0 && ts! < te) {
      e.preventDefault()
    }
    } else {
    e.preventDefault()
    }
    }
    document.documentElement.addEventListener('touchstart', onTouchStart, { passive: false })
    document.documentElement.addEventListener('touchmove', onTouchMove, { passive: false })

After that it works perfectly, no rage scroll breaks it. The only concern is that maybe some device has reversed scroll direction, in that case we will need to reverse touchmove condition logic, but anyway - it's working.

Murik1502 commented 1 month ago

I've investigated @tapswap_bot (props to them) source code (webkit debug) and found the solution. I've also made the scrolling working (their solution was breaking it).

So steps to implement:

  1. Document/body magic (without this touchmove step doesn't fix the problem for some reason):

(tailwind style) document: h-auto, overflow-hidden body: min-h-screen h-screen overflow-hidden

js code:

const overflow = 100
document.body.style.overflowY = 'hidden'
document.body.style.marginTop = `${overflow}px`
document.body.style.height = window.innerHeight + overflow + "px"
document.body.style.paddingBottom = `${overflow}px`
window.scrollTo(0, overflow)
  1. Block touchmove (block scroll on doc and body, because of the first step. Make body's child scrollable and then...):
let ts: number | undefined
const onTouchStart = (e: TouchEvent) => {
  ts = e.touches[0].clientY
}
const onTouchMove = (e: TouchEvent) => {
  if (scrollableEl) {
    const scroll = scrollableEl.scrollTop
    const te = e.changedTouches[0].clientY
    if (scroll <= 0 && ts! < te) {
      e.preventDefault()
    }
  } else {
    e.preventDefault()
  }
}
document.documentElement.addEventListener('touchstart', onTouchStart, { passive: false })
document.documentElement.addEventListener('touchmove', onTouchMove, { passive: false })

After that it works perfectly, no rage scroll breaks it. The only concern is that maybe some device has reversed scroll direction, in that case we will need to reverse touchmove condition logic, but anyway - it's working.

Hi! I've tryied to use your solution, but i couldn't figure out, how to use it properly. My page just get's stuck in place. I have changed scrollableEl to the one i have, but it still doesn't work. If you could write a more detailed guide I would appreciate it. Btw I'm using React. Thanks!

maybephilipp commented 1 month ago

Hi @Murik1502 ! We use React too (NextJS) and that's my code:

import AwaiterStyles from './Awaiter.module.scss'

export function Awaiter() {
  const [tgReady, setTGReady] = useState(false)

  useEffect(() => {
    if (!tgReady) return

    if (window.tgAppInited) return
    window.tgAppInited = true

    const scrollableEl = document.getElementById('app-scrollable')

    const tg = window.Telegram.WebApp

    tg.expand()

    const overflow = 100
    function setupDocument(enable: boolean) {
      if (enable) {
        document.documentElement.classList.add(AwaiterStyles.html)
        document.body.style.marginTop = `${overflow}px`
        document.body.style.height = window.innerHeight + overflow + "px"
        document.body.style.paddingBottom = `${overflow}px`
        window.scrollTo(0, overflow)
      } else {
        document.documentElement.classList.remove(AwaiterStyles.html)
        document.body.style.removeProperty('marginTop')
        document.body.style.removeProperty('height')
        document.body.style.removeProperty('paddingBottom')
        window.scrollTo(0, 0)
      }
    }
    setupDocument(true)

    let ts: number | undefined
    const onTouchStart = (e: TouchEvent) => {
      ts = e.touches[0].clientY
    }
    const onTouchMove = (e: TouchEvent) => {
      if (scrollableEl) {
        const scroll = scrollableEl.scrollTop
        const te = e.changedTouches[0].clientY
        if (scroll <= 0 && ts! < te) {
          e.preventDefault()
        }
      } else {
        e.preventDefault()
      }
    }
    document.documentElement.addEventListener('touchstart', onTouchStart, { passive: false })
    document.documentElement.addEventListener('touchmove', onTouchMove, { passive: false })

    const onScroll = () => {
      if (window.scrollY < overflow) {
        window.scrollTo(0, overflow)
        if (scrollableEl) {
          scrollableEl.scrollTo(0, 0)
        }
      }
    }
    window.addEventListener('scroll', onScroll, { passive: true })

    // authorize here

    return () => {
      setupDocument(false)
      document.documentElement.removeEventListener('touchstart', onTouchStart)
      document.documentElement.removeEventListener('touchmove', onTouchMove)
      window.removeEventListener('scroll', onScroll)
    }
  }, [tgReady])

  return <>
    <Script
      src='/js/telegram-web-app.min.js'
      onLoad={() => {
        setTGReady(true)
      }}
    />
  </>
}

Styles:

.html {
  @apply h-auto overflow-hidden;

  body {
    @apply min-h-screen h-screen overflow-hidden isolate;
  }
}

HTML tree is the following: html > body > div#app-scrollable

You can inspect our bot further, if you want to compare it with yours: @OGCommunityBot

Murik1502 commented 1 month ago

Hi @maybephilipp! Thanks a lot for your response. I have only one question: where do you call your Awaiter function?

BTW, your app looks so sick! Great job

maybephilipp commented 3 weeks ago

I have only one question: where do you call your Awaiter function?

@Murik1502 I call it in a root layout right after opening "html" tag: <Awaiter />. But the placement doesn't really make any difference - it just need to be loaded on the page. While it's loading or not inserted I show a full page loader.

It could be a hook, but for us the tg script is optional, so we insert this RJX element conditionally.

funtoom commented 1 week ago

Has anyone tried this solution with Flutter Web? It seems like scrollTo doesn't work after loading, leaving a white stripe. Nothing helped.