Skip to content

fix(mobile): keep scroll-to-bottom hidden during overscroll - #24987

Closed
JSMike wants to merge 2 commits into
pingdotgg:mainfrom
JSMike:fix/mobile-chat-overscroll-button
Closed

JSMike wants to merge 2 commits into
pingdotgg:mainfrom
JSMike:fix/mobile-chat-overscroll-button

Conversation

@JSMike

@JSMike JSMike commented Oct 25, 2026 •

Copy link
Copy Markdown

Problem

Dragging or overscrolling native mobile chat at its newest message shows the scroll-to-bottom button even when nothing is below the viewport. The button currently treats a paused live-follow gesture as evidence that the reader has scrolled away.

Reproduction: open a thread at the bottom, drag upward past the bottom and hold, then release. The button should stay hidden at the end, appear when reading earlier messages, and hide again on returning to the end.

Change

Track measured end position separately from the live-follow latch. Keep the button stable while the existing release timer determines whether momentum starts, then refresh its position. If the final stream chunk arrives after release at the end, use the existing keyboard-aware scroll helper once to catch up.

Reuse the native keyboard scroll view's content-size callback and existing settle timer. Skip size measurements while following or settling, and report position changes only when the value changes. No new native listeners, polling, or timers are introduced. Main's keyed thread body initializes both flags together on a thread switch; explicit follow resets also resynchronize the reported end position. The existing list layout/load callbacks refresh paused position after viewport changes and list remounts; false scroll measurements while following cannot seed a flash on the next drag.

Scope and approval

Related to the drag-at-bottom half of #12754, also reported in duplicate #13364. A maintainer identified the paused-follow visibility rule in #12754 (comment). This change is limited to that native mobile button behavior and the release/streaming cases needed to keep it accurate. It does not change the threshold for returning to live follow or affect web/desktop.

This is intended as a single, more complete alternative to #24869 for the same underlying problem: the scroll-to-bottom control must accurately represent whether unseen content remains after a bottom gesture. It includes the basic overscroll visibility fix and covers two additional release cases: transient position reports before native momentum starts, and a final streaming chunk arriving after a release at the end. The latter requires an explicit catch-up so resuming follow does not hide an outstanding gap.

Compared with #24869, this implementation reuses the existing scroll handlers, shared native content-size callback, and release timer instead of adding separate LegendList isAtEnd and totalSize subscriptions. This avoids those additional subscriptions; no measured runtime performance advantage is claimed. The streaming/release regression tests distinguish this work from a duplicate of the basic visibility patch. No maintainer decision to supersede or close #24869, or separate approval of this implementation, is claimed.

Verification

Rebased onto upstream main at 4013183171 on November 8. Resolved the feed JSX conflict by preserving main’s workspace mutation provider and current feed props, then retaining the position and catch-up callbacks. Both fix commits remain in the branch; the layout fix applied unchanged.

  • vp test run apps/mobile/src/features/threads/thread-feed-live-follow.test.ts: 43 passed, including bottom bounce, held-finger content growth, guarded streaming updates, no-momentum release growth, momentum handoff, and catch-up exclusions. The release-window regression cases failed before the corresponding fix.
  • vp run --filter @t3tools/mobile typecheck: passed.
  • Targeted vp lint on the four changed files: no errors; warnings remain in the existing large components.
  • Formatting and git diff --check: passed.
  • vp run check:changed upstream/main after the November 8 rebase: passed formatting, lint, both knip passes, affected-package typechecks (mobile and scripts), all 43 focused tests, and all 8 selected React compiler checks. Full test suites remain with CI.
  • The implementation before the upstream rebase was reviewed with Claude Opus 5.5 and Grok 4.7 through Cursor; the final review reported no remaining confirmed defects.

Android verification completed on a Medium Phone API 36.1 emulator (Android 16, 4 GB RAM), using the fingerprint-matched development client and an isolated backend with a synthetic four-message conversation. The before capture uses the four changed files from upstream 87d22b00f; the after capture uses 870561842, with the same native binary and fixture.

Observed: dragging upward past the bottom shows the arrow before the fix and keeps it hidden afterward. Scrolling back into history still shows the arrow; tapping it returns to the last message and hides it. The screenshots below are frames from the recordings during the bottom gesture.

Before After
Before: arrow appears at the end After: arrow stays hidden at the end

15-second comparison, before on the left, after on the right. Both use the same 700 px upward drag over 8 seconds; the clips are trimmed for comparison, not a performance benchmark.

t3-overscroll-comparison.mp4

Limitations: the Android captures above are from the pre-rebase commit 870561842; device verification was not repeated after this rebase. No iOS device verification or on-device streaming/release-race injection. Those edge cases have reducer regression coverage and source review; the Android recording demonstrates the original bottom-gesture defect and its fix. No performance benchmark was run.

Implementation: GPT-6 Astra through the Codex harness in T3 Code. Peer review: Claude Opus 5.5 and Grok 4.7 through Cursor CLI.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 25, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This is a contained mobile scroll bug fix, but it adds a nontrivial state machine spanning native scroll, layout, streaming, momentum, and automatic catch-up behavior. The extensive tests reduce risk, while the asynchronous platform interactions and cross-component runtime changes warrant human review.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review

No actionable comments were generated in the recent review.

Walkthrough

ThreadFeed tracks end position separately from end-follow state. ThreadDetailScreen uses that state to control scroll-to-end button visibility and requests a catch-up scroll when needed.

Changes: Thread feed end-position tracking

Layer / File(s) Summary
Live-follow and end-position rules
apps/mobile/src/features/threads/thread-feed-live-follow.ts
The event type and helper functions resolve end position, decide when geometry can be measured, and identify when scroll completion requires catch-up.
Feed state and scroll handling
apps/mobile/src/features/threads/ThreadFeed.tsx, apps/mobile/src/features/threads/thread-feed-live-follow.test.ts
ThreadFeed reports end-position changes, measures geometry when allowed, and requests catch-up when a scroll ends away from the end after release at the end. Tests cover these transitions.
Screen state and button visibility
apps/mobile/src/features/threads/ThreadDetailScreen.tsx
ThreadDetailScreen receives end-position updates and requests a catch-up scroll. The scroll-to-end button also requires the feed to be away from the end.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Low

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ThreadFeed
  participant ThreadDetailScreen
  User->>ThreadFeed: Scroll and release at feed end
  ThreadFeed->>ThreadFeed: Read current end position
  ThreadFeed->>ThreadDetailScreen: Request catch-up when release was at end and current position is not
  ThreadDetailScreen->>ThreadFeed: Scroll to end without animation
  ThreadFeed->>ThreadDetailScreen: Report end-position change
Loading

Possibly related issues

  • #12754 reports the scroll-to-bottom arrow appearing at the newest message. This change tracks feed end position to control the arrow during a drag at the end.

Possibly related PRs

  • pingdotgg/t3code#24869: Targets the same scroll-to-end arrow behavior and changes the ThreadDetailScreen and ThreadFeed end-state path.

Suggested labels: bugfix

Suggested reviewers: juliusmarminge, macroscopeapp

Merge Risk: Minimal · up to 3bb83

No concrete issue currently prevents merging this mobile scroll-button change. Device verification of the release and streaming interaction remains advisable.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title "fix(mobile): keep scroll-to-bottom hidden during overscroll" matches the main change: the scroll-to-bottom button now reflects measured end position instead of the paused live-follow latch,…
Linked Issues check Passed Only #24869 is directly linked and active (open). Its coding requirements are behavioral: [#24869] the mobile scroll-to-end arrow must stay hidden at the newest message, appear only when follow is pau…
Out of Scope Changes check Passed The reviewed changes stay within the mobile thread-feed scroll-to-end behavior required by [#24869]. The diff touches ThreadDetailScreen.tsx (end-state state, visibility condition, catch-up callback…
Description check Passed The description follows the repository template closely. It has all four sections (Problem, Change, Scope and approval, Verification) and states the defect, the fix, the linked issue and approval cont…
Full details: Title check

Explanation

The title "fix(mobile): keep scroll-to-bottom hidden during overscroll" matches the main change: the scroll-to-bottom button now reflects measured end position instead of the paused live-follow latch, so it stays hidden during bottom overscroll. The changeset also adds release/streaming catch-up handling, which the title does not mention, but that is secondary and expected to be omitted. The title is a conventional commit, short, and specific.

Full details: Linked Issues check

Explanation

Only #24869 is directly linked and active (open). Its coding requirements are behavioral: [#24869] the mobile scroll-to-end arrow must stay hidden at the newest message, appear only when follow is paused and the list is away from the end, and apply to Android and iOS. The reviewed diff meets these requirements. In ThreadDetailScreen.tsx, showScrollToEndButton now requires !endFollowEnabled && !isAtEnd, with isAtEnd supplied by onIsAtEndChange. In thread-feed-live-follow.ts, resolveThreadFeedIsAtEnd keeps the measured end state separate from the live-follow latch. shouldMeasureThreadFeedGeometry returns true only when follow is paused and the scroll is not settling, so content-size and layout events re-measure the end state while a drag holds at the bottom. shouldCatchUpThreadFeedOnScrollEnd covers a final stream chunk that arrives after a release at the end. The change does not alter the follow threshold, which the issue excludes. The issue's proposed LegendList isAtEnd/totalSize subscriptions are an implementation approach, not a requirement; the PR reuses existing callbacks instead. Automated coverage is reported in thread-feed-live-follow.test.ts (43 passing, including overscroll, growth under a held finger, and release/streaming cases). I did not read the test file in this review, so test coverage rests on the PR report and summary. iOS behavior is not device-verified, but the feed is shared and the logic is platform-independent. The #12754 and #13364 references are context only.

Full details: Out of Scope Changes check

Explanation

The reviewed changes stay within the mobile thread-feed scroll-to-end behavior required by [#24869]. The diff touches ThreadDetailScreen.tsx (end-state state, visibility condition, catch-up callback wiring), ThreadFeed.tsx (end reporting and catch-up hooks), thread-feed-live-follow.ts (pure helpers for end state, geometry measurement, and catch-up), and the live-follow test file. Each change supports the visibility rule or its release and streaming edge cases. The catch-up call runs only after a release at the end when a final chunk arrives, which keeps the button accurate when follow resumes. No web or desktop code, follow-threshold logic, or unrelated listeners, polling, or timers appear in the summarized changes. The raw summary is the only source for ThreadFeed.tsx and the test file. I inspected the live-follow helper and the screen diffs directly and found them consistent with that summary.

Full details: Description check

Explanation

The description follows the repository template closely. It has all four sections (Problem, Change, Scope and approval, Verification) and states the defect, the fix, the linked issue and approval context, and detailed verification with commands, results, and limitations. The Problem section gives a reproduction. The Change section explains the approach and why the latch and position are split. The Scope section links #12754 and a maintainer comment, and it openly says the work is an alternative to #24869 with no claimed maintainer decision on that PR. The Verification section lists focused and repo-wide checks, an Android before/after recording, and states what was not checked (iOS, on-device streaming races, rebase re-verification, performance). The template asks for the agent model and harness at the end, and the author gives 'GPT-6 Astra through the Codex harness in T3 Code' in the Verification section, so that requirement is met. The template also asks for a conventional commit title, but the PR title is not part of the description provided here, so that cannot be assessed from this input. Because every required section is substantively filled, this passes.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between c30f1d7 and 3bb83be.

Files selected for processing: 2

  • apps/mobile/src/features/threads/ThreadDetailScreen.tsx
  • apps/mobile/src/features/threads/ThreadFeed.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@shihasbacker

Copy link
Copy Markdown

Note

🤖 Claude Opus 5.5 responding on behalf of @shihasbacker

Independent confirmation of the bug, its root cause, and this approach. One caveat: the device results below come from a separate implementation, not from this branch.

Report: on Android app 2.0.0 and on iOS, starting a drag or overscroll while already at the newest message shows the scroll-to-bottom button above the composer. Desktop keeps it hidden until you actually leave the bottom.

Same cause found independently: handleScrollBeginDrag pauses end-follow before the list moves, and the button was shown whenever follow was paused (!endFollowEnabled).

Same approach, separate code: we tracked LegendList's isAtEnd separately from follow and showed the button only when follow is paused and the list has left the end. On an Android emulator over ADB, with that build:

  • Holding an overscroll at the bottom keeps the button hidden.
  • Scrolling into history shows it.
  • Dragging back to the end, or tapping the button, returns to the end and hides it.

Not checked: iOS on a device, software-keyboard resizing, short threads, and streaming during a held drag. This PR's release-timing and streaming cases go beyond what we covered, so we won't file a duplicate.

@JSMike
JSMike force-pushed the fix/mobile-chat-overscroll-button branch from 8705618 to 54f38ab Compare October 27, 2026 19:27

JSMike commented Oct 27, 2026

Copy link
Copy Markdown
Author

Note

Codex | GPT-6 Astra

Thanks @shihasbacker for the feedback and independent confirmation of the root cause and approach! The Android checks and clear coverage limits are helpful, and I appreciate you keeping the work together rather than opening a duplicate. Hope this lands!

@maria-rcks

Copy link
Copy Markdown
Collaborator

Note

Written by gpt-6.1-sol on behalf of Maria

Merged current main into this branch without rebasing or changing the four-file mobile fix. This brings in main's context-budget update that the old CI run failed against; its other CI jobs were cancelled.

Blacksmith vp run check:changed refs/remotes/github/main passed on c30f1d7eeedd144c5f7c8029d23995af8f502b68: format, lint, both knip passes, mobile/scripts typechecks, all 43 live-follow tests, and compiler checks. Two independent scoped static reviews found no blocking findings.

Native interaction verification on this merged head remains unverified: device access is disabled in this environment, and no Android device is attached. Existing recordings cover an earlier head. Final CI is pending.

Track the measured end position separately from paused live-follow. Preserve button visibility through release settling and catch up once if content grows after release at the end.

Reuse the keyboard scroll view content-size callback and existing settle timer. Add regression coverage for bounce, streaming, release races, and momentum.
Reuse the list's layout and load callbacks to refresh paused position after viewport changes or remounts. Ignore transient false scroll measurements while following so the next drag does not flash the scroll-to-bottom button. Preserve fresh release measurements for the existing catch-up path.
@JSMike
JSMike force-pushed the fix/mobile-chat-overscroll-button branch from c30f1d7 to 3bb83be Compare November 8, 2026 19:33

Copy link
Copy Markdown
Member

Thanks @JSMike for the careful fix and regression coverage. #28125 (4df0c1302a) replaced the custom follow and release state with Legend List’s end tracking: the button now uses isAtEnd independently of following, and the list maintains the streaming end itself. I’m closing this as covered by that change. If a release or streaming case still reproduces on current main, a small new PR focused on that case would be welcome.

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

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants