Consume control kinds this build has never heard of (§6.10)
`isRenderable` asked `kind !in Kind.CONTROL` — an allowlist of the four control kinds that existed when it was written. A control kind from a newer peer is in no set here, so it fell through and rendered as an "[unsupported attachment]" bubble: a message the user can neither read nor act on, for something addressed to a layer rather than a person. Every control kind added from here would have shipped that to every older install. `ctl: 1` is the flag that stops it; `isControl` trusts the flag as well as the list, since the four that predate it do not carry it and never will. ⚠️ `needsNewerClient` is deliberately NOT part of `isRenderable`. Unknown *content* already parses to a placeholder and is shown, which is right — the first cut of this change suppressed it and an existing test caught that immediately. Dropping a real message silently is the one failure a messenger cannot make, so the flag decides rather than the unknown-ness. engine 144, client 90, app 32. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cc8f4ca1d0
commit
c9e386ec0d
@@ -127,7 +127,35 @@ data class Content(
|
||||
* and control messages are consumed silently. Everything else is a message.
|
||||
*/
|
||||
val isRenderable: Boolean
|
||||
get() = reaction == null && call == null && kind !in Kind.CONTROL
|
||||
get() = reaction == null && call == null && !isControl
|
||||
|
||||
/**
|
||||
* Control traffic: consumed by a layer, never shown.
|
||||
*
|
||||
* 🔴 **An allowlist of known kinds was not enough.** `kind !in CONTROL` is
|
||||
* only correct for kinds this build has heard of; a control message from a
|
||||
* NEWER peer has a kind that is in no set here, so it fell through and was
|
||||
* rendered — a bubble full of JSON in somebody's conversation. Every future
|
||||
* control kind would have shipped that bug to every older install.
|
||||
*
|
||||
* So new control kinds carry `"ctl":1`, and this trusts that flag as well
|
||||
* as the list. The list stays because the kinds that predate the flag do
|
||||
* not carry it and never will.
|
||||
*/
|
||||
val isControl: Boolean
|
||||
get() = kind in Kind.CONTROL || raw?.optBoolean("ctl", false) == true
|
||||
|
||||
/**
|
||||
* A kind this build does not know, which is not control traffic.
|
||||
*
|
||||
* ⚠️ Deliberately **not** part of [isRenderable]. Unknown content already
|
||||
* parses to a `[unsupported attachment]` placeholder and is shown, which is
|
||||
* right: a peer sent the user something real, and silently dropping a
|
||||
* message is the one failure a messenger cannot make. This exists so the UI
|
||||
* can say something better than that placeholder if it wants to.
|
||||
*/
|
||||
val needsNewerClient: Boolean
|
||||
get() = kind !in Kind.KNOWN && !isControl
|
||||
|
||||
object Kind {
|
||||
const val TEXT = "text"
|
||||
@@ -169,6 +197,19 @@ data class Content(
|
||||
* chat.
|
||||
*/
|
||||
val CONTROL = setOf(DELIVERY_KEY, KEY_LOG_HEAD, SENT_COPY, AVATAR)
|
||||
|
||||
/**
|
||||
* Every kind this build can act on.
|
||||
*
|
||||
* ⚠️ Anything outside this set came from a newer peer. It is either
|
||||
* control traffic (swallowed, via the `ctl` flag) or content we cannot
|
||||
* draw (shown as a placeholder) — never raw JSON in a bubble.
|
||||
*/
|
||||
val KNOWN = CONTROL + setOf(
|
||||
TEXT, REACT, STICKER,
|
||||
Attachment.IMAGE, Attachment.AUDIO, Attachment.STORY,
|
||||
CallSignal.OFFER, CallSignal.ANSWER, CallSignal.END,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -137,6 +137,47 @@ class ContentTest {
|
||||
assertEquals("to everyone", parsed.text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 Forward compatibility for control traffic.
|
||||
*
|
||||
* Control messages ride the same channel as chat and are recognised by
|
||||
* NAME — an allowlist of kinds this build knows. A control kind from a
|
||||
* newer peer is in no such set, so it fell through and became a
|
||||
* `[unsupported attachment]` bubble: a message the user can neither read
|
||||
* nor act on, for something never meant for them to see.
|
||||
*
|
||||
* `ctl: 1` is what lets every kind added from here on stay quiet.
|
||||
*/
|
||||
@Test
|
||||
fun `a flagged unknown control kind is consumed, not rendered`() {
|
||||
val parsed = Content.parse("""{"fnc":1,"kind":"something-new","ctl":true}""")
|
||||
assertTrue(parsed.isControl, "a ctl-flagged envelope is control traffic")
|
||||
assertFalse(parsed.isRenderable, "and must never reach the thread")
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ The opposite has to stay true. Unknown *content* is still shown, with
|
||||
* a placeholder — silently dropping a real message is the one failure a
|
||||
* messenger cannot make, so the flag is what decides, not the unknown-ness.
|
||||
*/
|
||||
@Test
|
||||
fun `an unknown kind without the flag is still shown`() {
|
||||
val parsed = Content.parse("""{"fnc":1,"kind":"hologram"}""")
|
||||
assertTrue(parsed.needsNewerClient)
|
||||
assertFalse(parsed.isControl)
|
||||
assertTrue(parsed.isRenderable)
|
||||
}
|
||||
|
||||
/** Kinds that predate the flag keep working without carrying it. */
|
||||
@Test
|
||||
fun `known control kinds are still control without the flag`() {
|
||||
for (kind in listOf("delivery_key", "kt_head", "sent", "avatar")) {
|
||||
val parsed = Content.parse("""{"fnc":1,"kind":"$kind"}""")
|
||||
assertTrue(parsed.isControl, "$kind must stay control traffic")
|
||||
assertFalse(parsed.isRenderable, "$kind must not render")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown attachment kind shows a label rather than disappearing`() {
|
||||
val parsed = Content.parse("""{"fnc":1,"kind":"hologram"}""")
|
||||
|
||||
Reference in New Issue
Block a user