fix(chat): desktop thread search jumps to the right message#29399
Merged
Conversation
Clicking a thread-search hit (or a reply-quote / pinned-message jump) left the desktop thread blank, then landed in the wrong place once unblanked, then blinked the yellow highlight. Three separate causes: Blank thread: centered loads call messagesClear() before refetching, so LegendList's data goes non-empty -> empty -> non-empty. That transition trips shouldResetFreshDataLayout, which resets its layout state and then waits for a container layout event that never arrives, leaving readyToRender false forever. Bump a clearVersion on every messagesClear and key the list off it, so a cleared thread remounts and takes the fresh-mount path instead of the broken reset path. Wrong position: the scroll-to-center was a one-shot scrollIntoView, which writes scrollTop behind LegendList's back and desyncs its internal scroll state; its next position recompute then snapped somewhere unrelated, worst with tall image rows. Correct through its own scrollToOffset instead, in a closed loop that measures the row's real offset from the viewport center until it holds still. Also stop disabling maintainVisibleContentPosition while centered: the full thread response lands after the cached one and re-measures rows above the target, sliding it out of view unless the content is anchored. Highlight blink: LegendList's useDOMOrder re-sorts container elements 500ms after positions settle so DOM order matches visual order. A DOM move restarts CSS animations on the moved subtree, replaying the highlight fade from transparent long after it finished. Freeze the row into the animation's end state once it has played, so there is nothing left to restart.
chrisnojima
added a commit
that referenced
this pull request
Jul 9, 2026
dataKey is not a substitute for the key={listKey} remount that #29399 added.
A dataKey change sets shouldResetFreshDataLayout, which calls
resetInitialRenderState({resetLayout: true}): readyToRender drops to false and
the list then waits on a container layout event that never arrives, so the
thread renders blank forever. A remount escapes that because the reset is
guarded on !isFirst.
Desktop cannot use dataKey here at all, not just on clear: the layoutReady rAF
gate deliberately feeds an empty data array on every listKey change, and
previousDataLength === 0 is the other trigger for the same reset.
Repro: search a thread and jump to a hit (a centered load clears the thread
before refetching, bumping clearVersion). Fixed and confirmed on desktop.
chrisnojima
added a commit
that referenced
this pull request
Jul 9, 2026
…ights (#29401) * fix(chat): stop recycled message rows from leaking state and stale heights These are list-agnostic row fixes, split out of the native LegendList work. Desktop already renders the thread with a recycling LegendList, so each of these is a live bug there today; on the native FlatList they are inert guards. - per-row state (sent animation, reaction picker, exploding retained height, ash tower) is now keyed to messageKey. A recycled container reuses the component instance for a different message, so mount-captured state leaked across rows: the picker stayed open on the wrong row, the sent-animation wrapper stuck on, and a measured retain height was applied to the wrong message (and could not self-correct, since retainHeight forces the style height so onLayout only ever reports the forced value back) - recycle-pool suffixes are limited to ones that are stable for the message's lifetime (:failed, :reply). The pool label is recorded when a container is allocated and never updated in place, so :pending (flips on every send confirmation) and :reactions (toggles) left stale labels behind and recycled containers painted at the wrong pooled height - getItemType splits headered rows into their own pool (:hdr). A message that leads its author group is ~40px taller than a grouped follow-on of the same render type, so a shared pool paints recycled views at the wrong height for a frame. It reads the same sticky username cache the rows render with, else a row that keeps its header after a scroll-back load is typed headerless and poisons the headerless pool's height average - useSyncRowLayout: when a row's content settles to a new height after first paint (flip result streams in, reactions appear, an unfurl loads), flush the row measure synchronously so the list's bottom re-pin uses the final height on the same frame instead of a frame late, which otherwise parks the thread above the newest message - SwipeableRow takes an `enabled` prop that conditionally spreads its pan handlers, so a list can shed per-row touch evaluation during a fast fling without unmounting the row. Toggling the Swipeable's subtree instead would remount its children and flash images. No caller passes it yet - desktop LegendList: dataKey replaces the key remount on conversation switch, and maintainScrollAtEnd opts back into footerLayout, which 3.x stopped implying once the trigger set is given explicitly * fix(chat): keep the desktop LegendList remount on clear dataKey is not a substitute for the key={listKey} remount that #29399 added. A dataKey change sets shouldResetFreshDataLayout, which calls resetInitialRenderState({resetLayout: true}): readyToRender drops to false and the list then waits on a container layout event that never arrives, so the thread renders blank forever. A remount escapes that because the reset is guarded on !isFirst. Desktop cannot use dataKey here at all, not just on clear: the layoutReady rAF gate deliberately feeds an empty data array on every listKey change, and previousDataLength === 0 is the other trigger for the same reset. Repro: search a thread and jump to a hit (a centered load clears the thread before refetching, bumping clearVersion). Fixed and confirmed on desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Clicking a thread-search hit (or a reply-quote / pinned-message jump) left the desktop thread blank, then landed in the wrong place once unblanked, then blinked the yellow highlight. Three separate causes:
Blank thread: centered loads call messagesClear() before refetching, so LegendList's data goes non-empty -> empty -> non-empty. That transition trips shouldResetFreshDataLayout, which resets its layout state and then waits for a container layout event that never arrives, leaving readyToRender false forever. Bump a clearVersion on every messagesClear and key the list off it, so a cleared thread remounts and takes the fresh-mount path instead of the broken reset path.
Wrong position: the scroll-to-center was a one-shot scrollIntoView, which writes scrollTop behind LegendList's back and desyncs its internal scroll state; its next position recompute then snapped somewhere unrelated, worst with tall image rows. Correct through its own scrollToOffset instead, in a closed loop that measures the row's real offset from the viewport center until it holds still. Also stop disabling maintainVisibleContentPosition while centered: the full thread response lands after the cached one and re-measures rows above the target, sliding it out of view unless the content is anchored.
Highlight blink: LegendList's useDOMOrder re-sorts container elements 500ms after positions settle so DOM order matches visual order. A DOM move restarts CSS animations on the moved subtree, replaying the highlight fade from transparent long after it finished. Freeze the row into the animation's end state once it has played, so there is nothing left to restart.