Skip to content

feat(chat): edit and delete RPCs with optimistic reconciliation - #714

Merged
bmc08gt merged 3 commits into
mainfrom
feat/chat-message-mutations
Sep 2, 2026
Merged

bmc08gt merged 3 commits into
mainfrom
feat/chat-message-mutations

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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. ChatMessagingService wraps them, with the client protocol and mock updated so tests can drive both outcomes.

Optimistic overlay. ConversationStore holds 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. MutationEntry is what gets overlaid.

Reconciliation. ConversationController+MessageMutations applies 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.

@bmc08gt bmc08gt self-assigned this Sep 2, 2026
@bmc08gt
bmc08gt force-pushed the feat/chat-tombstone-presentation branch from 4b5d667 to a76b007 Compare September 2, 2026 17:23
@bmc08gt
bmc08gt force-pushed the feat/chat-message-mutations branch from 91af46f to 11f6f26 Compare September 2, 2026 17:23
@bmc08gt
bmc08gt force-pushed the feat/chat-tombstone-presentation branch from a76b007 to 32cfe22 Compare September 2, 2026 22:23
@bmc08gt
bmc08gt force-pushed the feat/chat-message-mutations branch from 11f6f26 to 617e567 Compare September 2, 2026 22:23
…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
bmc08gt force-pushed the feat/chat-message-mutations branch from 617e567 to 36eb1e5 Compare September 2, 2026 22:31
@bmc08gt
bmc08gt changed the base branch from feat/chat-tombstone-presentation to main September 2, 2026 22:31
@bmc08gt
bmc08gt merged commit fcff619 into main Sep 2, 2026
1 check passed
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.
@bmc08gt
bmc08gt deleted the feat/chat-message-mutations 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