Files
buzz/mobile/lib
Tom Brow 439d7bcc12 fix(mobile): keep a sent thread reply above the composer (#9026)
Fixes two thread symptoms that come from the same state: after sending a
reply, the composer clears but the new reply sits hidden under the
composer (or out of view entirely) until you leave and return, and the
**Latest** button stays visible even though the list looks like it is at
the bottom.

### Cause

1. A sent reply grows into place over its 500 ms entrance transition
(`LocalSendRow`). The thread's one-shot tail correction can finish while
the row is still growing. Once it has finished, nothing moves the list
as the reply keeps growing, so the list stops short of the real end
(about 40 pt on an iPhone 17 Pro simulator) and the reply ends up behind
the composer. Because the list isn't actually at its end, **Latest**
shows. It is visible even with the composer focused when an earlier drag
detached tail-follow.
2. A thread opened at a specific reply (from Inbox, a push notification
or a message link) skipped the new-reply follow logic for its whole
lifetime, not just for the replies it opened with. If the reader
scrolled at all before replying, their own sent reply was never brought
into view: it landed below the viewport, and a quick swipe couldn't
reach it before the next layout.

### Fix

- While the thread follows its tail, a `ScrollMetricsNotification`
listener keeps a position that was at its previous end on the new end
whenever the extent grows. It only acts when the last reply is laid out,
the reader isn't dragging or opted out, and no navigation to the tail is
in progress, so programmatic jumps away from the tail are left alone.
This also covers other growth at the tail, such as media or typing rows.
- A targeted entry now absorbs the replies it hydrates with until its
target jump has revealed the list. Hydration can come in two steps (the
route's snapshot of the target, then the relay's full thread). Absorbing
only the first let the second, when it held the reader's own earlier
replies, schedule a tail correction that cancelled the one-shot target
jump and left the list hidden behind its loading placeholder. Later
arrivals use the ordinary rules: the reader's own reply comes into view,
and other people's replies still don't move a reader who is away from
the tail.

### Before and after

Same flow on an iPhone 17 Pro simulator, in a synthetic QA channel
thread: scroll the list away from the bottom, focus the composer, send a
long reply that wraps onto many lines. Before (main) the sent reply ends
with its bottom edge under the composer; after (this branch) it ends
clear above it.

Before (main):


https://github.com/user-attachments/assets/5101b63d-e2df-46d1-bfd1-49038330ddf5

After (this branch):


https://github.com/user-attachments/assets/1df81ad5-bb6b-4df6-b4e3-5c37fd8aab67

### Validation

- Test `a sent reply stays above the composer as it grows in` now runs
for a plain entry, a targeted entry (`initialMessageId`), and a targeted
entry whose relay hydration is delayed and includes the reader's own
replies. The targeted row opens at an older reply, checks it is visible,
drags a little, focuses the composer with the keyboard up, sends a
multi-line reply, and checks that the reply ends above the composer and
**Latest** is hidden. Each row fails without its fix (growth: reply
bottom at 504, composer top at 402; targeted: the reply is never
followed; delayed hydration: the target never becomes hit-testable
because the list stays hidden).
- Full `flutter analyze` and mobile test suite pass.
- Before/after capture on an iPhone 17 Pro simulator (iOS 27, debug
builds of this branch head and of current main), in a synthetic QA
channel thread with enough replies to scroll. Flow: scroll away from the
bottom, focus the composer, send a long wrapping reply. Over 7 runs on
main, the reply's bottom ended 3 to 25 pt under the composer in 5; over
6 runs on this branch, it ended 17 pt above the composer every time.
Limitation: debug simulator builds only, and the shortfall on main
varies run to run.
- Automated full-app run on an iPhone 17 Pro simulator (iOS 27) against
the Remote Agents Pilot community. In a test DM thread, I sent short and
wrapping replies with no prior scroll, after dragging up and returning,
and after the follow-up narrowing. Each reply ended fully above the
composer, and **Latest** stayed hidden at the bottom. I also dragged up
with the keyboard dismissed and confirmed the list stays at the tail
with no stray **Latest**. For the targeted entry, I opened a thread from
a message link, scrolled, and sent replies: without the targeted-entry
fix they stayed out of view under the keyboard; with it hot-reloaded
they were followed (the combined build is covered by the widget test
rows above). I then reopened that link right after an app restart, which
hydrates in two steps: before the hydration fix the thread showed only
its root with the replies hidden, and with it the list revealed at the
target and a sent reply came into view. Human testing is waived per the
owner's standing exception. Limitation: this run did not cover physical
devices or media replies.

The simulator recording was taken in an internal community, so it is
shared internally rather than embedded here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
2026-11-09 06:58:43 -08:00
..