# fix\(mobile\): folding the Duo keeps the thread's end or the reader's line in place · PR \#23149 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23149)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: duo/feed-anchoring

Target branch: duo/search-observer

Head commit: 2b3f7fbf39e2eb82d540e41553db84c46c8760f7

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/23149)

[Commits](https://git.cafe/pingdotgg/t3code/pull/23149/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/23149/diffs?format=markdown)

## Description

---

Folding or unfolding the iPhone Duo re-wraps every row of the thread feed, and LegendList re-measures them over several commits. A feed following the end pinned it once, then relied on `maintainScrollAtEnd`, which scrolls a frame or more after each re-measure, so earlier text showed in between. A reader's row kept its top while its text moved, and the feed restored the reader's line 200 ms after the rows settled, which was a visible hop. This layer builds on #22978, which first kept the feed's place across width changes.

LegendList now takes a `sizeChangeAnchor`: the scroll offset a size change should leave, read in the same pass that repositions the rows. It is applied through the scroll adjustment LegendList already uses for MVCP, so the native offset moves in the same commit as the new positions. The patch also:
- commits the content height together with those positions, not ahead of them
- lets a shrinking list run the pass too
- skips the delayed end scroll while an anchor holds
- adjusts only past the offset that shrinking content already pulls back to its end
- still runs end maintenance after data-change passes, which don't apply the anchor, so a row added while a fold settles scrolls into view

On iOS the feed holds the end while following, and otherwise holds the reader's line at the same share of its row, from the first width step of a fold until 600 ms after the last. Android keeps its after-the-fact restore, because its list moves only while MVCP is on, and following turns MVCP off.

This layer also stops a thread re-render from turning following back on. The feed's reset effects listed its own handlers as dependencies, so any parent re-render that passed a new follow callback reset following and its scroll session. The resets now run as an effect event, keyed by the thread and the submitted message.

## Verification

- `thread-feed-live-follow.test.ts` covers the reset and anchoring rules.
- Following the end, then reading mid-thread, through an unfold and a fold. This is the same flow as #22978's videos, at this PR's base `fdfc6ea1c15` and head `5b5e9062d3e` (restacked commits), on one development client built from the stack's tip on current main (fingerprint `a6c8232e`). Both sides run on it, because this layer's native inputs don't change (the LegendList patch is JS):

![Before: unfolding shows earlier messages before the feed returns to its end](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ee21dda00b96d6ba/pr23147-anchoring-BEFORE-fdfc6ea1c15.mp4)

![After: the feed holds its end and the reader's line](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ec5a1d0ced31ad19/pr23147-anchoring-AFTER-5b5e9062d3e.mp4)

## Known limitation

Unfolding still shows brief residue because UIKit re-wraps rows before LegendList repositions them. Measured at this layer:
- **Reading mid-thread:** a user bubble draws over the reply above it for about 52 ms per width step.
- **Following the end:** the last line sits 101 pt above the composer, instead of 32 pt, for about 150 ms.

Removing that needs LegendList to position rows without a React commit, a larger change deferred for now.

Implemented with Claude Opus 5.5 in Claude Code, running in T3 Code.

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

