fix(mobile): Android feed stays at its end after the keyboard or sidebar changes#33611
Stacked on #33599 (t3code/android-workspace-sequencing), so one build tests both.
Problem
On Android, start at the true end of a long thread. Focusing the composer, or toggling the sidebar once, leaves the feed short of the end: the last reply and its timestamp footer stay under the composer, even after the keyboard closes. Opening a thread also landed short. iOS was fine.
Cause
ThreadFeed passed the composer height as a static contentInset on Android only (added in #5585 to seed the first open). Under Legend List 3.7.2 that prop now works against us twice:
- Legend's core end math (
getContentInsetEnd) ignoresprops.contentInsetoff iOS, and RN's Android ScrollView has nocontentInset. KeyboardAwareLegendListsubtracts the staticcontentInset.bottomfrom every padding report fromKeyboardChatScrollViewon non-iOS.
So Legend was told the composer padding was 92 − 92 = 0. The native scroll view still pads by the composer height, which puts the true end one composer height past where Legend thinks it is. Every end target came out short by that amount: initial open, keyboard settle, and the width-change end hold from #33459. Instrumented emulator logs before the fix: Legend's end was 2877, the native end reached by swiping was 2969, and the reported inset was 0. After the fix both are 2969, with an inset of 92.
Change
Drop the Android-only static inset (deriveThreadFeedInitialContentInset and its test). The list already reports the live composer inset when it attaches (attachList), which covers the first open and remounts. iOS is unchanged: the helper always returned undefined there.
Verification
Local Pixel_10_Pro_Fold emulator (unfolded, Gboard), dev build, isolated backend, seeded replay thread. The same script ran before (#33599 head c82a436) and after. Frames left to right: true end, keyboard open, keyboard closed, true end again, sidebar collapsed, sidebar expanded.
Before: after the keyboard opens or the sidebar changes, step 24's reply and its footer stay hidden under the composer.
After: the last reply and its footer stay visible in every state.
Recordings (1.5×, feed column only):
- Before: https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3b8781cf99cc061f/feedend-before-crop.mp4
- After: https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c52f60229742ea75/feedend-after-crop.mp4
There's no test. The bug is in how two Legend modules disagree about a prop, so the only test available would mirror mock calls; the recordings are the proof. The iOS code path doesn't change. vp run check:changed passes.
Follow-up: dismissing the keyboard by tapping message text
Problem. Testing on the Fold: with the keyboard open at the end of a thread, tapping the empty area to dismiss kept the end, but tapping a message's text left the feed scrolled up.
Cause. Message text is a natively selectable TextView. A tap on it moves focus from the composer to that TextView, and React Native's Android ScrollView scrolls a newly focused child into view (requestChildFocus → scrollToChild). For a reply taller than the viewport, that's a large native scroll with no drag. Logs on the emulator: the offset jumped 833 → 376 right after the tap, before any inset change, and Legend's updateScroll treated it as a user scroll (interruptMaintainScrollAtEnd). End following was therefore off when the keyboard inset shrank. Taps on empty space take no focus, so they kept the end. Both paths run the same dismiss code (onTouchEnd → blur()), and neither starts a drag, which rules out the other two hypotheses. A short reply that's fully on screen doesn't need a scroll, so it only shows with replies taller than the visible area.
Change. scrollsChildToFocus={false} on the feed list. It's an Android-only ScrollView prop, so iOS is unchanged. Long-press text selection still works.
Verification. Same emulator, a seeded thread whose last reply is taller than the viewport, and a keyboard-open tap on the reply's text or on the blank footer row. Frames: text tap with keyboard open, after; blank tap with keyboard open, after.
Before (eaea60b): the text tap leaves the feed on Paragraphs 5–11, while the blank tap keeps Paragraph 14 and its footer.
After (8faf945): both taps keep Paragraph 14 and its footer above the composer.
- Before recording: https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f42f230cb861268c/fd-before2-crop.mp4
- After recording: https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8d24be6d15c2163e/fd-after2-crop.mp4
Long-press selection still works after the change:
Stack update: tested on the Fold
Rebased onto #33599's new head (5965c6c), where the sidebar stays put while the keyboard is up and the thread header keeps its height while typing. That removes the mid-history reflow reported on the Fold: the pane width no longer changes with the keyboard, so rows don't re-wrap when the keyboard opens or closes. The two commits here are unchanged.
Full stack on the physical Galaxy Z Fold (dev client, seeded thread whose last reply is taller than the viewport, sidebar titles blurred). Frames: at the end; keyboard open; keyboard closed; text-tap dismiss; sidebar collapsed; sidebar expanded. Each one keeps the last paragraph and its footer above the composer. With the keyboard up, the sidebar stays.
Mid-history, pressing the feed with the keyboard open or closed didn't move it in any run (offset stable within a few px, no end-following interrupt). The keyboard-open mid-history case is in #33599.
🤖 Generated with Claude Code
Checks
Failing1
- CodeRabbitGitHubCancelled
Approvability
Verdict: Approved at fd7e211
Macroscope's review found this PR approvable — This adjusts Android thread-feed inset and focus scrolling to preserve end-following when the keyboard or sidebar changes. Its effects are limited to feed presentation, with no persistent-data or access changes.
Notes:
- Diff unchanged. Approvability was decided on eligibility alone.
You can add or adjust custom eligibility rules. Learn more.
🚀 Expo continuous deployment is ready!
- Project → t3-code
- Platforms → android, ios
- Scheme → t3code-preview
| 🤖 Android | 🍎 iOS | |
|---|---|---|
| Fingerprint | 9fd8656574d836abb66594826ddf766639eff49c | bfb4edba8f9a306e923f1329a6994f1c94bd42cb |
| Build Details | Build PermalinkDetailsDistribution:INTERNALBuild profile: preview:devRuntime version: 9fd8656574d836abb66594826ddf766639eff49cApp version: 2.2.0Git commit: 6ec4424ae511af7e385772bafda78b0e7da9139b | Build PermalinkDetailsDistribution:INTERNALBuild profile: preview:devRuntime version: bfb4edba8f9a306e923f1329a6994f1c94bd42cbApp version: 2.2.0Git commit: 9c5fd30e0fc4a3fd05519e5c8612fe560dab77bd |
| Update Details | Update PermalinkDetailsBranch:pr-33611Runtime version: 9fd8656574d836abb66594826ddf766639eff49cGit commit: 6ec4424ae511af7e385772bafda78b0e7da9139b | Update PermalinkDetailsBranch:pr-33611Runtime version: bfb4edba8f9a306e923f1329a6994f1c94bd42cbGit commit: 6ec4424ae511af7e385772bafda78b0e7da9139b |
| Update QR | External image | External image |
Added 2 commits
Finishing Touches
-
Generate docstrings
-
- Commit to this branch
- Create a new PR
Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts