Skip to content

feat(chat): the edit and delete UI - #711

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

bmc08gt merged 10 commits into
mainfrom
feat/chat-message-actions

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 2, 2026 •

Copy link
Copy Markdown
Collaborator

Last of four stacked PRs adding edit and delete to chat messages. Stacked on #714 — this diff is against it.

Everything a person touches: the context menu that offers the actions, the composer that performs an edit, and the keyboard behaviour around both.

Contents

Context menu built from capabilities. The menu lists what the message allows, resolved upstream in #713, so adding a group permission later changes the resolution rather than this call site.

Composer takeover. An edit swaps the bar's controls instead of adding a banner above it, so entering and leaving an edit does not shift the transcript. Cancel replaces Send Cash in the leading slot; the submit button stays up for the whole edit with its glyph crossfading arrow to checkmark. Confirming an unchanged edit exits without a request. The cancel button needs an explicit content shape — the glyph is its only drawn content, so taps landing on the glass around it missed the button while the interactive platter lit up, leaving the edit open.

Keyboard handoff around the context menu. UIKit hides the keyboard for a context menu's whole lifetime but leaves the composer first responder, and lays the menu out in the space the keyboard vacated — so a long press left a caret with no keyboard, and a press near the bottom of the transcript put the bubble behind the keyboard once the menu landed. Three points fix it:

  • Freeze the transcript inset when the menu is configured, before anything moves, so the keyboard's space stays reserved and the transcript does not reflow under the lifted preview.
  • Resign at willDisplayContextMenu rather than at the long-press threshold, so the keyboard animates away alongside the menu. The resign stays outside the animator's block — the composer bar rides the keyboard's own notifications, and folding it in strands the bar at its keyboard-up position.
  • Re-take the responder as willEndContextMenuInteraction fires rather than in its completion; waiting for the menu to finish left a beat of empty composer before the keyboard moved.

Actions then follow the keyboard they need: edit focuses the composer, delete keeps it down so it does not rise behind the confirmation dialog. Delete asks "Delete message?" over "This can't be undone", and its destructive action names the scope — "Delete For Everyone" — because WhatsApp's sheet offers that beside "Delete for me" and we have no delete-for-me to distinguish it from.

Blur behind the menu, held through the edit. UIKit only dims the content behind a context menu, so every other bubble stays legible under the platter; WhatsApp blurs it. MessageBackdrop fades a .systemUltraThinMaterialDark view over the navigation controller's view, which the menu's own container sits above, so the platter and the lifted bubble stay sharp.

Choosing Edit claims that same blur from the menu action itself, before the dismissal would fade it, and drops it below the composer bar — so the transcript never flashes back to legible between the two states, and the field stays sharp. The edited message is floated above the blur as a detached snapshot, re-framed on scroll and layout: a UIVisualEffectView renders its backdrop through a private layer that ignores layer.mask, so cutting a hole for the real bubble does not render. Tapping the blur ends the edit, as tapping outside the message does in WhatsApp.

.maestro/login.yaml signs in through the flipcash://login deep link so simulator runs can reach a conversation without hand-driving onboarding; the access key is passed at run time, not stored.

Reply is scoped to a separate plan and is not built here.

@bmc08gt bmc08gt self-assigned this Sep 2, 2026
@bmc08gt bmc08gt changed the title feat(chat): edit and delete messages feat(chat): the edit and delete UI Sep 2, 2026
@bmc08gt
bmc08gt changed the base branch from main to feat/chat-message-mutations September 2, 2026 17:15
@bmc08gt
bmc08gt force-pushed the feat/chat-message-actions branch from 18c7913 to 326ad15 Compare September 2, 2026 17:23
@bmc08gt
bmc08gt force-pushed the feat/chat-message-mutations branch 2 times, most recently from 11f6f26 to 617e567 Compare September 2, 2026 22:23
@bmc08gt
bmc08gt force-pushed the feat/chat-message-actions branch from 94f82ef to 123be48 Compare September 2, 2026 22:23
@bmc08gt
bmc08gt force-pushed the feat/chat-message-mutations branch from 617e567 to 36eb1e5 Compare September 2, 2026 22:31
The composer field now serves both writing a new message and revising an existing one.
ComposerModel holds the draft, the mode, and the stashed new-message draft, so beginning
an edit does not discard what was being typed and cancelling puts it back. An edit whose
text matches the original cannot be submitted.
The menu was hardcoded to Copy for text rows. It now renders whatever actions the mapper
put on the message, in that order, so what a row offers is decided once by the capability
model rather than again at the point of presentation. Copy still resolves inside the
transcript; the rest report the row's id to the screen.

Menu construction is split out of the configuration so the contents can be asserted
without presenting a menu — UIContextMenuConfiguration does not expose its provider.
Edit loads the message into the composer; delete confirms first, because it applies for
everyone in the chat. A conflict or a failure reads as "nothing happened" on the
transcript, so the controller's mutation alert is surfaced as a dialog.
The mapper defaults to hiding deleted rows for callers that pass nothing, and the
coordinator passes the policy's presentation instead. This asserts the two agree, so a
change to either default fails here rather than in the transcript.
The editing banner sat above the bar and pushed the transcript, so
entering and leaving an edit shifted the messages. WhatsApp keeps the
bar's own footprint and swaps its controls, so do that: while editing,
a cancel control replaces Send Cash in the leading slot and the submit
button stays up for the whole edit, its glyph crossfading arrow to
checkmark. Confirming an unchanged edit exits without a request.

The cancel button needs an explicit content shape — the glyph is its
only drawn content, so taps landing on the glass around it were missing
the button while the interactive platter lit up, leaving the edit open.

The bar's stack is top-aligned now that the controls sit beside a field
that grows downward.

Delete's confirmation copy follows WhatsApp: "Delete message?", "This
can't be undone.", "Delete for everyone".
UIKit hides the keyboard for a context menu's whole lifetime but leaves
the composer first responder, so a long press left a blinking caret,
no keyboard, and nothing to type into. It also lays the menu out in the
space the keyboard vacated, which is why a press near the bottom of the
transcript put the bubble behind the keyboard once the menu landed.

Three points, each picked for a reason:

- Freeze the inset when the menu is configured, before anything moves,
  so the keyboard's space stays reserved and the transcript does not
  reflow under the lifted preview.
- Resign at willDisplayContextMenu, not at the long-press threshold, so
  the keyboard animates away alongside the menu. The resign stays out of
  the animator's block — the composer bar rides the keyboard's own
  notifications, and folding it in strands the bar at its keyboard-up
  position.
- Re-take the responder as willEndContextMenuInteraction fires rather
  than in its completion; waiting for the menu to finish left a beat of
  empty composer before the keyboard moved.

Actions follow the keyboard they need: edit focuses the composer, delete
keeps it down so it does not rise behind the confirmation sheet.
The spec described an inline "Edited" suffix and a banner above the
composer; both were replaced during implementation. Update it to the
corner-pinned marker with its clear reservation run and the bar-takeover
composer.

.maestro/login.yaml signs in through the flipcash://login deep link so
simulator runs can reach a conversation without hand-driving onboarding.
The access key is passed at run time (-e ACCESS_KEY=...), not stored.
UIKit only dims the content behind a context menu, so every other bubble stays
legible under the platter. WhatsApp blurs it, leaving the lifted bubble as the
one sharp thing on screen.

`ContextMenuBackdrop` fades a `UIVisualEffectView` in over the navigation
stack — transcript, composer and navigation bar together — riding the menu's own
animator so it lands and leaves with the platter. The transcript's present and
dismiss callbacks now carry that animator. The effect is
`.systemUltraThinMaterialDark`: on a near-black transcript anything heavier
erases the bubbles instead of softening them.
"Delete for everyone" said what happens but read as a sentence fragment
next to Cancel, and the subtitle's full stop was the only one on a
two-line dialog. Title-case the action to match the other destructive
labels in the app ("Log Out", "Permanently Delete Account", "Block") and
drop the period.
WhatsApp keeps the transcript blurred after Edit, with the edited message
the one sharp thing above the composer. We faded the blur out with the menu
and dropped straight back to a legible transcript.

The menu's blur is now claimed from the Edit action itself — before
`willEndContextMenuInteraction` would have faded it — reparented below the
composer bar so the field stays sharp, and given a tap target, so tapping
outside the message cancels the edit as well as the X does.

The edited message is a detached snapshot floated above the blur rather than
a hole cut in it: `UIVisualEffectView` renders its backdrop through a layer
that ignores `layer.mask`, and the real bubble can't be raised out of the
collection view that owns it. The copy is re-framed on scroll and on layout,
so it follows the row as the keyboard and the bar reflow the transcript.

`afterContextMenu` now queues its work instead of replacing it. An edit
hands over two pieces of follow-up — raise the keyboard, pin the spotlight —
and the second assignment was dropping the first.

Verified on the simulator for a row mid-transcript and for the last row,
where the copy lands directly above the composer.
@bmc08gt
bmc08gt force-pushed the feat/chat-message-actions branch from 123be48 to 8c7cd09 Compare September 2, 2026 22:38
@bmc08gt
bmc08gt changed the base branch from feat/chat-message-mutations to main September 2, 2026 22:38
@bmc08gt
bmc08gt merged commit 77b7bf8 into main Sep 2, 2026
1 check passed
@bmc08gt
bmc08gt deleted the feat/chat-message-actions branch September 15, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant