Skip to content

feat(chat): jump to the unread line with a Catch up pill - #29601

Open
chrisnojima wants to merge 3 commits into
masterfrom
nojima/HOTPOT-catch-up
Open

feat(chat): jump to the unread line with a Catch up pill#29601
chrisnojima wants to merge 3 commits into
masterfrom
nojima/HOTPOT-catch-up

Conversation

@chrisnojima

Copy link
Copy Markdown
Contributor

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 onViewableItemsChanged to 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 ItemSeparatorComponent is handed {highlighted, leadingItem} and never a trailingItem, which SeparatorConnector requires, so it returned null every time. The native list casts FlatList to Record<string, unknown>, which is why the prop mismatch never reached tsc. NativeSeparator adapts it: 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 the message desktop passes as trailingItem.

Known gap left in place: VirtualizedList gives 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

  • No mark-as-read behaviour is added. One inherited consequence of the centered load: Jump to recent shows at the bottom until you scroll back to the newest message, same as after a search jump.
  • separator.test.tsx joins the existing tsconfig.native.json exclude list for DOM-asserting tests; desktop tsc still checks it.

Testing

  • New unit tests: the show/hide predicate and dismissal (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:all clean, 0 react-compiler bailouts and 0 whole-props deps.
  • Verified by hand on desktop and the iOS simulator.

This comment was marked as outdated.

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
chrisnojima force-pushed the nojima/HOTPOT-catch-up branch from 98bb2b5 to 9431e61 Compare September 9, 2026 13:55
>
<Kb.Icon type="iconfont-arrow-full-up" color={theme.whiteOrWhite} sizeType="Small" />
<Kb.Text type="BodySmallSemibold" style={styles.label}>
Catch up

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you try showing the number of unread messages or is it too redundant with badging?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants