Skip to content

feat(messenger): redesign the group invite flow - #1575

Merged
bmc08gt merged 15 commits into
code/cashfrom
feat/group-invite-sheet-redesign
Sep 28, 2026
Merged

bmc08gt merged 15 commits into
code/cashfrom
feat/group-invite-sheet-redesign

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Implements the group invite flow redesign (Figma section node 10417:20555) in three commits.

Link cards follow the bubble corner rules

Link cards drew with a fixed CodeTheme.shapes.medium, and groupsWith broke the run on any card row, so an invite (a card, then the note sent with it) read as two unrelated messages.

  • LinkCardView takes a shape. BareLinkCard passes bubbleShape(position, isFromSelf), so a card squares its corners against its neighbours like a text bubble does.
  • Each card kind cuts its pieces from that outline: the cash voucher's top half and stub, the ghost stub's path, TokenCard's bill (new shape parameter), and the invite card's frame.
  • groupsWith now breaks a run only on a bare emoji.
  • LinkCardShapeTest renders each card kind at Solo, First, Middle and Last, from both senders, and checks the outline against bubbleShape. BubblePositionTest covers a card joining a run on either side.

Empty group card (node 10330:19164)

An empty group showed ChatInfoCard with an "Invite People To Join" button under it. It now shows GroupInviteLinkCard in the header slot, at 72% of the row width, with an "Invite People" CTA that opens the invite sheet. GroupInviteLinkCard takes its CTA label as a parameter; in a DM transcript it says "View" (was "Start Chatting") and opens the group. The empty-state card is built from the chat's own record, so it needs no link lookup.

Invite sheet (nodes 10329:12104, 10330:19549)

  • Titled "Invite People", with Share and Copy Invite Link tiles that reuse the profile screen's ProfileShortcut (now internal).
  • "Recent Chats" lists ChatCoordinator.feed(CONTACT_DM, TIP_DM) as a multi-select.
  • The "Add a message" field and white Invite button appear once a chat is picked.

GroupInviteViewModel keeps the selection in tap order. Invite sends the link to each chat, then the trimmed message as a separate message when it isn't blank. Chats send concurrently, and one failing doesn't stop the others. A failed send leaves the usual retryable row in that chat's transcript. A chat whose link failed doesn't get the message, so a retry can't put the card after its note. Once every send has answered, the sheet dismisses and opens the chat picked first.

GroupInviteViewModelTest covers the bar's visibility, link-then-message order, a blank message, landing on the first-picked chat, and failure isolation.

@bmc08gt bmc08gt self-assigned this Sep 25, 2026
@github-actions github-actions Bot added type: feature New functionality area: ui Compose UI, theme, components, resources labels Sep 25, 2026
@bmc08gt
bmc08gt force-pushed the feat/group-invite-sheet-redesign branch 2 times, most recently from a2d84f0 to b9edbeb Compare September 27, 2026 03:07
A text bubble takes its corners from bubbleShape(position, isFromSelf),
with the small corner on the sender's side where it meets a neighbour.
Link cards ignored that: LinkCardView used CodeTheme.shapes.medium,
BareLinkCard passed no position, and groupsWith broke the run on any
card row. An invite, a card and then the note sent with it, read as two
unrelated messages.

LinkCardView now takes a shape, and BareLinkCard hands it
bubbleShape(position, isFromSelf). Each kind cuts its pieces from that
outline: the cash voucher's top half and stub, the ghost stub's drawn
path, TokenCard's bill, and the invite card's frame. groupsWith breaks a
run only on a bare emoji now, since a card has an edge to square
against.

GroupInviteLinkCard becomes public and takes its button label, so the
group's empty state can draw it too. The transcript card says "View"
instead of "Start Chatting".

LinkCardShapeTest renders each card kind at every position and checks
its outline against bubbleShape; BubblePositionTest covers a card
joining the run on either side.
An empty group showed ChatInfoCard with an "Invite People To Join"
button under it. Node 10330:19164 replaces both with the group-invite
card a DM transcript renders for the link, labelled "Invite People".

MessageList draws GroupInviteLinkCard in the header slot when the group
has no messages and the viewer has an invite URL. The CTA sends
ChatAction.InviteToGroup, as the old button did. ChatSubject.Group
builds the card from the chat's own record (title, picture, member
count, rules), so it needs no link lookup. The card sits at 72% of the
row, the frame's 290 of 402.

Every other chat still gets ChatInfoCard.
The invite sheet only offered Share and Copy. Nodes 10329:12104 and
10330:19549 add a multi-select list of the viewer's 1:1 chats and a
message bar, so an invite can go straight into existing conversations.

GroupInviteSheet is titled "Invite People" and draws Share and Copy
Invite Link with the profile screen's ProfileShortcut, now internal.
"Recent Chats" lists ChatCoordinator.feed(CONTACT_DM, TIP_DM). The
"Add a message" field and white Invite button appear once a chat is
picked.

GroupInviteViewModel keeps the selection in tap order. Invite sends the
link to each picked chat, then the trimmed message as a second message
if it isn't blank. Chats are sent to concurrently and one failing does
not stop the others; a failed send leaves its usual retryable row in
that transcript. A chat whose link failed does not get the message, so
a retry cannot put the card after its note. Once every send answers,
the sheet dismisses and pushes the chat picked first.

GroupInviteViewModelTest covers the bar's visibility, the link-then-
message order, a blank message, landing on the first-picked chat, and
failure isolation.
On a Seeker, picking a chat in the invite sheet showed the message bar
mostly below the screen edge, and the Copy tile read "Copy Invite L…".

InviteToGroup was a WrapContentSheet. A wrap-content body is measured at
its full height and then clipped, so a long chat list pushed the bar
under the gesture area. As a full sheet the body is bounded: the list
scrolls and the bar stays above it.

ProfileShortcut fixed its width at staticGrid.x20, too narrow for "Copy
Invite Link" on one line. It now takes that as a minimum, so the
profile's one-word labels sit where they did and the longer label
widens its tile.
The invite sheet's message bar drew TextInput's own rounded, bordered
field inside the pill, with the hint flush against that field's edge.
Node 10329:11963 has one 14dp pill: a bare "Add a message" hint inset
16dp at 40% white, and a 34dp white Invite button inset 8dp.

The field now takes transparent background and border colours and the
button's height, so the hint and the button label share a baseline, and
the pill carries the node's padding and corner radius.
…poser

The Recent Chats list followed the live DM feed, so a chat receiving a
message while the sheet was open jumped to the top and moved rows under
the viewer's finger. The view model now reads the feed once when the
sheet opens.

Rows get the dividers from node 10330:19585, inset to start at the name,
and the header is ruled off below its label.

The message bar is now the chat screen's ChatInput. A new
ChatInputSubmit.Action shows a labelled button ("Invite") whether or not
anything is typed, since the link goes out with or without a message.
The list scrolls under the composer and blurs progressively behind it,
as in node 10330:19709.
The sheet only listed 1:1 chats, so a group link could not be passed to
another group without leaving the sheet to share it. The feed now
includes GROUP chats, mixed with DMs in the feed's own order.

The group being invited to is left out of its own list: the sheet names
it through GroupInviteViewModel.inviteTo, which also stops it from being
picked. Group rows already take their title and picture from
toConversationReference.
The card's padding and gaps came from the theme's inset and grid, which
grow on larger phones, so the card drew taller than iOS's GroupCardView.
It now uses iOS's fixed values: 12dp side and bottom padding, 13dp above
the title and 11dp above each requirement line.
… list

This ports the iOS sheet (code-payments/code-ios-app#873, node
10330:19387). The sheet no longer pins a header: the title bar, with a
glass close button, and the message composer float over one list that
scrolls under both.

- "Recent Chats" is an ordinary list row with a divider under it.
- Share and Copy are their own tiles per node 10330:19400: two equal
  95dp tiles at 5% white with wrapping labels. Copy turns into "Copied"
  with a check. ProfileShortcut goes back to private and its fixed width.
- The composer appears once a chat is picked and slides up on a spring
  rather than fading, since a fading glass field shows the list through
  it. It has no outline, and ChatInputSubmit.Action gains a busy flag
  that swaps the Invite label for a spinner while sending.
- Both edges fade to the sheet's surface colour, the same scrim the
  Chats list draws under its title bar, instead of blurring. The bottom
  fade is drawn whether or not the composer is up, so it holds still
  while the composer slides through it.
ChatInput clips to its rounded shape and then applies the caller's
modifier, so the padding the sheet passed in put the rounded clip on the
padded box and left the blur square inside it. The square corners showed
past the pill. The inset now sits on a wrapper.

The pill also drew ChatInput's 68% fill over the haze tint, which made it
opaque. It now takes one fill, node 10329:11963's 46% #1E1E1E, over the
blur. ChatInput gains a containerColor parameter for callers that draw
their own glass; its default is the old fill, so the chat screen is
unchanged.
…'s top

A fling that reached the top of Recent Chats carried on into the bottom
sheet and dismissed it. The list now uses sheetResignmentBehavior, as the
other scrolling sheets do, so leftover momentum stops at the top. A fresh
drag down from the top still closes the sheet.
The sheet read CONTACT_DM alongside TIP_DM and GROUP. Contact DMs have
their own inbox, so a person with both kinds of DM appeared twice, and
contact rows without a name showed blank. The sheet now reads
chatListFeed(), the same TIP_DM and GROUP set the Chats screen shows.
The invite sheet closed after sending but never opened the chat picked
first. It closed itself and then dispatched the navigation through the
conversation's event collector, which is only composed while the
transcript is on screen. Opened from the group's profile, nothing
collected the event, so it was dropped.

InviteToGroup now returns a GroupInviteResult with that chat's id, and
openGroupInvite() navigates from the result callback of whichever screen
opened the sheet: the transcript or the group's profile. The keyboard is
hidden before the sheet returns so the screen behind doesn't shift.
The invite card only replaced the group's info card while the transcript
was empty, so a group whose only member had posted anything fell back to
the old header. iOS decides on who is in the chat instead: the card heads
the transcript while the viewer is a member and nobody else has joined
(ConversationScreen.showsGroupInvite).

State.showsGroupInviteCard now carries that rule and MessageList reads
it. Once someone joins, the header goes back to the info card and the
invite stays on the group's profile.
Link cards and the solo group's head card came out about 30dp narrower
than iOS's on the same screen. iOS takes 78% of the transcript's full
width for both (ChatViewController.maxBubbleWidthFraction). Android took
78% of the row after its side insets for link cards, and 72% for the
head card.

transcriptCardWidth() adds the insets back before applying the fraction,
and both the card rows in ContentBubble and the head card in MessageList
use it. Text and cash bubbles keep their existing widths.
@bmc08gt
bmc08gt force-pushed the feat/group-invite-sheet-redesign branch from 12c0308 to afaa5fa Compare September 28, 2026 02:38
@bmc08gt
bmc08gt merged commit 5977aa0 into code/cash Sep 28, 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