Skip to content

🤖 fix: jump-to-bottom button never disappears on iOS PWA#2674

Open
ibetitsmike wants to merge 7 commits intomainfrom
mike/scroll-navigation-jdpg
Open

🤖 fix: jump-to-bottom button never disappears on iOS PWA#2674
ibetitsmike wants to merge 7 commits intomainfrom
mike/scroll-navigation-jdpg

Conversation

@ibetitsmike
Copy link
Contributor

@ibetitsmike ibetitsmike commented Feb 27, 2026

Summary

Fixes the "Jump to bottom" button staying visible permanently on iOS PWA after it first appears.

Background

On iOS Safari/PWA, the handleScroll callback in useAutoScroll used a 100ms window after the last touchmove to distinguish user scrolls from programmatic scrolls. Multiple iOS scroll scenarios cause this window to expire before the scroll position settles at the bottom:

  • Momentum scrolling: after the user lifts their finger, touchmove events stop but inertial scroll continues
  • Slow drags: irregular timing between touchmove and scroll events

Implementation

Debounced scroll-settled check (useAutoScroll.ts): After each downward scroll event, start a 150ms debounce timer. When scrolling stops and the viewport is at the bottom, re-enable auto-scroll. The 150ms delay avoids re-enabling during upward momentum from the bottom.

Guards to prevent the debounce from interfering with intentional programmatic disables:

  • disableAutoScroll(): New method for code-driven disables (navigate-to-message, edit-message). Sets a programmaticDisableRef flag that blocks debounce re-arming, and clears any pending timer. Used in ChatPane.tsx instead of setAutoScroll(false).
  • Direction guard: debounce only arms on downward scroll movement, so a small upward scroll near the bottom doesn't get its disable undone.
  • programmaticDisableRef: Cleared when the user touches the scroll container (markUserInteraction via onTouchMove), allowing the debounce to work for subsequent user scrolls.

Generated with mux • Model: anthropic:claude-opus-4-6 • Thinking: high • Cost: $1.39

On iOS Safari/PWA, momentum (inertial) scrolling continues after
the user lifts their finger, but no touchmove events fire during
momentum scrolling. The handleScroll callback used a 100ms window
after the last touchmove to distinguish user scrolls from
programmatic scrolls — momentum scroll events fell outside this
window and were ignored entirely.

This meant that when a user scrolled up (showing the button) and
then scrolled back down to the bottom via momentum, the scroll
events reaching the bottom were classified as 'programmatic' and
the button stayed visible forever.

Fix: in the non-user-scroll path, still check if we've reached
the bottom and re-enable auto-scroll. This is safe because our
own programmatic scrolls (performAutoScroll, jumpToBottom) already
set autoScroll=true before scrolling, so this branch only fires
for momentum/inertial scrolling.
@ibetitsmike
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b52c958664

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Only re-enable auto-scroll during non-user-initiated scrolls when
the momentum is moving downward toward the bottom. This prevents
upward inertia near the bottom from incorrectly snapping the view
back down while the user is trying to read older content.
@ibetitsmike
Copy link
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

The previous momentum-based fix was insufficient — on iOS PWA, the
100ms touchmove-based user interaction window is too fragile for
all scroll scenarios (slow drags, varied momentum timing, etc.).

New approach: a debounced 'scroll settled' check fires 150ms after
the last scroll event. If scrolling has stopped and we're at the
bottom, re-enable auto-scroll. The 150ms delay ensures upward
momentum from the bottom will still be producing scroll events
(resetting the timer) before it clears the bottom threshold.

Also adds onTouchStart as a user interaction marker alongside
onTouchMove, catching edge cases where touchmove doesn't fire.
@ibetitsmike
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3451c547ca

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Add disableAutoScroll() that clears the pending scroll-settled
timer, so programmatic disables (navigate-to-message, edit-message)
aren't undone by the debounced re-enable. The scroll handler's own
disable (user scrolls up) deliberately doesn't clear the timer so
the debounce can recover when the user scrolls back to the bottom.
@ibetitsmike
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 713e3150fb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Prevents a small upward scroll near the bottom (< threshold) from
having its autoScroll disable undone 150ms later. The debounce timer
is now only started/restarted when scroll movement is downward, so
the user can scroll up slightly to pause reading without the jump
button disappearing.
@ibetitsmike
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ad296a5b3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Add programmaticDisableRef flag set by disableAutoScroll() to
block the scroll-settled debounce from re-arming during programmatic
scrolls (scrollIntoView after navigate-to-message/edit-message).
The flag is cleared when the user touches the scroll container
(markUserInteraction), allowing the debounce to work again for
subsequent user scrolls.
@ibetitsmike
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fd84c88e2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

onTouchStart fires for ALL touches including taps on message links,
which opens the user-interaction window before scrollIntoView in
handleNavigateToMessage. This caused programmatic scrolls near the
bottom to be misclassified as user scrolls and re-enable auto-scroll.

onTouchMove is the correct event — it only fires during actual
drag/scroll gestures, not for taps.
@ibetitsmike
Copy link
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant