"Unsend" did not unsend. Say what the button does (android#18)

The message menu labelled the delete `if (line.mine) "Unsend" else "Delete
for me"`, and both branches call `deleteMessage` → `Session.deleteMessage` →
`vault.deleteLine`. That is the whole implementation. No envelope leaves the
phone; the recipient's copy stays exactly where it is.

🔴 **Worse than an overclaiming page**, which is why this got a test and not
just an edit. A page that overclaims can be read sceptically. A button that
reports success cannot: the feedback IS the claim, and it arrives at the
moment the person is most relieved to believe it.

"Delete for me" was already the other half of that ternary, and it was always
the accurate half.

⚠️ Two tests, not one, because the first is worthless without the second: a
source scan that walks the wrong directory finds no files and passes every
assertion in it. `the scan actually reaches the chat screen` pins that the
walk reaches the file the bug was on. The walk climbs to the module root
rather than resolving a relative path, for the same reason.

Proved by breaking it, not by watching it pass:

  label reverted → FAILED, naming ChatScreens.kt line ~1564
  restored       → 2 tests, 0 failures

1190 tests pass across the suite.

⚠️ This is ios#57 on the other platform, fixed there in the same pass. A real
retract is a new `kind` on rails that already exist — §6.15's edit machinery
is implemented and tested on both clients. But even then it is a request, not
a guarantee: a client can ignore it and the message may already have been
read. Whatever the button eventually says must not promise more than "we
asked them to remove it."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-04 22:06:57 -04:00
co-authored by Claude Opus 5
parent 99a33627a4
commit 8c2d7f1f65
2 changed files with 101 additions and 3 deletions
@@ -1953,7 +1953,16 @@ fun ColumnScope.ChatScreen(
)
}
DropdownMenuItem(
text = { Text(if (line.mine) "Unsend" else "Delete for me") },
// 🔴 **Said "Unsend" on your own messages and did
// nothing of the kind (android#18).** `deleteMessage`
// reaches `vault.deleteLine` and stops there — no
// envelope leaves the phone, and the recipient's copy
// stays exactly where it is. Somebody who sent to the
// wrong person tapped it, watched the bubble vanish,
// and had no way to find out. "Delete for me" was
// already the other half of this ternary, and it was
// always the accurate half.
text = { Text("Delete for me") },
onClick = { model.deleteMessage(line); menuOpen = false },
modifier = Modifier.testTag("delete-message"),
)
@@ -3950,8 +3959,8 @@ internal fun memberLabel(
* rather not. The cursor sits at the end for the same reason.
*
* ⚠️ Saving nothing is refused rather than treated as a delete. Emptying the
* box is the shape of a slip, and "Unsend" is right there in the same menu for
* anybody who meant it.
* box is the shape of a slip, and "Delete for me" is right there in the same
* menu for anybody who meant it.
*/
@Composable
private fun EditMessageSheet(