Skip to content

Add group chats (behind a build switch) - #166

Merged
xss3cut10n3r merged 18 commits into
mainfrom
feat/groups
Nov 14, 2026
Merged

xss3cut10n3r merged 18 commits into
mainfrom
feat/groups

Conversation

@xss3cut10n3r

@xss3cut10n3r xss3cut10n3r commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Why

Fireplace has only 1:1 chats. This adds group chats: end-to-end encrypted with MLS, up to 16 people, with the same look and behaviour as a 1:1 chat. Design and owner decisions: decision 0050.

Groups are off unless the app is built with --dart-define=FIREPLACE_GROUPS=true, so an ordinary build is unchanged. They also need the matching server rules and functions, which the owner deploys.

What you get

Light Dark
Chat list. Groups sit beside 1:1 chats with the same row, a preview that names the sender, and an unread count.
Conversation. Your messages in your colour; everyone else in the soft tint of their colour, with their name above a run and their avatar beside its last message. Small lines say who was added, left, was removed, renamed the group or became an administrator.
Holding a message opens the same menu as a 1:1 chat. Anyone can react; only the author can edit or delete for everyone.
Editing happens in the composer, as in a 1:1 chat.
Joining an existing group. Earlier messages are not shown, and the conversation says so.
Group info. Mute, add people, rename, members and administrators, and people who are invited but have not joined.

These are widget renders with fictional names (how they are made). The reaction bar shows empty boxes because the preview font has no emoji.

How it works, for a reviewer

  • Who may do what. Anyone can create a group. Any member can invite someone they already have an accepted 1:1 chat with. Administrators remove people, rename the group and choose other administrators. Anyone can leave; a group ends when its last member does.
  • Inviting and joining. The server has no group keys, so only a member's phone can add someone. The inviter's phone does that work when it sends the invitation. The invited person then taps Join and is in the conversation at once, even if nobody else is online. Declining, or seven days without an answer, removes them again.
    • Consequence, accepted by the owner: from the invitation on, the invited phone holds the keys for messages sent afterwards. The server still refuses it everything until the person joins, the app shows nothing from before they joined, and Group info lists them as Invited.
  • Colours travel only inside the group, encrypted. A newcomer announces theirs and each member answers with their own.
  • Unread counts, mutes and the lines about changes are worked out on the phone from the encrypted local history. Nothing about what was read or muted leaves the phone. The lines come from the group's authenticated membership changes, not from the server's list.
  • Notifications stay generic. The invited person gets one notification for an invitation.

This changes what is sent and stored (group requests and membership records), so the design note and the protocol note are updated in this pull request with the reasons and the Firestore cost of each operation: 0050, group-chats-v1.

Code: lib/src/model/groups/ (the service and its models), lib/src/view/groups/ (screens), lib/src/app/group_providers.dart. Two shared 1:1 widgets gained an optional parameter each so the group conversation can reuse them (MessageBubble: the colour of someone else's message; ChatComposer: no 1:1 send controller needed). The local history has a new kind of entry for the lines about changes; it is never counted as unread or matched by search. No new package.

How it was checked

  • scripts/check.sh passes: 1,485 tests and 19 timing tests. CI is green.
  • CLA check passes.
  • New behaviour has tests that fail without it: 24 tests with two or three phones on the real MLS core (test/model/groups/group_service_test.dart), including joining while the inviter's phone is closed, and 13 screen tests (test/view/groups/group_screens_test.dart).
  • UI change: previews above, committed under docs/reviews/group-chats/.
  • Tested on an Android emulator against local Firebase emulators: inviting; a plain refusal when the person has never opened the app; joining with no other member signed in; the note about earlier messages; sending, the held-message menu and editing in the composer; unread counts; the lines; colours; making someone an administrator. Declining, muting, and Join opening the conversation were not driven by hand there; tests cover them. Not tested on a real phone or on iOS. Notifications cannot be delivered on the emulator.

AI assistance

Claude Code wrote this change, directed by the owner, who tried it on the emulator and chose the behaviour. It builds on a design proposal written with GPT (OpenAI Codex). A human must review the rules-facing and cryptographic parts before release.

Privacy and safety

  • No new data leaves the device or is stored on the server — left unticked on purpose. For each group the server holds who is a member and who is invited (account and device ids, who invited them, when the invitation expires, when someone joined). Group names, colours, the lines about changes and message text are encrypted or stay on the phone.
  • No secrets, keys or real user data in the diff. Test accounts are fictional.

Not included

  • The invitation card inside the 1:1 chat with the inviter: Groups: show the invitation card in the 1:1 chat with the inviter #164.
  • Adding a member's newly linked phone to their groups.
  • The group's name in the invitation notification (the server never has the name).
  • The conditions for turning groups on in a release, listed at the end of decision 0050, are still open.

🤖 Generated with Claude Code

GroupService keeps one device's groups: key packages, membership requests,
Commits applied strictly in epoch order and checked against the group's
rules (renames and roles only from administrators, no membership change in a
self-update), messages bound to their authenticated sender and id, member
colours shared inside the group, and sends that are never resent
automatically. Its MLS records and cursors are saved atomically in an
encrypted file. Nine tests run two devices through the real Rust core.

AI assistance: Claude Code.
…groups.

Wires GroupService into the session (only in builds with
--dart-define=FIREPLACE_GROUPS=true) and adds the screens: New chat offers
Message someone or New group; groups appear in the chat list and search;
invitations wait in Message requests with Join and Decline; the group chat
shows names, avatars and each member's colour; Group info lists members and
administrators with Add people, Rename, Remove and Leave.

Owner's choices (2026-11-14), recorded in 0050: any member invites people they
already chat with; only administrators remove and rename; a group ends when its
last member leaves; only a message's author edits or deletes it for everyone.

AI assistance: Claude Code.
The New chat button now opens a full-height panel: Cancel and the title at the
top, a To: field, Create group (when groups are on), and the people you
already chat with. Typing filters the list; a username that is not in it can
be messaged directly. Group start failures are now printed in debug builds.

AI assistance: Claude Code.
The caller disposed the dialog's text controller while the dialog was still
animating closed, which tripped a framework assertion on the device. The
dialog now owns its controller.

AI assistance: Claude Code.
The inviter's device sends the Add Commit and the Welcome with the invitation.
The invited phone joins from the Welcome it already has; no other phone is
needed. A declined or expired invitation is fenced and removed like a leave.
Members answer a newcomer's profile with their own, so someone who joins later
has everyone's colour. Changes to the group are written into the history as
lines, built from the MLS state before and after each Commit. An administrator
can make another.
…ead counts.

The conversation reuses the 1:1 bubble, held-message menu, reactions, day
separators and composer, and edits in the composer instead of a dialog. Other
people's bubbles use the soft tint of their own colour, with their name and
avatar in its text shade. The list row has the 1:1 layout and an unread count
taken from the local history. Lines say who was added, left, was removed,
renamed the group or became an administrator, and a newcomer sees where their
history starts. Group info lists people who are invited and lets an
administrator make another.
…ne lacks.

Found on the emulator: Join left the person on Message requests, and a group
from before members answered a newcomer kept showing them in Stone.
Hold the row or use Group info. The mute is kept with the 1:1 mutes and never
leaves the phone. A muted group keeps its count on the row, in grey, and is
left out of the app icon's badge, which now counts group messages too.
The notes and comments named the second set of changes by a working label that
means nothing to a reader.
@xss3cut10n3r xss3cut10n3r changed the title Group chats: screens, permissions, and round 2 (instant join, colours, lines, roles) Add group chats (behind a build switch) Nov 14, 2026
@xss3cut10n3r
xss3cut10n3r marked this pull request as ready for review November 14, 2026 10:25
@xss3cut10n3r
xss3cut10n3r requested review from a team as code owners November 14, 2026 10:25
@xss3cut10n3r
xss3cut10n3r merged commit 25812ab into main Nov 14, 2026
9 checks passed
@xss3cut10n3r
xss3cut10n3r deleted the feat/groups branch November 14, 2026 10:25
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.

2 participants