Queue undelivered stickers - the bubble already promised it
A sticker to somebody signed out was marked "Undelivered - will send" and then never entered the outbox, so the label was a lie for exactly one content kind. iOS got this fix earlier via a wire field on its outbox entry; Android's queue turned out not to need one - conversations.send transmits its argument as the ratchet plaintext verbatim, and a sticker's wire IS its plaintext, so the existing replay path delivers both kinds once the entry stores the encoded envelope instead of prose. OutboxTest gains the sticker version of the end-to-end promise: queued while they are away, delivered AS A STICKER once a device exists (a real second identity, not a fake bundle), and the bubble restated without turning into text. Six outbox tests green; engine, client and app suites green. ⚠️ The same lie still stands for GROUP sends, text and sticker both: they record PENDING when nobody was reachable and nothing queues them. That needs a thread-aware outbox (per-member retry, a completion rule, restating the group thread) - designed work, not a patch, and it is on the roadmap rather than half-built here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6a26668d72
commit
a68c9212d9
@@ -236,9 +236,19 @@ class Session(
|
||||
fun sendSticker(peerUserId: Long, pack: String, stickerId: String):
|
||||
Conversations.Sent {
|
||||
val sent = conversations.sendSticker(peerUserId, pack, stickerId)
|
||||
record(Vault.threadForUser(peerUserId), "", mine = true,
|
||||
val at = System.currentTimeMillis()
|
||||
record(Vault.threadForUser(peerUserId), "", mine = true, at = at,
|
||||
sticker = Sticker(pack, stickerId),
|
||||
delivery = if (sent.reachedSomeone) Delivery.DELIVERED else Delivery.PENDING)
|
||||
// 🔴 **Queued, like text — because the bubble already promised it.**
|
||||
// A sticker to somebody signed out was marked "Undelivered — will
|
||||
// send" and then never entered the outbox, so the label was a lie for
|
||||
// exactly one content kind. The queue stores the ratchet plaintext,
|
||||
// and a sticker's wire IS its plaintext, so the same replay path
|
||||
// delivers both.
|
||||
if (!sent.reachedSomeone) {
|
||||
enqueue(peerUserId, io.flamenet.e2e.Content.sticker(pack, stickerId), at)
|
||||
}
|
||||
return sent
|
||||
}
|
||||
|
||||
@@ -326,6 +336,13 @@ class Session(
|
||||
* ⚠️ Which is worth being honest about in the UI: "Sending…" here means
|
||||
* "waiting on your phone", not "in flight".
|
||||
*/
|
||||
/**
|
||||
* ⚠️ `text` is the ratchet PLAINTEXT, not necessarily prose. For a text
|
||||
* message the two are the same string; for a sticker it is the encoded
|
||||
* content envelope. `flushOutbox` replays it through `conversations.send`,
|
||||
* which transmits its argument as the plaintext verbatim — so both kinds
|
||||
* take the same path out.
|
||||
*/
|
||||
private fun enqueue(peerUserId: Long, text: String, at: Long) {
|
||||
val pending = vault.outbox()
|
||||
pending.put(JSONObject()
|
||||
|
||||
@@ -179,6 +179,49 @@ class OutboxTest {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 The sticker version of the promise, because it was broken separately:
|
||||
* a sticker to somebody signed out was marked "Undelivered — will send"
|
||||
* and then never entered the outbox — the label was a lie for exactly one
|
||||
* content kind. This asserts all three parts: queued, delivered AS A
|
||||
* STICKER once a device exists, and the bubble restated.
|
||||
*/
|
||||
@Test
|
||||
fun `a sticker to somebody with no device is queued and later delivered as a sticker`() {
|
||||
val session = session()
|
||||
server.enqueue(json("""{"devices":[]}"""))
|
||||
session.sendSticker(peer, "flamenet.core", "wave")
|
||||
|
||||
val thread = session.history(Vault.threadForUser(peer))
|
||||
assertEquals(1, thread.size)
|
||||
assertEquals("wave", thread[0].sticker?.id, "the bubble is a sticker, not text")
|
||||
assertEquals(Session.Delivery.PENDING, thread[0].delivery)
|
||||
assertEquals(1, session.vault.outbox().length(),
|
||||
"the sticker must be waiting in the outbox")
|
||||
|
||||
// They reinstall.
|
||||
val them = DeviceIdentity.create()
|
||||
val opk = them.generateOneTimePreKeys(1).first()
|
||||
val bundle = them.registrationWire().put(
|
||||
"opk", org.json.JSONObject()
|
||||
.put("id", opk.id)
|
||||
.put("pub", android.util.Base64.encodeToString(
|
||||
opk.publicKey, android.util.Base64.NO_WRAP)))
|
||||
server.enqueue(json(org.json.JSONObject()
|
||||
.put("devices", org.json.JSONArray().put(bundle)).toString()))
|
||||
server.enqueue(json("""{"ok":true}"""))
|
||||
|
||||
session.flushOutbox()
|
||||
|
||||
assertEquals(0, session.vault.outbox().length())
|
||||
assertEquals(Session.Delivery.DELIVERED,
|
||||
session.history(Vault.threadForUser(peer))[0].delivery,
|
||||
"the sticker bubble must stop saying undelivered")
|
||||
assertEquals("wave",
|
||||
session.history(Vault.threadForUser(peer))[0].sticker?.id,
|
||||
"restating the delivery must not turn the sticker into text")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a message stamped once is the one that gets restated`() {
|
||||
val session = session()
|
||||
|
||||
Reference in New Issue
Block a user