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):
|
fun sendSticker(peerUserId: Long, pack: String, stickerId: String):
|
||||||
Conversations.Sent {
|
Conversations.Sent {
|
||||||
val sent = conversations.sendSticker(peerUserId, pack, stickerId)
|
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),
|
sticker = Sticker(pack, stickerId),
|
||||||
delivery = if (sent.reachedSomeone) Delivery.DELIVERED else Delivery.PENDING)
|
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
|
return sent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +336,13 @@ class Session(
|
|||||||
* ⚠️ Which is worth being honest about in the UI: "Sending…" here means
|
* ⚠️ Which is worth being honest about in the UI: "Sending…" here means
|
||||||
* "waiting on your phone", not "in flight".
|
* "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) {
|
private fun enqueue(peerUserId: Long, text: String, at: Long) {
|
||||||
val pending = vault.outbox()
|
val pending = vault.outbox()
|
||||||
pending.put(JSONObject()
|
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
|
@Test
|
||||||
fun `a message stamped once is the one that gets restated`() {
|
fun `a message stamped once is the one that gets restated`() {
|
||||||
val session = session()
|
val session = session()
|
||||||
|
|||||||
Reference in New Issue
Block a user