feat(chat): start a chat by typing someone's username - #1338
Merged
Conversation
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.
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.
bmc08gt
force-pushed
the
feat/chat-by-username
branch
from
August 26, 2026 20:28
b848d45 to
29bf0ca
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.getProfileForUsernameand opens the conversation.How the chat opens before it exists
ChatIdentifier.ByUsercarries 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.An empty tip DM has no composer — typing is gated on a first payment — so
SendCashButtontakes 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:
TipChatRowalready renders name-or-handle, preview, last activity and unread count, andAppNavigationBaralready 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 chatreplaces 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 onviewModelScope, so backing out mid-hold cancels the navigation. Only the new-chat lookup adopts it here.Result.onSuccessWithDelaydoes 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 reportedSent Cash. It sendsTIPCARDand reportsSent Tipinstead.Locationhas 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 reachesonRootReached, 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.AmountEntryis now aSheetand the chat flow runs the modal sheet scene strategy, the same mechanismSwapFlowScreenuses for its currency pickers, so the conversation stays visible behind it. The app bar's leading up-arrow becomes a trailing close to matchTipAmountEntryScreen— dismissing drops the amount rather than stepping back.