The transcript records a message id and a reply reference

🔴 The wire always carried both and Session.Line never stored them.
Content.groupText has taken a messageId since §6.9 shipped and parse
reads it back — the transcript was where it stopped. So nothing here
could name an earlier message: no replies to resolve, no reactions to
fold, no edits to apply. All three were "codec done" and none worked.

A reply from the iPhone app arrived as a bare message with the quote
silently removed, which is worse than the missing id: the reader had no
way to know anything had been dropped.

🔴 Also fixes a real bug in sendGroup. It built the envelope twice —
once to send, once for the outbox — so an id minted inside either would
differ between them, and a member who was offline would eventually
receive a DIFFERENT message from everybody else, which nothing could
reconcile with a reply or a reaction. One id, minted by the caller, used
by both.

Both fields written only when present: a null on every message would
grow the transcript for nothing and make "no id" indistinguishable from
"an id of nothing".

Refs paulhitt/flamenet-messenger-ios#58, #10

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-02 15:50:34 -04:00
co-authored by Claude Opus 4.8
parent 0dec029917
commit 3b7e69c0eb
4 changed files with 155 additions and 6 deletions
@@ -212,10 +212,22 @@ class Conversations(
* at the sender as an incoming one. The sender's own copy is written
* locally by the caller.
*/
fun sendToGroup(group: RelayClient.Group, text: String, myUserId: Long): Sent {
/**
* @param messageId one id for the whole fan-out (§6.9). It is ONE message:
* a reply or a reaction from any member has to name the same thing, so
* per-recipient ids would make that impossible.
*
* ⚠️ Taken as a parameter rather than minted here, because the caller has
* to build the same envelope again for the outbox. Two constructions with
* two ids means a queued retry arrives as a *different* message from the
* one everybody else got.
*/
fun sendToGroup(group: RelayClient.Group, text: String, myUserId: Long,
messageId: String? = null): Sent {
val delivered = mutableListOf<String>()
val failed = LinkedHashMap<String, String>()
val body = Content.groupText(text, group.id, groupName = vault.groupName(group.id))
val body = Content.groupText(text, group.id, messageId = messageId,
groupName = vault.groupName(group.id))
for (member in group.memberIds) {
if (member == myUserId) continue