diff --git a/app/src/main/assets/stickers/flamenet.core.json b/app/src/main/assets/stickers/flamenet.core.json new file mode 100644 index 0000000..d43933d --- /dev/null +++ b/app/src/main/assets/stickers/flamenet.core.json @@ -0,0 +1,31 @@ +{ + "pack": "flamenet.core", + "name": "Flamenet", + "fallback": "๐Ÿ™‚", + "stickers": [ + { "id": "wave", "glyph": "๐Ÿ‘‹", "name": "Wave" }, + { "id": "yes", "glyph": "๐Ÿ‘", "name": "Yes" }, + { "id": "no", "glyph": "๐Ÿ‘Ž", "name": "No" }, + { "id": "laugh", "glyph": "๐Ÿ˜‚", "name": "Laughing" }, + { "id": "love", "glyph": "โค๏ธ", "name": "Love" }, + { "id": "party", "glyph": "๐ŸŽ‰", "name": "Party" }, + { "id": "think", "glyph": "๐Ÿค”", "name": "Thinking" }, + { "id": "cry", "glyph": "๐Ÿ˜ญ", "name": "Crying" }, + { "id": "fire", "glyph": "๐Ÿ”ฅ", "name": "Fire" }, + { "id": "clap", "glyph": "๐Ÿ‘", "name": "Applause" }, + { "id": "sleep", "glyph": "๐Ÿ˜ด", "name": "Sleeping" }, + { "id": "eyes", "glyph": "๐Ÿ‘€", "name": "Looking" }, + { "id": "ok", "glyph": "๐Ÿ‘Œ", "name": "OK" }, + { "id": "pray", "glyph": "๐Ÿ™", "name": "Please" }, + { "id": "strong", "glyph": "๐Ÿ’ช", "name": "Strong" }, + { "id": "coffee", "glyph": "โ˜•", "name": "Coffee" }, + { "id": "rocket", "glyph": "๐Ÿš€", "name": "Rocket" }, + { "id": "bug", "glyph": "๐Ÿ›", "name": "Bug" }, + { "id": "check", "glyph": "โœ…", "name": "Done" }, + { "id": "warning", "glyph": "โš ๏ธ", "name": "Careful" }, + { "id": "wave-bye", "glyph": "๐Ÿซก", "name": "Salute" }, + { "id": "shrug", "glyph": "๐Ÿคท", "name": "Shrug" }, + { "id": "star", "glyph": "โญ", "name": "Star" }, + { "id": "cake", "glyph": "๐ŸŽ‚", "name": "Cake" } + ] +} diff --git a/engine/src/main/kotlin/io/flamenet/e2e/Content.kt b/engine/src/main/kotlin/io/flamenet/e2e/Content.kt index fb0eefa..23b1041 100644 --- a/engine/src/main/kotlin/io/flamenet/e2e/Content.kt +++ b/engine/src/main/kotlin/io/flamenet/e2e/Content.kt @@ -49,8 +49,31 @@ data class Content( /** Set iff this is a reaction: the emoji, or empty to take one back. */ val reaction: String? = null, val call: CallSignal? = null, + /** Set iff this is a sticker: which pack, and which one in it. */ + val sticker: Sticker? = null, ) { + /** + * A sticker: an id, never an image. + * + * ๐Ÿ”ด Sending the picture would cost an upload and a download of bytes both + * ends already ship with, on every sticker. The receiver draws it from its + * own pack, so this is about sixty bytes on the wire. See docs/STICKERS.md. + * + * โš ๏ธ An unknown pack or id must degrade, never break โ€” the renderer falls + * back rather than showing an empty bubble. + */ + data class Sticker(val pack: String, val id: String) { + fun toJson(): JSONObject = JSONObject().put("pack", pack).put("id", id) + + companion object { + fun fromJson(json: JSONObject) = Sticker( + pack = json.optString("pack"), + id = json.optString("id"), + ) + } + } + /** * A pointer at an earlier message. * @@ -107,6 +130,9 @@ data class Content( const val TEXT = "text" const val REACT = "react" + /** A sticker: renderable, but never as text. See docs/STICKERS.md. */ + const val STICKER = "sticker" + /** A contact's delivery key, so we can be reached sealed (ยง14.4). */ const val DELIVERY_KEY = "delivery_key" @@ -191,6 +217,20 @@ data class Content( fun reaction(emoji: String, about: Reference, groupId: String? = null): String = assemble("react", caption = emoji, groupId = groupId, about = about) + /** + * โš ๏ธ Hand-ordered like the rest: the receive side's marker check + * matches the serialized prefix, so `fnc` has to come first. + */ + fun sticker(pack: String, id: String, groupId: String? = null, + messageId: String? = null): String { + val stk = JSONObject().put("pack", pack).put("id", id) + val parts = mutableListOf("\"fnc\":1", "\"kind\":${quote(Kind.STICKER)}") + if (messageId != null) parts += "\"mid\":${quote(messageId)}" + parts += "\"stk\":$stk" + if (groupId != null) parts += "\"gid\":${quote(groupId)}" + return "{" + parts.joinToString(",") + "}" + } + fun callSignal(signal: CallSignal): String { val call = JSONObject().put("id", signal.callId) signal.sdp?.let { call.put("sdp", it) } @@ -252,6 +292,17 @@ data class Content( messageId = messageId, about = about, reaction = caption) } + // A sticker is renderable but is NOT text: it draws as a glyph with + // no bubble, so it has to be recognised here rather than falling + // through to the text path and rendering as an empty message. + if (kind == Kind.STICKER) { + val stk = json.optJSONObject("stk") + if (stk != null) { + return Content("", kind = kind, raw = json, groupId = groupId, + messageId = messageId, sticker = Sticker.fromJson(stk)) + } + } + // Control traffic. Returned with its envelope so the caller can act // on it, and never renderable. //