Repository navigation
feat(chat): mark where unread messages start with a divider - #845
Merged
Merged
Conversation
Opening a chat now draws an "N Unread Messages" band above the first message someone else sent after the viewer's READ pointer, matching Android. The boundary is read once, in the load coordinator's init, before the opening task's markRead advances the pointer, and later arrivals do not move it or change its count. A missing self member row resolves to no divider rather than treating the whole transcript as unread, which is what hasUnread would do: a group roster is paged, so the row can be absent. With more unread than fits on screen the chat opens with the divider at the top of the visible area; otherwise it opens at the bottom as before. The loader grows its window back to the read-through message when there are at most 200 unread. UnreadDividerLifetime.current keeps the divider until the chat closes. Setting it to .untilSend clears it on the viewer's first send instead. UnreadDividerTests encodes the table of cases Android shares, run through the database, the stored member row and the mapper. Placement follows the spec's "Own message first after pointer" row, which its pairwise formula cannot produce: the divider heads the first message after the pointer that someone else sent, found by id range so a deleted read-through message still places it.
Android (code-android-app#1546) handles the "Own message first after pointer" row at resolution rather than in the placement rule: the read-through becomes one below the first message after the pointer that someone else sent, deleted ones included. For 1:other 2:self 3:other with pointer 1 that gives readThrough 2, count 1. iOS now resolves the same way through Database.firstInboundMessageID, and the mapper goes back to the pairwise rule, so both platforms compute the same boundary rather than two rules that agree only on the table. The divider is restyled to match: a plain caption in textSecondary between two 1pt hairlines, inset to the transcript margins, instead of a filled band with bold text. When a date separator heads the divider, the opening scroll top-aligns the date instead of the divider. Aligning the divider hid the date under the navigation bar.
With no stored message at or below the resolved read-through, the store has a gap under the unread run: older unread messages may exist that it never fetched, so the count could be short. Resolve to no divider there, and add the case to the shared table.
The label used Avenir Regular 12. Android's divider uses the caption token, whose Medium weight loads avenir_next_demi, so it renders in Avenir Demi 12. appTextCaption is the same face and size on iOS, and the date separator above the divider already uses it.
bmc08gt
added a commit
that referenced
this pull request
Sep 24, 2026
…-avatars * origin/main: feat(chat): mark where unread messages start with a divider (#845) feat(chat): thank the sender when a cash link from chat is collected (#843) fix(notifications): treat a lost WAL lock race as contention in the extension store (#842) # Conflicts: # FlipcashCore/Sources/FlipcashCore/Models/Chat/ChatItem.swift
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.
Opening a chat now draws an "N Unread Messages" band above the first message someone else sent after the viewer's READ pointer. This is the iOS half of the shared rule in the Android design spec (
2026-09-23-chat-unread-divider-design.md).Boundary.
ConversationLoadCoordinator.initreads the boundary fromConversationStore.selfReadPointer. The screen builds the coordinator inonAppear, before the opening.taskreachesmarkRead, so the boundary reflects the pointer at open. It is never read again on that visit. With no stored self member row there is no divider. A group roster is paged, so the row can be missing, andhasUnread's treatment of a nil pointer as unread would head the whole transcript. The count comes fromDatabase.inboundMessageCount: stored rows past the pointer that are not the viewer's and not tombstones.Placement. Resolution sets the read-through to one below the first stored message after the pointer that someone else sent, deleted ones included. The mapper then draws the divider between the adjacent pair where
older.id <= readThrough < newer.idand newer isn't from the viewer. Comparing ranges means a deleted read-through message doesn't matter. This is Android's resolution (code-payments/code-android-app#1546). It is what makes the table's "Own message first after pointer" row work:1:other 2:self 3:otherwith pointer 1 resolves toreadThrough 2, count 1, and the pairwise rule alone finds no gap for it. When a date separator falls at the same gap, the date is drawn above the divider. The divider is a plain caption between two hairlines: "1 Unread Message" or "N Unread Messages", and "99+" past 99.Scroll. When the divider starts above the visible area after the bottom scroll,
ChatViewControllerrestores a.topChatLayout snapshot on it, or on the date separator above it when there is one, so it lands just under the nav bar. When it is already visible, the chat stays at the bottom. The loader grows its window back to the read-through message when there are at most 200 unread. Past that, the chat opens at the bottom and the divider shows once the reader pages back to it.Lifetime.
UnreadDividerLifetime.currentis.untilClose. Changing it to.untilSendclears the divider on the viewer's first send of the visit.UnreadDividerTestsencodes the seven shared cases, run through the database, the stored member row, the controller's resolver and the mapper.Read tracking is unchanged: the chat is still marked read in full on open.