Skip to content

feat(chat): show the unread count in the chat row badge - #1553

Merged
bmc08gt merged 4 commits into
code/cashfrom
feat/chat-unread-count
Sep 24, 2026
Merged

bmc08gt merged 4 commits into
code/cashfrom
feat/chat-unread-count

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

The chat list's unread badge was an 8dp dot: UnreadBadge took a count and never drew it. It now shows the number, as in node 10329:8245, and the number is the real count: unreadCount returned 0 or 1, so every unread chat would have read "1".

  • UnreadBadge renders the shared Badge from ui/components, the one the Chats tab already uses, with indicator as the fill and background as the text colour. The digits are tracked in by 0.06em. One digit is an 18dp circle; "15" measures 21.7×18dp at 412dp width, against the design's 22×18.
  • Badge takes contentPadding and an optional height, with defaults that keep the tab bar and ChatNode as they were. The unread pill needs both: grid.x1 scales with window width and staticGrid.x1 is 5dp, so neither gives the design's 4dp. Avenir's 12sp line is about 16dp and doesn't shrink for a smaller lineHeight, so 2dp of vertical padding lands at 20dp; the pill sets 18dp directly.
  • ChatRowTrailing drops the badge's start padding, so the gap to the timestamp is the title row's 4dp spacing rather than 8dp.
  • unreadCount (FeedProjection) subtracts the unreadSeq stamp on the message the READ pointer names from the newest message's stamp; with no pointer, the newest stamp is the count. The feed reads that stamp from chat_messages when it builds (ChatState.readStamps), and the paged feed per row. When the pointer's message isn't stored, the feed fetches it with GetMessage and keeps its stamp in memory for the session (ChatState.fetchedReadStamps), never in chat_messages. The count is null, drawn as the old plain dot, until that lands, when the server returns NOT_FOUND, or when the newest stamp is at or below the pointer's. The tab's unread total still counts a null. Same rules as iOS's Conversation.unreadCount.
  • TipChatRow gets a preview with a read row and rows at 2 and 15 unread. TipChatRowScreenshotTest adds a 15-unread row.

The timestamp keeps its caption style and still turns indicator blue when a chat has unread messages. Counts over 100 read "101+", Badge's existing behaviour and the same as the tab bar.

The row's unread badge was an 8dp dot that ignored its count. It now
renders the shared Badge in the indicator colour with the count in the
background colour, 18dp tall, as in node 10329:8245. The badge's start
padding is gone so the gap to the timestamp is the title row's 4dp.
@bmc08gt bmc08gt self-assigned this Sep 24, 2026
@github-actions github-actions Bot added the type: feature New functionality label Sep 24, 2026
The pill drew 5dp of horizontal padding on a Pixel 10, where grid.x1
scales with width and staticGrid.x1 is 5dp, and came out 20dp tall
because Avenir's 12sp line is about 16dp and doesn't shrink for a
smaller line height. Badge takes contentPadding and an optional height;
UnreadBadge passes 4dp horizontal and a fixed 18dp. Defaults are
unchanged for the tab bar and ChatNode.
@github-actions github-actions Bot added the area: ui Compose UI, theme, components, resources label Sep 24, 2026
The row badge showed "1" on every unread chat on a device: unreadCount
returned 0 or 1, and only the previews passed it anything larger.

Each message carries the server's unreadSeq, a running count of
unread-eligible messages, so the count is the newest message's stamp
less the stamp on the message the READ pointer names. The feed reads
that stamp from the message table when it builds, and the paged feed
per row. With no pointer, the newest stamp is the count.

The count is null, drawn as the old plain dot, when it can't be known:
the pointer's message isn't stored (after a database rebuild), or the
newest stamp is at or below the pointer's. The tab's unread total still
counts those chats. This matches iOS's Conversation.unreadCount.
When the message a chat's READ pointer names isn't in the message
table, as after a database rebuild, the row showed a dot with no count.
The feed now fetches that message with GetMessage (view mode FULL) and
counts from its unreadSeq, so every unread row gets an exact number.

Fetched stamps live in ChatState for the session and are never written
to chat_messages: a lone message there reads as part of the transcript.
A stored stamp wins over a fetched one. Only unread chats with a READ
pointer fetch, one request per chat and message at a time. NOT_FOUND
isn't asked for again this session and keeps the dot; any other failure
is retried on the next feed build. DENIED and NOT_FOUND log at info.

This matches iOS, where the fetch runs per visible row. Here it runs
from the feed build, for every chat that needs one.
@bmc08gt
bmc08gt merged commit 4435f26 into code/cash Sep 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ui Compose UI, theme, components, resources type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant