Skip to content

feat(chat): start a chat by typing someone's username - #1338

Merged
bmc08gt merged 6 commits into
code/cashfrom
feat/chat-by-username
Aug 26, 2026
Merged

feat(chat): start a chat by typing someone's username#1338
bmc08gt merged 6 commits into
code/cashfrom
feat/chat-by-username

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Covers the two Figma frames on Usernames [Aug 24] — node 9491:6298 (start a chat by username) and node 9491:6299 (receive one).

The gap

The Chats tab could only show conversations that already existed. A tip DM appeared once someone had tipped you, so there was no way to reach a person you had never exchanged money with — and the empty state told you to share your Tip Card and wait.

The "+" in the Chats title bar now opens a handle entry field (node 9442:5825). It resolves the username through ProfileController.getProfileForUsername and opens the conversation.

How the chat opens before it exists

ChatIdentifier.ByUser carries the resolved user id and profile. It is the only identifier that can address a chat the server does not have yet:

  • ChatCoordinator.generateChatId(userId) derives the canonical TIP_DM id offline and deterministically, so the chat opens on the id the first tip will land on. No round trip, and no empty-chat placeholder to reconcile later.
  • The profile rides along because the lookup returned it anyway. The header card renders on the first frame instead of waiting on a members fetch that, for a chat with no messages, has nothing to return.

An empty tip DM has no composer — typing is gated on a first payment — so SendCashButton takes over the whole bar as a "Send Tip" call to action rather than collapsing to a full-width transparent "$".

Node 9491:6299 needed no code: TipChatRow already renders name-or-handle, preview, last activity and unread count, and AppNavigationBar already badges unread tip DMs onto the Chat tab.

Navigation

The entry screen is a top-level AppRoute.Messaging.NewChat, not a step of the tipping flow. The Chats list is a tab home, so a step pushed inside it keeps the tab bar over the field. Opening the chat replaces the entry screen, so backing out of the chat lands on the list it belongs to rather than on a spent text field.

Input and errors

Handle input is clamped to the server's ^[a-z0-9_]{2,15}$ as it is typed, which is stricter than the claim screen — nobody is choosing a handle here, so an over-long value is a typo or an eager paste. Left unclamped it fails request validation and surfaces as "Something Went Wrong", which the user can do nothing with.

Two rejections get their own copy: an unknown handle, and your own.

Shared helper

The first commit adds BaseViewModel.dispatchSuccessThen. Four view models already spell out dispatch-success, sleep, navigate by hand at 400ms or 500ms; without the sleep the checkmark is swapped away on the frame it is drawn. The helper names the hold and runs the continuation on viewModelScope, so backing out mid-hold cancels the navigation. Only the new-chat lookup adopts it here.

Result.onSuccessWithDelay does not cover this case — it measures from the start of the operation, so the hold reaches zero exactly when the work was slow enough for the checkmark to need it. It has no callers in either of the two files that define it.

One thing to settle

The self-handle copy ("That's Your Username" / "You can't start a chat with yourself") is invented. The spec has no frame for typing your own handle, so this is a guess at what should happen.

Three changes on the chat screen

The "Send Tip" call to action now sends a tip. It went out as TipDmPayment.Location.CHAT, which the server reads as the verb "Sent" — so a button that said tip titled itself "Sent" in the recipient's activity feed and reported Sent Cash. It sends TIPCARD and reports Sent Tip instead. Location has two values and the server treats them as the verb rather than as a place, so that is the only way to ask for a tip. Every other send from this screen is untouched: the money button beside the composer only exists once typing is unlocked, which is exactly when the call to action is gone.

With that, the message bubble stops splitting the received side — "You received a tip" collapses into "You received" for both verbs. The sender still reads "You tipped" or "You sent".

Leaving a chat hides the keyboard first. It popped with the IME still up, so the keyboard collapsed over the pop instead of ahead of it. Every exit converges on the flow host's onExit — the top bar's up control pops the inner navigator, which at the flow root reaches onRootReached, and system back arrives there too — so one call covers all of them.

In-chat amount entry is a sheet, not a pushed screen. Sending cash from a conversation covered the thread you were sending into. ChatStep.AmountEntry is now a Sheet and the chat flow runs the modal sheet scene strategy, the same mechanism SwapFlowScreen uses for its currency pickers, so the conversation stays visible behind it. The app bar's leading up-arrow becomes a trailing close to match TipAmountEntryScreen — dismissing drops the amount rather than stepping back.

Screens that navigate off a confirm button all dispatch success, sleep, then go
— UsernameEntryViewModel, NameEntryViewModel, PhotoSelectionViewModel and
CurrencyCreatorViewModel each spell out the same four lines, at 400ms in two
places and 500ms in the rest. Without the sleep the checkmark is swapped away on
the frame it is drawn.

dispatchSuccessThen puts the timing on BaseViewModel with the hold as a named
default, and runs the continuation on viewModelScope, so backing out mid-hold
cancels the navigation instead of pushing into a screen the user has left.

Result.onSuccessWithDelay does not cover this: it measures from the start of the
operation, so the hold reaches zero exactly when the work was slow enough for the
checkmark to need it. It has no callers in either file that defines it. Only the
new-chat lookup adopts the new helper here; the sites above are unchanged.
The Chats tab could only show conversations that already existed: a tip DM
appeared once someone had tipped you, and there was no way to reach a person you
had never exchanged money with. The "+" in the title bar now opens a handle entry
field (node 9442:5825) that resolves the username and opens the conversation.

ChatIdentifier.ByUser carries the resolved user id and profile, and is the only
identifier that can open a chat which does not exist yet. The canonical TIP_DM id
is derived from the user id offline, so the chat opens on the id the first tip
will land on; the header renders from the profile the lookup already returned
rather than waiting on a members fetch that would come back empty.

An empty tip DM has no composer — typing is gated on a first payment — so
SendCashButton takes over the whole bar as a "Send Tip" call to action instead of
collapsing to a full-width transparent "$".

The entry screen is a top-level route rather than a step of the tipping flow: the
Chats list is a tab home, so a step pushed inside it would leave the tab bar
sitting over the field. Opening the chat replaces the entry screen, so backing out
of the chat lands on the list it belongs to.

Handle input is clamped to the server's ^[a-z0-9_]{2,15}$ as it is typed.
Unclamped, an over-long paste fails request validation and surfaces as
"Something Went Wrong", which tells the user nothing they can act on.
@bmc08gt bmc08gt self-assigned this Aug 26, 2026
@github-actions github-actions Bot added area: ui Compose UI, theme, components, resources type: feature New functionality labels Aug 26, 2026
Leaving a chat popped the screen without dismissing the IME first, so the
keyboard collapsed over the pop instead of ahead of it.

Every way out converges on the flow host's `onExit`: the top bar's up control
pops the inner navigator, which at the flow root reaches `onRootReached`, and
system back arrives there too. Hiding there covers all of them, and
`hideIfVisible` is a no-op when nothing had focus.
A tip DM shows one full-width "Send Tip" button until its first payment unlocks
typing. That payment went out as `TipDmPayment.Location.CHAT`, which the server
reads as the verb "Sent" — so a button that said tip titled itself "Sent" in the
recipient's activity feed, and reported `Sent Cash`.

Send `TIPCARD` from that call to action and report `Sent Tip` with it. `Location`
has two values and the server treats them as the verb rather than as a place, so
`TIPCARD` is the only way to ask for a tip. Every other send from this screen is
unaffected: the money button beside the composer only exists once the thread is
unlocked, which is exactly when the call to action is gone.

The message bubble stops splitting the received side — "You received a tip"
collapses into "You received" for both verbs. The sender still reads "You tipped"
or "You sent"; which button they pressed isn't the recipient's to be told.
Sending cash from a conversation pushed a full-screen amount entry over the
thread, so the conversation you were sending into disappeared while you typed
the amount. Marking `ChatStep.AmountEntry` as a `Sheet` and giving the chat
flow the modal sheet scene strategy keeps the thread visible behind it, the
same mechanism `SwapFlowScreen` already uses for its currency pickers.

The app bar's leading up-arrow becomes a trailing close, matching
`TipAmountEntryScreen`: dismissing drops the amount rather than stepping back.
The close control and a completed send both popped the AmountEntry entry
outright, which deletes the sheet scene on the spot — the sheet disappeared
instead of sliding down, while a swipe or system back animated normally.
Both now go through the scene's own dismissal, which settles the sheet at
Hidden and pops the entry on completion.

`ResultBackNavigator` gains an optional `exit` for this: it delivers the
result first, then hands the exit to the screen instead of popping. The
default is unchanged.
@github-actions github-actions Bot added the area: payments Payments, transfers, intents, billing label Aug 26, 2026
@bmc08gt
bmc08gt force-pushed the feat/chat-by-username branch from b848d45 to 29bf0ca Compare August 26, 2026 20:28
@bmc08gt
bmc08gt merged commit 041bb0c into code/cash Aug 26, 2026
5 of 6 checks passed
@bmc08gt
bmc08gt deleted the feat/chat-by-username branch August 26, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: payments Payments, transfers, intents, billing 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