Repository navigation
feat(chat): render a group conversation - #1470
Merged
Merged
Conversation
This was referenced Sep 16, 2026
bmc08gt
force-pushed
the
feat/group-chat-ui
branch
2 times, most recently
from
September 16, 2026 15:34
5dfed6d to
a14d31f
Compare
bmc08gt
force-pushed
the
feat/group-chat-feed
branch
2 times, most recently
from
September 16, 2026 16:03
598da87 to
f8f2ec5
Compare
The flag was read once inside onUserLoggedIn, so flipping it on only took effect after a process restart replayed login hydration — the toggle looked dead until you killed the app. Make it what its description already claims: a filter on the chat list. Every read of the chat cache is scoped to the chat types its caller asks for — feed(), feedPaged() and observeUnreadConversations() all take the types, and observeFeedPaged filters in SQL — so a group stored with the flag off reaches no list and no badge. Gating the fetch also contradicted the roster-change routing beside it, which was never gated. Drops the chat module's dependency on featureflags; the flag now lives only in the chat list that filters on it.
A group feed sync persisted each group's last-message preview and stopped there, so opening a group the device had only ever seen in the feed showed exactly one bubble. The DM feed has handled this since it was written: after persisting, it compares each chat's applied cursor against the feed head and emits LoadMessages or DeltaSyncNeeded. The group feed now does the same, and the coordinator routes the new DeltaSyncNeeded to the event stream. The applied cursor is the right test rather than "does this chat have messages" — persisting the preview makes the latter true for a chat whose transcript was never pulled.
MessageRow asked LazyPagingItems for the item above the one it was drawing — peek(index + 1) under reverseLayout — without checking the snapshot had one. The oldest loaded message never does, so a transcript holding a single message crashed on compose: IndexOutOfBoundsException: Illegal attempt to access index 1 in ItemSnapshotList of size 1 That was reachable in group chats, which until the previous commit cached one message and no more, but the bound was missing for any list. bottomSpacingFor already guards its peek(index - 1) the same way; startsSenderRun treats a null neighbour as the start of a run, which is what the top of a loaded page is.
Typing unlocked once the chat held a cash message. That is the tip DM's rule — the bottom bar is a "Send Tip" call to action until the first payment — and a group inherited it by falling through, so a group nobody had tipped into had no composer at all. A group already has an answer to who may post: its chat rules, evaluated into GroupAccess against the live balance. A group that reaches the composer is Membered, because every other state renders the gate bar in its place, so typing follows membership rather than tip history. The DM path is unchanged, and so is the TIP_DM-scoped check that decides whether a send goes out as a tip.
Sending cash into a group is not wired up, so the composer row drops the button rather than showing one that does nothing. Where it is shown it now condenses to the icon for a group the same way it does for a tip chat: a group has no single counterparty to "Send $" to, and the expanded pill would take the row from a composer that is always open there.
A group's transcript had two things that look tappable and were not: the picture beside each incoming bubble, and the info card at the head of the thread that a DM uses to reach its counterparty's profile. The gutter picture opens the sender's profile. The transcript has already resolved that person to draw the picture, so the tap answers from the same map rather than fetching them again — `senderProfiles` becomes a held StateFlow for it. The card opens the group's own profile, a new step in the chat flow. It carries no payload: a group is identified by the conversation the flow is already on, and parcelling one would mean parcelling its rules. The screen reads it off the shared view model, as the transcript does. That profile shows the group's picture, title and size, and offers Leave Chat to a member. `leave` already clears the membership before the call returns, so the screen closes onto a transcript that is showing the join gate again.
The row is the only thing on a group's profile that acts, and a way out of a group is not what to hand someone while joining one is still settling. The screen is its identity header until then. Everything behind the row stays wired — the confirmation, the call, the close on success — so bringing it back is the one constant.
A citation asked `State.participant` who wrote the message it cites. In a DM that is right — the counterparty sent everything the viewer did not. A group has no participant, so every quote of someone else came out with a blank name, in the composer strip and in the sent bubble alike. The cited message carries its own sender, so the name now comes from that sender's profile, out of the map the transcript already uses to draw their bubble. A sender the device has not seen is requested rather than left unresolved for the session.
bmc08gt
force-pushed
the
feat/group-chat-ui
branch
from
September 16, 2026 16:07
a14d31f to
73fc676
Compare
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.
Stack:
Third of three, on top of
feat/group-chat-feed. A group chat renders, and the Chats tab carries groups.The conversation screen used to read a contact. It now reads a
ChatSubject— a contact DM, a tip DM, or a group — and the title bar, info card and messenger each render from that one thing rather than each deciding what a null contact means.The last stretch is what device testing turned up: the group feed syncs regardless of the feature flag (the flag is presentation filtering, so a chat that arrives while it is off is still there when it goes on), a synced group fetches its transcript instead of opening on its one cached preview, the sender-run peek stays inside the loaded page, and the cash button is left out of a group, which has no single counterparty to send to.
Tapping a sender's picture opens their profile; tapping the info card opens the group's own. Leaving a group is wired but held back behind a constant — a way out is not what to hand someone while joining is still settling.