Skip to content

feat(chat): group invite link card, and a row per link card - #1530

Merged
bmc08gt merged 3 commits into
code/cashfrom
feat/group-invite-link-card
Sep 23, 2026
Merged

bmc08gt merged 3 commits into
code/cashfrom
feat/group-invite-link-card

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Ports code-payments/code-ios-app#830 to Android. It's two commits, meant to be reviewed in order.

1. A link card gets its own row

A message with a carded link (cash link, token info, group invite) now becomes up to three transcript items: the text before the URL as a bubble, the card with no bubble behind it, and the text after as a bubble. Rows keep source order, blank segments are dropped, and each row's id is the message id plus #leading, #card or #trailing.

Every row carries the whole message. Selection, reactions, reply, edit, delete, copy (whole text), retry, quote jumps and the jump highlight all act on the message from any row. The reply quote goes on the first row, and "Edited" plus the receipt go on the last. On a bare card row, "Edited" moves to the metadata line. A card row breaks the bubble-grouping run the way a bare emoji does. Other links in the text stay underlined inline, and a message has at most one card.

2. Group invite card

app.flipcash.com/chat/{uuid} now classifies to a card. /chat/{uuid}/send and the tip chat route are still refused. The card is fetched with GetChat in ViewMode.REDACTED and built from title, picture, rosterSummary.memberCount and rules only. members is never read. An untitled group shows "Group Chat" (provisional copy).

Only the button takes a tap. "Start Chatting" dispatches ChatAction.OpenGroup, and the messenger pushes AppRoute.Messaging.Chat(ByChatId) onto the current stack, so Back returns to the chat that held the link. The pushed screen does the join or buy gating. A card linking to the chat on screen does nothing. Cash and token cards are still tappable anywhere.

Design: nodes 10125:19157, 10127:118280, 10127:116723.

Constants (GroupInviteCardDefaults)

Name Value Note
SHAPE 12dp radius group header card
STROKE white 10%, dimens.border (1px) card and avatar ring
BAND_HEIGHT 46dp
BAND_OPACITY 0.28 pending design sign-off
AVATAR 64dp centred on the band's bottom edge
MEMBER_COUNT_GAP 2dp title to count
BUTTON_GAP 16dp least gap before the button
BUTTON_HEIGHT 44dp
min height width × 224/328 (LinkCardDefaults.CARD_ASPECT) a floor, not a fixed size

Band tint comes from BlurHash.averageColor: chars 2–5 decoded as base-83 to 0xRRGGBB sRGB with no linearisation, and null if malformed. The fallback is contactAvatar.colors.first(). The helper is tested against test-vectors/blurhash_average.json, copied into common-ui/src/test/resources.

Where Android differs from iOS

  • Requirement line and member count styling. iOS uses 15 medium in textMain at 50% for the requirement, and 13 medium for the count. Android's ChatInfoCard renders both as textSmall in textSecondary, and the card matches Android's header, since matching the header is the rule.
  • Buttons. Android has no compact button style, and this PR doesn't add one. Both buttons are CodeButton sized to BUTTON_HEIGHT with zero content padding. "Start Chatting" is ButtonState.Filled. "Group Unavailable" is ButtonState.Filled20 and disabled, mirroring iOS's filled20Compact.
  • Height. The card takes its intrinsic height at its own width, coerced to the floor. That avoids the iOS bug where measuring at ideal width overlapped neighbouring rows. The avatar's fixed size is its outermost modifier, which keeps the intrinsic query away from its BoxWithConstraints.
  • Long press. The card has no tap of its own, so a detectTapGestures long press passes selection back to the transcript.
  • Row order. The transcript is reverseLayout, so the view model emits a split message's rows bottom-first.
  • Removed. withoutLinkSpan and its test are gone, as is iOS's cutRange.

Tests and previews

  • SplitAroundLinkCardTest: order, blank segments dropped, unique ids.
  • BubblePositionTest: the grouping run breaks around a card row.
  • LinkCardClassifierTest: chat yields a card, and /chat/{uuid}/send doesn't.
  • LinkCardResolverTest: group cases.
  • BlurHashAverageColorVectorTest: the shared vectors.
  • Previews in GroupInviteLinkCard.kt cover resolved, no picture, unavailable, loading and 2× font scale. MessageBubble.kt has a split group-invite message next to a cash card.

A carded link drew inside the message's bubble, with the URL cut out of the
text around it. The message now splits into up to three transcript rows, as
on iOS (code-ios-app#830): the text before the link, the card with no bubble
behind it, and the text after. Blank segments are dropped, and the rows key
as the message id plus `#leading`, `#card` or `#trailing`.

Every row keeps the whole message, so selection, copy, edit, reply, retry,
jumps and the jump highlight act on the message from whichever row they
start on; selection now compares `messageKey` instead of the row key. The
reply citation goes on the first row and the "Edited" marker and receipt on
the last. A card row breaks the bubble run the way a bare emoji does, and
its "Edited" marker moves to the metadata line under it.

The view model emits the rows bottom-first because the list is
reverseLayout. LinkCardView now owns the card's tap and hands a long press
back to the transcript, which the bare row needs to stay selectable.
`withoutLinkSpan` and its test go, since no row cuts the link out of its
text any more.
A link to `app.flipcash.com/chat/{uuid}` stayed plain text. It now gets
a card, as on iOS (code-ios-app#830). `/chat/{uuid}/send` and the tip chat
route are still refused.

The card reads the group with GetChat in the redacted view, which answers
for non-members, and uses only its title, picture, roster summary and
rules. `members` is never read, so no member name or avatar can reach the
card. An untitled group is named "Group Chat" for now, pending copy.

Layout follows the group header card (nodes 10125:19157, 10127:118280,
10127:116723): radius 12, a white 10% stroke, a 46dp band across the top
tinted at 28% opacity, and a 64dp ringed avatar centred on the band's
bottom edge. Under it are the title, the member count and the rule, as
`ChatInfoCard` renders them, then a 44dp button at least 16dp below.
The band's colour is the picture's blurhash DC component, falling back to
the avatar placeholder gradient's first stop.
`BlurHash.averageColor` is checked against the shared
`blurhash_average.json` vectors.

The card is as wide as the cash card, and its height is the larger of its
content and width × 224/328, measured intrinsically at the card's own
width. So a wrapped requirement line or a large font scale grows the row
instead of overlapping its neighbours.

Only the button takes a tap. "Start Chatting" pushes the group's
conversation onto the root stack, so Back returns to the chat holding the
link, and the pushed screen handles joining or buying. A card linking to
the chat on screen does nothing. An unavailable group shows only the band
and a disabled "Group Unavailable" button.
@bmc08gt bmc08gt self-assigned this Sep 23, 2026
@github-actions github-actions Bot added the type: feature New functionality label Sep 23, 2026
The label and the amount now break onto two lines, in the chat header and
the group invite card alike, since both read the same strings.
@bmc08gt
bmc08gt merged commit 9be33c3 into code/cash Sep 23, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/group-invite-link-card branch September 23, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant