feat(chat): add the message selection bar, edit composer and delete - #1394
Merged
Conversation
bmc08gt
changed the base branch from
feat/chat-message-presentation
to
code/cash
September 2, 2026 22:14
Long-pressing a bubble raises a selection bar in place of the conversation title, offering exactly what the transcript already resolved for that message: delete inline, copy and edit under the overflow. The bar reads ContentBubble.capabilities rather than deciding anything itself, so a later group role taxonomy changes the resolver and not this surface. Editing takes over the composer rather than stacking a banner above it. Cancel replaces send-cash in the leading slot and the submit glyph animates to a checkmark, so the bar keeps its shape and the same control changes meaning. Whatever the composer held when the edit began is stashed and put back when the edit ends, whether by confirming, cancelling or backing out, and confirming an unchanged message exits without a request. TextBubble loses its SelectionContainer: its selection handles consumed the long-press before the row could see it. In-bubble text selection is gone as a result, and copying a message is the selection bar's Copy action instead.
Follow-ups from the first pass on a device: - The selection count only ever read "1", and the back arrow already says the bar is a selection, so drop it. - The row highlight fought the bubble's own background. Blurring and dimming the rest of the transcript instead reads better and matches the backdrop iOS holds from the context menu through the edit that can follow it. - The pressed row dips and the selected one springs up, so a long-press answers the finger rather than only changing the bar. - BottomBarManager renders below the IME and a long-press leaves the IME up, so the delete confirmation opened invisible. Hide the keyboard first. - Round the overflow menu and drop it clear of the button it hangs from. - Editing holds the bar in its takeover state with a bare back arrow: falling back to the title bar mid-edit offered the profile and left the system back gesture as the only way out.
KeyboardController.visible only starts tracking from the composition that creates it. The selection bar is composed by a long-press that leaves the IME up, so its own controller read the keyboard as hidden and hideIfVisible skipped the hide — putting the confirmation sheet behind the keyboard, which is where BottomBarManager renders. Hold the controller in ChatTopBar, which is composed before the IME ever comes up.
The blurred transcript still took row gestures while an edit was open, so a long-press behind the backdrop swapped the selection out from under the message the composer was editing, and a plain tap only dismissed the keyboard. Rows accept no gestures while `state.editing` is set, and a tap anywhere on the transcript cancels the edit instead — the exit iOS gives its held blur.
The selection backdrop looked modal but wasn't: rows behind it still took taps, so a tap moved the selection to whichever message it landed on, and Edit then acted on that one instead of the message the bar was raised for. The rows take no gestures for as long as the backdrop is up, and a tap on the transcript dismisses what is up — the edit if one is open, otherwise the selection. The row's own tap is now only reachable with the backdrop down, so it does nothing but dismiss the keyboard.
Cancelling the sheet left the message alone behind the backdrop under a selection bar the user had just backed out of, which reads as a second confirmation still pending. The sheet's close now ends the selection either way, so cancelling returns the transcript to rest. Also title-cases the confirm button to "Delete For Everyone".
…flag The composer took `isEditing` and `onSendMessage` as separate parameters, so the call site read `state.editing != null` twice — once to pick the glyph, once inside the lambda to pick between SubmitEdit and SendMessage. Nothing tied the two reads together, so a checkmark that sends a new message was a one-line mistake away. `ChatInputSubmit` carries the action with the kind, so the caller reads its edit state once and the glyph cannot disagree with what the tap does. The click reads the current submit rather than the one the crossfade is showing, so a tap mid-transition does what the composer is now for. An overload pair would have cost the animation: two composables behind an if/else occupy different composition slots, so entering an edit would tear down the text field and swap in a second one, losing focus and replacing the glyph crossfade with a hard cut.
The confirmation sheet rises over the transcript, and the selected message stayed sharp and lifted behind it — half of it clipped by the sheet's top edge, the rest reading as still live while a modal asked about it. `confirmingDelete` puts every row behind the backdrop for as long as the sheet is up. The selection itself is untouched, so Cancel still has a bar to return to; only the focus goes. The flag clears wherever the selection does, so the sheet's close — confirmed or cancelled — brings the transcript back to rest. Also drops the trailing period from the sheet's body.
The bar named its own split: delete inline, copy and edit under the overflow. That was one layout applied to every screen, and on a 411dp phone all three actions fit with room to spare, so two of them sat behind a menu for no reason. The actions now get a share of the bar — 35% — and take icons in priority order for as far as that reaches, with the remainder collapsing into the overflow. A 411dp phone shows delete, copy and edit; a 320dp one falls back to delete inline plus an overflow holding the other two, which is what the bar did everywhere before. The overflow only claims a slot when it has something to hold, so the last action is never displaced by a menu containing only it. Both the icons and the menu items now come off one list, so a label and the action it fires cannot drift apart, and reply takes its place in that order when it lands rather than arriving with a layout decision attached.
The backdrop gated the rows but not the targets inside them, so a cash bubble behind a raised selection bar still opened token info — and so did the contact card at the start of history. Both now read the same flag the rows do, which drops their click rather than consuming the tap, so the press falls through to the backdrop and dismisses like a tap anywhere else on the dimmed transcript. Token info opens as a push from here rather than the fade-in-place expand. That transition is the wallet card growing into its own detail; a transcript has no card for it to grow from, and discovery already drills in the same way.
bmc08gt
force-pushed
the
feat/chat-message-actions
branch
from
September 2, 2026 22:16
c62e54a to
fca272a
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.
Stacked on #1393. This is the part the user touches.
Long-pressing a bubble raises a selection bar in place of the conversation title: a back arrow, then delete, copy and edit as icons. How many of those stay icons falls out of the width — the actions get 35% of the bar and whatever does not fit collapses into an overflow, so a 411dp phone shows all three and a 320dp one keeps delete inline with the rest a tap away. The bar renders straight from
ContentBubble.capabilities— it decides nothing itself, so the grouprole taxonomy, when it arrives, changes the resolver from #1392 and not this surface. A bubble whose
only capability is reply is not selectable at all, since a long-press there would open a bar with
nothing in it.
Selection is one message at a time. Every capability applies to a single message, so a multi-select
would only ever be a bar with most of its actions disabled. The bar carries no count for the same
reason — it would only ever read "1".
Selecting a message blurs and dims the rest of the transcript and leaves the selected bubble sharp,
and the bubble springs up under the press. The backdrop is held through the edit that a selection can
lead to, matching what iOS carries from its context menu into the edit.
Modifier.blurneeds API 31,so below that the treatment degrades to the dim alone. The backdrop is modal: the transcript takes no
gestures for as long as it is up, and a tap on it dismisses what is up rather than reaching the
message underneath. That covers the targets inside the rows too — a cash bubble behind the bar would
otherwise have opened token info from under it, as would the contact card at the start of history.
Tapping a cash bubble now pushes token info rather than expanding it in place. The fade-in-place
expand is the wallet card growing into its own detail, and a transcript has no card for it to grow
from; discovery already drills in the same way.
Editing takes over the composer rather than stacking a banner above it. Cancel replaces send-cash in
the leading slot, and the submit glyph animates from the send arrow to a checkmark so the same
control changes meaning instead of two controls swapping places. Whatever the composer held when the
edit began is stashed and restored when the edit ends — confirmed, cancelled, or backed out — so
starting an edit never costs a half-written message. Confirming without having changed the text exits
edit mode without sending a request.
Delete asks first, through the app's own bottom sheet rather than a Material dialog: "Delete
message?" / "This can't be undone" / "Delete For Everyone". The sheet takes the focus with it — the
selected message drops back behind the backdrop with the rest rather than sitting sharp and clipped
at the sheet's edge — while the selection itself survives, so Cancel has a bar to return to. Closing
the sheet ends the selection whichever way it closes, so cancelling returns the transcript to rest
rather than back to a bar the user just backed out of.
Editing holds the bar in its takeover state with a bare back arrow rather than dropping back to the
conversation title, which would offer the profile mid-edit.
Back unwinds innermost first — an open edit, then the selection bar, then the conversation.
One behaviour is removed rather than added:
TextBubbleloses itsSelectionContainer. Its textselection handles consumed the long-press before the row could see it, so in-bubble text selection is
gone and copying a message is now the selection bar's Copy action.