Skip to content

feat(chat): add the message selection bar, edit composer and delete - #1394

Merged
bmc08gt merged 10 commits into
code/cashfrom
feat/chat-message-actions
Sep 2, 2026
Merged

feat(chat): add the message selection bar, edit composer and delete#1394
bmc08gt merged 10 commits into
code/cashfrom
feat/chat-message-actions

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 group
role 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.blur needs 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: TextBubble loses its SelectionContainer. Its text
selection 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.

@bmc08gt bmc08gt self-assigned this Sep 2, 2026
@github-actions github-actions Bot added area: ui Compose UI, theme, components, resources type: feature New functionality labels Sep 2, 2026
@bmc08gt
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
bmc08gt force-pushed the feat/chat-message-actions branch from c62e54a to fca272a Compare September 2, 2026 22:16
@bmc08gt
bmc08gt merged commit 96cb620 into code/cash Sep 2, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/chat-message-actions branch September 3, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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