Skip to content

feat(chat): show a person card for Flipcash profile links - #1578

Merged
bmc08gt merged 3 commits into
code/cashfrom
feat/user-link-card
Sep 27, 2026
Merged

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

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Links to a Flipcash profile — flipcash.com/<handle>, flipcash.com/<uuid>, and legacy /tip/<uuid> on the Flipcash hosts — now render in chat as a person card instead of underlined text. This ports code-payments/code-ios-app#872 and #879, plus the stray-punctuation split fix from #880.

Design decisions

Classification

  • Person links classify through the existing AppRouter (DeeplinkType.Tipcard / TipcardByUsername), so the reserved profile paths (download, privacy, terms, support, help, about, blog, legal, currencycreator, app, api, assets, fonts, icons, js, v1, pool) stay website links. iOS copied that list from Android, and AppRouterTest already covers it. The messenger classifier test uses a fake router, which mirrors the list and points at AppRouterTest, because AppRouter is internal to its module.
  • Non-Flipcash hosts (e.g. discord.gg/x) don't produce a card.
  • The shared link_detection.json fixture still expects no card for its tip-card-by-id vector. As on iOS, that vector is marked "ahead of fixture" rather than the fixture being edited here.

Lookup (UserLinkLookup)

  • It fetches the profile with ProfileController.getProfileForUser / getProfileForUsername only. Nothing is reused from the tip flow: no profile caching, no bill overlay, no navigation.
  • A profile with no userId (an unclaimed handle) is not found. A thrown error is also not found, and isn't memoized, so the card retries the next time it appears. A found profile is memoized per identity, the same way the group card is.
  • Own-link detection compares against userManager.accountId.
  • The card reads only Profile: picture blurhash, picture, display name, @username, and joinedAt. It doesn't show minDmChatInitFee (the DM's fee sheet still handles payment) and doesn't read tip card colours.

Tap (ChatAction.OpenUser)

  • The whole card is the tap target. For someone else, it pushes AppRoute.Messaging.Chat(ChatIdentifier.ByUser(...)) on the current stack, the same way the group card pushes its chat.
  • Your own link calls rootNavigator.navigateAll(listOf(AppRoute.Tabs.Menu)). That's where your tip card lives on Android, and where the own-card deep link and scanning your own card already go.
  • A link to the person you're already DMing does nothing. A not-found card has no tap action, but long-press still reaches the transcript.

Look (UserLinkCard.kt, constants in UserLinkCardDefaults mirroring iOS LinkUserCardContent.Layout)

  • The row hugs its text, capped at the bubble width: avatar 44, gap 12, padding 10 top/bottom, 10 leading, 16 trailing, line gap 2.
  • At font scale ≥ the accessibility threshold, the avatar sits above the text and every line wraps with no ellipsis.
  • The backdrop is the blurhash, cropped to fill, under black at 0.6 (TINT_OPACITY, provisional pending design sign-off). With no blurhash, the tint sits over CodeTheme.colors.background. The photo itself is never the background.
  • The backdrop is drawn in drawBehind rather than Modifier.paint: with bounded constraints, paint(sizeToIntrinsics = false) raises the minimum size to the maximum, and the card filled the whole bubble.
  • The shape and border reuse GroupInviteCardDefaults.SHAPE / STROKE, the group card's radius 12 and white 0.1 stroke.
  • The avatar is the existing ContactAvatar with the same ring. Not found uses the generic placeholder avatar.
  • Type: name is textMedium, 2 lines then ellipsis. The handle is caption at 0.5, and the detail line is caption at 0.45. No new type tokens.
  • Detail line: "Joined " comes from a new shared joinedLine(), which the profile screen now uses too, so the format lives in one place. It's omitted when joinedAt is null. Not found shows the existing "No Such Account" string in that spot, with the URL's @handle as the name.
  • Loading is a 200×64 shimmer at the card radius.
  • The card shares no code with the tip card. Its previews reuse the group card's previewPicture helper, which went from private to internal.

Stray punctuation (ChatListItem.splitAroundLinkCard)

  • Punctuation touching the link goes with the link: link. and (link) give the card only, and link, then lunch gives the card then "then lunch". Punctuation after a space stays with its words. A leftover segment of only whitespace and punctuation is dropped, while symbols and emoji ($, 🎉) are kept. Copy still copies the original message.

Differences from iOS

  • When a resolved profile has no display name and no username, iOS falls back to a generic counterpart name. Android has no equivalent string, so the name line is omitted and the handle/joined lines carry the card.

Also in this PR

  • test(chat): ChatGroupCashLinkTest didn't compile on code/cash because ChatViewModel gained constructor dependencies. It now passes them, so the messenger tests run.

A message ending https://flipcash.com/x. drew the card plus a bubble holding
only ".", because the split around the card trimmed whitespace alone.
Punctuation touching the link now goes with it, and a segment of nothing but
whitespace and punctuation is dropped, matching iOS (code-ios-app#880).
A `flipcash.com/<handle>`, `flipcash.com/<uuid>` or legacy `/tip/<uuid>`
link in chat now renders as a person card instead of underlined text,
matching iOS (code-ios-app#872, #879). The router's reserved profile
paths (download, privacy, terms, ...) still classify as website links.

The card looks the profile up by user id or username through
ProfileController, with no tip-flow side effects. An empty profile or a
thrown error shows "No Such Account"; errors are not memoized, so the
card retries the next time it appears. Tapping opens the person's DM,
your own link opens your tip card on the You tab, and a link to the
person you're already messaging does nothing.

The profile screen's "Joined <Month Year>" line moves to a shared
joinedLine() so the card and the profile format it the same way.
@bmc08gt bmc08gt self-assigned this Sep 27, 2026
@github-actions github-actions Bot added the type: feature New functionality label Sep 27, 2026
@bmc08gt
bmc08gt merged commit 7232921 into code/cash Sep 27, 2026
3 checks passed
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