feat(chat): the edit and delete UI - #711
Merged
Merged
Conversation
bmc08gt
force-pushed
the
feat/chat-message-actions
branch
from
September 2, 2026 17:23
18c7913 to
326ad15
Compare
bmc08gt
force-pushed
the
feat/chat-message-mutations
branch
2 times, most recently
from
September 2, 2026 22:23
11f6f26 to
617e567
Compare
bmc08gt
force-pushed
the
feat/chat-message-actions
branch
from
September 2, 2026 22:23
94f82ef to
123be48
Compare
bmc08gt
force-pushed
the
feat/chat-message-mutations
branch
from
September 2, 2026 22:31
617e567 to
36eb1e5
Compare
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
force-pushed
the
feat/chat-message-actions
branch
from
September 2, 2026 22:38
123be48 to
8c7cd09
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.
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:
willDisplayContextMenurather 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.willEndContextMenuInteractionfires 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.
MessageBackdropfades a.systemUltraThinMaterialDarkview 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
UIVisualEffectViewrenders its backdrop through a private layer that ignoreslayer.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.yamlsigns in through theflipcash://logindeep 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.