feat(chat): jump to the unread line with a Catch up pill - #29601
Open
chrisnojima wants to merge 3 commits into
Open
feat(chat): jump to the unread line with a Catch up pill#29601chrisnojima wants to merge 3 commits into
chrisnojima wants to merge 3 commits into
Conversation
When the orange unread line sits above the viewport, show an orange
"Catch up" pill at the top right of the thread. Tapping it centers on the
oldest unread message using the existing search-jump path with the
highlight turned off, then dismisses itself for that unread line.
Ordinals are monotonic and the unreadline is a MessageID carried as one,
so comparing it against the oldest visible ordinal covers both "scrolled
off above" and "not loaded at all" with one predicate. Desktop's
LegendList had no viewability wiring, so it gains onViewableItemsChanged
to report the same signal mobile already tracks.
Also fixes the orange line never rendering on mobile at all. React
Native's ItemSeparatorComponent receives {highlighted, leadingItem} and
never trailingItem, which the separator required, so it bailed out every
time. The native list casts FlatList to Record<string, unknown>, which
hid the mismatch from tsc. NativeSeparator adapts the prop: the
separator belongs to cell i and sits between items i and i+1, and this
list reverses its data, so item i is the newer of the pair -- exactly
what desktop passes as trailingItem.
ClickableBox is a bare div on desktop, so the pill was unreachable from the keyboard. Add an opt-in asButton prop for button semantics and enter/space activation rather than turning every clickable row into a tab stop.
React 19 renders a context itself as the provider. The production chat tree already did this; the rest of the app and the tests were still on the legacy form.
chrisnojima
force-pushed
the
nojima/HOTPOT-catch-up
branch
from
September 9, 2026 13:55
98bb2b5 to
9431e61
Compare
zoom-ua
approved these changes
Sep 11, 2026
| > | ||
| <Kb.Icon type="iconfont-arrow-full-up" color={theme.whiteOrWhite} sizeType="Small" /> | ||
| <Kb.Text type="BodySmallSemibold" style={styles.label}> | ||
| Catch up |
Collaborator
There was a problem hiding this comment.
did you try showing the number of unread messages or is it too redundant with badging?
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.
What
When the orange unread line is scrolled above the viewport, an orange Catch up pill appears at the top right of the thread. Tapping it jumps to the oldest unread message so you can read downward, then dismisses itself.
It reuses the thread-search jump path with the highlight mode set to
none, so the scrolling behaviour is the one that already exists rather than a second implementation of it.How it decides to show
Ordinals are monotonic and the unreadline arrives as a MessageID carried as an ordinal, so one comparison against the oldest visible ordinal covers both cases: the line is loaded but scrolled off above, and the line isn't in the loaded window at all. A partially visible row counts as visible, which gives a row of natural hysteresis with no pixel math.
Hidden while thread search is open (both would be fighting over the same center slot). Dismissal is local to the conversation mount and nothing is persisted; it re-arms if the unread line moves, e.g. after Mark as unread.
Desktop's LegendList had no viewability wiring, so it gains
onViewableItemsChangedto report the signal mobile already tracked. Desktop data is oldest-first so the oldest visible row is the first viewable item; mobile's is reversed, so it's the last.Also: the orange line never rendered on mobile
Found while testing this on the simulator, and pre-existing rather than caused by it — Mark as unread didn't draw a line either.
React Native's
ItemSeparatorComponentis handed{highlighted, leadingItem}and never atrailingItem, whichSeparatorConnectorrequires, so it returned null every time. The native list castsFlatListtoRecord<string, unknown>, which is why the prop mismatch never reached tsc.NativeSeparatoradapts it: the separator belongs to celliand sits between itemsiandi+1, and this list reverses its data, so itemiis the newer of the pair — exactly the message desktop passes astrailingItem.Known gap left in place:
VirtualizedListgives the last cell no separator, which in reversed data is the oldest loaded message, so an unread line belonging above that row still won't draw on mobile.Notes
separator.test.tsxjoins the existingtsconfig.native.jsonexclude list for DOM-asserting tests; desktop tsc still checks it.Testing
catch-up.test.tsx), and the separator drawing an orange line for both the desktop and native prop shapes (separator.test.tsx). The native case was written failing first and went green with the adapter.yarn test:unit chat/conversation— 42 suites, 382 tests passing.yarn lint:allclean, 0 react-compiler bailouts and 0 whole-props deps.