Repository navigation
feat(messenger): jump-to-bottom button with unread badge - #1783
Merged
Merged
Conversation
Floating glass button shown while the transcript is off the newest row. Its badge counts inbound messages past the viewer's stored READ pointer, the pointer the read reporter advances, using the unread divider's query as a Flow so the two start equal and the badge falls as rows are read. Adds a badgeFocus theme color (#0A84FF).
The count stream read the account id once and fell back to an empty flow when the per-user database was missing, so a screen that subscribed before login finished stayed at 0 for the session. Both inputs are now resolved reactively, as hasEverTipped does, and resubscribe when either changes.
The visibility derivation was remembered without a key, but selecting is a plain Boolean, so it kept the value from its first read and the button stayed up behind the selection backdrop.
… until the viewer scrolls The badge opens at the unread divider's own count instead of subtracting rows already read on screen, and rises by one for each inbound message that arrives after the open. It follows the live count after the first drag or fling on the settled transcript, or once the viewer's own message is confirmed. Scrolls the list makes itself do not release it.
…pshot newestInboundId and resolveUnreadBoundary returned null/None while the account id or the per-user database was missing. The view model mapped null to -1, so once both came up every inbound row in the chat counted as an arrival. Both now suspend until ready, so -1 only means no inbound at open.
… the newest inbound The frozen badge counted as an arrival every inbound message past the newest inbound id stored at open. With an empty cache that id is absent, so history fetched after the open counted as arrivals and inflated the badge. The cutoff is now the later of the viewer's read pointer and the newest inbound id, both read after the account and database are ready. History at or below the pointer is already read, so it can load late without counting. The test that asserted every later inbound counts with no inbound at open encoded the bug. It now covers a read pointer with an empty cache, and the no-pointer, no-inbound case keeps counting everything.
The cutoff was read before the opening sync, so with an empty cache the history that sync stored counted as arrivals. It is now read after openTranscript returns, and nothing counts as an arrival until then. A message that arrives during the load is not counted. The frozen badge also read its divider term from unreadBoundary, which the UntilSend lifetime clears when a message is sent, before the send succeeds. The opening count is now snapshotted into state at open, so a failed send leaves the badge where it was.
… recording the badge cutoff The cutoff was read after openTranscript returned, which also happens when the load fails or is skipped (group feed not synced, outside readers load elsewhere), so history stored later counted as arrivals. ChatCoordinator.newestPageLoads() now emits when a newest-page load is stored, from any path; the view model records the cutoff on the first emission for its chat.
…load Drives the real ChatViewModel open path: no cutoff on open, none after a load for another chat, recorded once this chat's newest page lands (history stored by then is behind it), and not moved by later loads.
The painted fade covered the newest message rows, and the button's glass blur already separates it.
The jump button used the plain inset as its end padding while the $ button sits at the bar's side inset and is wider, so their centres differed. Its end padding is now the composer's side inset plus half the width difference.
…er queries unchanged Adds a ChatMessageDaoTest case that inserts a senderless System row and checks the inbound count, newest inbound id, first-unread id and own-send range are the same as before.
…one layout-phase inset Both read the margin through ComposerInsets while laying out, so a keyboard animation frame moves the jump button and the $ button together. The old composition-time end padding in MessageList is gone.
The button built its own blur, skipped the composer's container fill and rim gradient, and carried a shadow the other surfaces don't have. It now uses a shared chatGlassMaterial() with the message bar, the $ button and the join-requests card.
bmc08gt
added a commit
that referenced
this pull request
Oct 15, 2026
#1783 made opening a chat wait on newestPageLoads().first { }. The test's relaxed ChatCoordinator returns a Flow that completes empty, so first threw NoSuchElementException and both tests failed on code/cash. Stub it with a MutableSharedFlow, as ChatOpenTranscriptTest does.
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.
Adds a floating jump-to-bottom button to the chat transcript. It carries a blue badge counting the inbound messages below what you've seen, and the count goes down as you scroll toward the bottom.
How the badge counts
messageReceived/tipReceivedfor each inbound message it crosses. That's existingreportCrossedMessagesbehaviour, and iOS does the same.ChatMessageDaoTesthas a case for this.The button
ComposerInsets.sidePx), so they move together while the keyboard opens and closes. A 30fps recording on an emulator showed a 0px gap between their centres on every frame. Before this change the jump button trailed by up to 9px.chatGlassMaterial()inChatGlass.kt, so the four can't drift apart.Design: Figma node 11721:806 (Option A). The decision notes are in node 11722:207.