React to a story with one tap
Android half of ios#52, spec §6.22. 🔴 Decoded before §6.9's reaction path, which requires `about`. A story reaction carries `sre` instead — a story is not in the conversation and has no `mid` there, so one carrying `re` would fold onto whatever message happened to share that id, or onto nothing. Falling through would leave it unhandled and drawn as an empty line in the thread. Tested both ways, including that an ordinary thread reaction still decodes as before. ⚠️ Its own disclosure and its own stored key. A reaction reveals that you watched exactly as a reply does; what differs is the deliberation. A reply is typed. A reaction is one tap on a story already filling the screen, so an account told about replies has been warned about the considered case and not the careless one. ⚠️ The row does not share a gesture with anything that navigates: an acknowledgement delivered by a mis-tap is a disclosure the person did not make. Branched from feat/story-replies, which carries the `sre` plumbing this reuses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
52a793ce5c
commit
8b719032c2
@@ -0,0 +1,71 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import org.json.JSONObject
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/** One-tap reactions to a story (spec §6.22, ios#52). */
|
||||
class StoryReactionTest {
|
||||
|
||||
@Test
|
||||
fun `a story reaction carries the emoji and the story`() {
|
||||
val content = Content.parse(Content.storyReaction("👍", "s7"))
|
||||
assertEquals("👍", content.reaction)
|
||||
assertEquals("s7", content.storyReplyId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 **Why §6.22 is its own path.** §6.9's reaction names a message by
|
||||
* `mid` and folds onto it. A story is not in the conversation and has no
|
||||
* `mid` there, so one carrying `re` would fold onto whatever message
|
||||
* happened to share that id — or onto nothing.
|
||||
*/
|
||||
@Test
|
||||
fun `a story reaction carries no message reference`() {
|
||||
val json = JSONObject(Content.storyReaction("👍", "s7"))
|
||||
assertFalse(json.has("re"))
|
||||
assertTrue(json.has("sre"))
|
||||
}
|
||||
|
||||
/** 🔴 Decoded before §6.9's path, which requires `re`. */
|
||||
@Test
|
||||
fun `a story reaction is never a thread reaction`() {
|
||||
val content = Content.parse(
|
||||
"""{"fnc":1,"kind":"react","caption":"x","sre":{"sid":"s7"}}""")
|
||||
assertNull(content.about)
|
||||
assertEquals("s7", content.storyReplyId)
|
||||
assertTrue(content.text.isEmpty())
|
||||
}
|
||||
|
||||
/** An ordinary §6.9 reaction must be untouched by this. */
|
||||
@Test
|
||||
fun `a thread reaction still decodes as before`() {
|
||||
val about = Content.Reference("m1", 3, "hi")
|
||||
val content = Content.parse(Content.reaction("🎉", about))
|
||||
assertEquals("m1", content.about?.messageId)
|
||||
assertNull(content.storyReplyId)
|
||||
assertEquals("🎉", content.reaction)
|
||||
}
|
||||
|
||||
/** ⚠️ An empty emoji withdraws one already sent. */
|
||||
@Test
|
||||
fun `an empty emoji is a withdrawal and still carries the story`() {
|
||||
val content = Content.parse(Content.storyReaction("", "s7"))
|
||||
assertEquals("", content.reaction)
|
||||
assertEquals("s7", content.storyReplyId)
|
||||
}
|
||||
|
||||
/** A story reaction is not a bubble. */
|
||||
@Test
|
||||
fun `a story reaction is not renderable`() {
|
||||
assertFalse(Content.parse(Content.storyReaction("x", "s7")).isRenderable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the marker stays first`() {
|
||||
assertTrue(Content.storyReaction("x", "s").startsWith("{\"fnc\":1"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user