Join Nostr
2026-10-28 19:06:18 UTC
in reply to

doctor on Nostr: ## A design from cloud fodder (2026-10-28), and what it lands on in the code > new ...

## A design from (2026-10-28), and what it lands on in the code

> new settings, in appearance, you can add your custom emoji packs, what that will do is, it will download
> those as images into your local 'cache', then, when you send one of those emojis, it'll be.. encrypted
> blossom style to the room.

Three changes to what the issue above assumes, and they are improvements — recording them as the shape to build,
not a second issue.

**1. Packs are added by hand in Settings › Appearance, not read off a profile.** `AppearanceScreen.kt:114` is the
screen; a pack is a kind 30030 `naddr`/URL a person pastes, fetched once, its `["emoji", name, url]` rows kept.
Part 1 of the issue above (fetch the account's kind 10030 and follow its `a` tags) becomes optional sugar rather
than the entry point — worth keeping in mind that 's ask was the packs *already linked to a profile in
another client*, so the two want to meet eventually.
Where the list lives is a real choice: `AppPrefs` (prefs.json) is **this device only**, like `blossomServers`
(`AppPrefs.kt:46`) and the quick reaction row (`:79`), so packs added on the phone would not reach the desktop.
An account-wide stamped document (as chat folders are) or publishing a kind 10030 of our own makes them travel.

**2. The images are downloaded at add time into a local cache.** This settles the privacy decision the issue
leaves open, and settles it the good way: the pack's host sees one fetch, once, when you add it — not a fetch per
reader per chat open, and not a reaction chip silently calling a third party while someone reads a message. It
also needs the thing we have never had: `#2c4debef` — there is **no disk cache for images at all**; `LinkImages`
is 24 MB in memory, process-wide. So this feature brings the app's first persistent image store, and `#b69118a2`
applies to it (Coil's existing on-disk cache is plaintext and survives sign-out; a pack cache should go through
`BlobCipher` and be dropped with the account).

**3. Sending one re-encrypts it to the room, Blossom style.** The mechanism exists whole: `MediaService.prepare`
(cordn-sync `MediaService.kt:65`) encrypts under the group's epoch key and uploads with server fallback,
`MediaCipher` is `cordn-em-v1` (ChaCha20-Poly1305, nonce in `imeta n`, key = MLS exporter of the carrying
message's epoch — `AccountRuntime.kt:503`), and an 80-byte PNG is nowhere near `MAX_FILE_BYTES` (8388592).
Consequences to decide before coding:

- **The key is per group and per epoch.** The same emoji sent in four rooms is four blobs, and after a commit
moves the epoch it is a new one again, so there has to be a small "this emoji, this group, this epoch →
url/nonce/hash" memo or every reaction pays an upload. (The receiver's side is already right: the media key is
persisted on the row at ingest, so opening needs no key exchange.)
- **A bubble cannot draw an inline image today, and a row carries one attachment.** `messageText` (ui
`util/MessageText.kt:21`) tokenises mentions, event refs and URLs only — there is no image token — and
`Imeta.find` (`Imeta.kt:56`) takes the **first** `imeta` tag as *the row's* media. Two pack emoji in one
sentence therefore need a tag of their own (one per emoji, carrying name + url + nonce + hash + mime) and a new
token in the text pass; reusing `imeta` would make the message look like an attachment.
- **Pick the tag name so an older client ignores it.** cordn-web has no custom-emoji support at all (nothing in
its source touches 10030/30030 or an `emoji` tag), so whatever we add, a member on another client sees the
content string — `:monero:` — which is the right degradation. An `imeta` would instead make them draw an image
attachment where we meant an emoji.
- **Reactions still need both ends.** `References.reactionTags` (`References.kt:36`) writes `e`/`p`/`k` only, and
`References.reaction` (`:79-85`) keeps `content` and **drops the tags**, so the chip's URL is thrown away before
the UI sees it. `AccountSession.react` (`AccountSession.kt:773`) passes the emoji as content.
- **The picker hands back a character.** `EmojiPickerSheet`'s `onPick: (String) -> Unit` is one whole emoji
(`EmojiPicker.kt:66`), and all five callers store one: the quick row (`List<String>` in prefs), a group icon.
A pack entry is a name plus an image, so `onPick` needs a type, and a caller that cannot take an image (a group
icon is in the group's document, read by cordn-web too) should not offer the pack sections.

## Corrections to the Verify section above

Step 4 as written ("receive a `:monero:` reaction and message **sent from Ditto or Vector**") is not reachable:
those clients are not members of an MLS room. The pack *comes* from them; the messages come from Staircase or
cordn-web. Replace it with:

4. A member on cordn-web receives your pack emoji: they see `:monero:` as text, nothing broken, no image
attachment card.
5. Add a pack on the phone and check what the desktop shows (whichever answer #1 above is decided, the step says
so up front).
6. Send the same pack emoji in two different rooms, and again after someone joins or leaves one of them (epoch
change): it draws both times, and the log shows one upload per room-epoch, not one per send.
7. Turn the network off and send a pack emoji you have used before in that room: it goes out of the cache, with
no fetch to the pack's host.
8. Sign out: the cached pack images are gone from disk (`#b69118a2`).

Steps 1, 2, 3, 5 (unknown shortcode), 6 (unservable image) and 7 (quick slot) of the body still stand.