feat(chat): edit and delete RPCs with optimistic reconciliation - #714
Merged
Merged
Conversation
bmc08gt
force-pushed
the
feat/chat-tombstone-presentation
branch
from
September 2, 2026 17:23
4b5d667 to
a76b007
Compare
bmc08gt
force-pushed
the
feat/chat-message-mutations
branch
from
September 2, 2026 17:23
91af46f to
11f6f26
Compare
bmc08gt
force-pushed
the
feat/chat-tombstone-presentation
branch
from
September 2, 2026 22:23
a76b007 to
32cfe22
Compare
bmc08gt
force-pushed
the
feat/chat-message-mutations
branch
from
September 2, 2026 22:23
11f6f26 to
617e567
Compare
…store `MutationEntry` is the mirror of `PendingEntry`: it describes a mutation that has been issued but not confirmed, and `displayedMessages` applies it over the stored row so the transcript reflects an edit or delete immediately. The overlay is bounded by `expectedSequence` — the `eventSequence` the message carried when the mutation was issued, and the same value sent as `expected_event_sequence`. Once the stored row moves past it, the server's answer has landed and the overlay stops applying, so a refused mutation reverts without the caller having to notice.
Both requests carry `expected_event_sequence`, the server's optimistic-concurrency guard, and both responses return a message on `CONFLICT` as well as on `OK` — the state that won. `MessageMutation` keeps that message alongside the conflict flag, so a caller reconciling a lost race has the winning copy rather than having to wait for the next delta. `conflict` reports at `.info`: it is the guard working, not a client defect.
`edit` and `delete` overlay the change, issue the RPC, then persist whatever the server returns. Accepted and conflicted responses take the same path: a conflict's payload is the state that won, which is exactly what has to land locally. There is no retry — reissuing would clobber the change that beat this one. `expected_event_sequence` is read from the database, never from the overlay. A second edit that read its own optimistic value would send a sequence the server never assigned and conflict every time. A message with sequence 0 is unconfirmed and cannot produce a valid request, so both methods refuse it before the call. The failure path drops the overlay and reports through `mutationAlert`; the transcript reverts on its own once the overlay is gone.
bmc08gt
force-pushed
the
feat/chat-message-mutations
branch
from
September 2, 2026 22:31
617e567 to
36eb1e5
Compare
bmc08gt
changed the base branch from
feat/chat-tombstone-presentation
to
main
September 2, 2026 22:31
bmc08gt
added a commit
that referenced
this pull request
Sep 2, 2026
Last of the four PRs adding edit and delete to chat messages, on top of #714. 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.
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.
Third of four stacked PRs adding edit and delete to chat messages. Stacked on #713 — this diff is against it.
The transport and the optimistic path. After this, edit and delete work end to end through the controller; only the UI that calls them is missing.
Contents
Edit and delete RPCs.
ChatMessagingServicewraps them, with the client protocol and mock updated so tests can drive both outcomes.Optimistic overlay.
ConversationStoreholds a pending mutation over the stored message rather than mutating it, so the transcript shows the change immediately and the stored row stays the server's version until the server agrees.MutationEntryis what gets overlaid.Reconciliation.
ConversationController+MessageMutationsapplies the overlay, sends the request, and either clears the entry when the server confirms or rolls it back when it does not.Tests cover the overlay's effect on reads, and the controller's confirm and rollback paths for both edit and delete.
The store and transport work here does not actually depend on #713's rendering — the stack is linear because the commits are, and either could merge first if the branches are re-pointed.