Files
coop/docs/NIP-30.md
T
2026-10-11 16:20:20 +07:00

109 lines
5.9 KiB
Markdown

# NIP-30: Custom Emoji
Plan for adding NIP-30 support. NIP-30 is display-side: events carry
`["emoji", <shortcode>, <image-url>, <emoji-set-address>?]` tags and clients
replace `:shortcode:` occurrences in text with the referenced image.
Shortcodes are `[A-Za-z0-9_-]`. It applies to kind 0 (`name`/`about`), kind 1,
kind 1111, kind 7, and kind 30315 content.
Two existing pieces make this cheap:
- The nostr SDK (locked rev `463f0c2`, v0.45.5) ships
`nostr::nips::nip30::Nip30Tag` with `parse()`/`TryFrom<&Tag>`, shortcode
validation, and the optional emoji-set coordinate. `nip51::Emojis` (kind
10030) and `Kind::EmojiSet` (30030) cover the sending side.
- gpui-base 0.7.1 renders inline markdown images (`![alt](url)`) at
`line_height * 0.75` when no dimensions are given — emoji scale, flowing
with text. Remote image loading already works (`MessageMedia`, markdown
image nodes).
## Current render paths
| Surface | Data | Render |
|---|---|---|
| Messages (NIP-17 DMs, concord, NIP-29) | `chat::Message` built in `crates/chat/src/message.rs::from_parts` (tags → mentions, replies, files) | `chat_ui::text::rendered_text` → `ui::markdown::RenderedText` → `TextView::markdown` |
| Reactions | `ChatPanel::insert_reaction` stores `(content, pubkey)`; `nip29::Group::push_reaction` stores `Reaction { emoji, author }` | `ui::message::group_reactions` → `ReactionBar`, emoji as a plain text child (`crates/ui/src/message.rs`) |
| Profiles | `person::Person` keeps only `Metadata`, no tags | `Person::name()` as plain `SharedString`; person `about` is not rendered anywhere |
Kind coverage in coop: kind 7 (reactions) and kind 1111 (concord comments)
apply directly; kind 1/kind 9 message content goes through the same shared
path. Kind 0 is names only (no `about` surface exists). Kind 30315 is out of
scope — the app has no NIP-38 status feature.
## Phase 1 — Receive and display
1. **Carry emoji tags on messages** (`crates/chat/src/message.rs`): add
`emojis: Vec<Nip30Tag>` to `Message`; parse in `from_parts` alongside
mentions, skip invalid tags, dedupe by shortcode (first wins).
2. **Image replacements in the markdown pipeline**
(`crates/ui/src/markdown.rs`): extend `InlineReplacement` (enum: `Link` as
today, or a sibling `Image { alt, url }`) so `RenderedText::new` can splice
`![shortcode](<url>)` instead of a link. Reuse the existing range
validation; URLs from `nostr::types::Url` are already well-formed, and an
http(s)-only check (like `is_web_url`) is a cheap extra guard.
3. **Image-aware reactions** (`crates/ui/src/message.rs`): add
`image: Option<SharedUri>` to `ReactionGroup`, let `group_reactions` take
an image resolver, and render `img(...)` at caption glyph size in
`ReactionBar` when set, text otherwise.
4. **Emojify message text** (`crates/chat_ui/src/text.rs`): `rendered_text`
gains the event's emojis; scan for `:shortcode:` and emit image
replacements only for shortcodes present in the event's tags (this alone
prevents false positives like `12:30:45`), merged with mention
replacements sorted by range. Update the two `rendered_texts_by_id`
call sites in `ChatPanel::render_row` and `GroupPanel::render_row`.
5. **Wire reactions**: `ChatPanel::insert_reaction` keeps the reaction's
shortcode→url map per target and passes the resolver;
`nip29::Reaction` (`crates/nip29/src/group.rs::push_reaction`) parses the
reaction event's emoji tags; update `crates/nip29_ui/src/message.rs`.
6. **Edits** (concord `KIND_EDIT`): recompute rendered text with the edit
event's own emoji tags wherever content replacement invalidates
`rendered_texts_by_id`.
## Phase 2 — Send custom emoji (optional)
1. **Emoji source**: subscribe to the user's kind 10030 emoji list
(`nip51::Emojis`) and/or kind 30030 sets; resolve pointed sets on demand.
2. **Reactions**: extend `GroupId::reaction` (`crates/nip29/src/protocol.rs`)
and the DM reaction builder (`crates/chat/src/room.rs::rumor`) to append
`emoji` tags; list custom emoji in the React submenus, sending
`:shortcode:` content plus tags. `EMOJI_RE` in `crates/chat_ui/src/lib.rs`
classifies emoji-only replies as reactions and won't match `:foo:` — the
regex or the classification needs updating.
3. **Composer**: offer custom emoji in `render_emoji_menu` (insert
`:shortcode:`); on send, attach matching `emoji` tags for shortcodes found
in the content.
## Phase 3 — Kind 0 profiles (optional)
- Parse emoji tags from the kind 0 event when constructing `Person` and store
the shortcode→url map.
- Emojify `name`/`about` where displayed. `Person::name()` returns a plain
`SharedString` used in dozens of plain-text sites, so start with an
element-returning name variant for `MessageRow`'s author and let plain-text
contexts degrade to literal `:shortcode:`.
## Edge cases
- Only emojify shortcodes that have an emoji tag on the same event (spec
requirement; also the main guard against false positives).
- Invalid shortcodes and malformed tags are skipped (SDK validation).
- Emojification happens pre-markdown, so a matching shortcode inside a code
block becomes an image. Acceptable for v1.
- Reply previews and notifications stay plain text — known limitation.
- Image URLs are attacker-controlled but only load through GPUI's HTTP
resource loader; keep the http(s) restriction as defense in depth.
## Validation
- Unit tests: emoji tag parsing and dedupe in `from_parts`; `RenderedText`
splicing (`![alt](<url>)`, overlap with mentions, unknown shortcodes
untouched); `group_reactions` with images.
- Manual: inject a kind 7 with `:shortcode:` content plus an emoji tag (e.g.
via `nak event`) into a DM and an open group; verify the image renders in
the reaction bar and message body, and that `:unknown:` and `12:34` render
literally.
- `cargo test -p chat -p ui` plus a clippy pass.
Phase 1 is the deliverable: roughly six files, no new dependencies. Phases 2
and 3 are each comparable in size and can ship independently.