An emote picker that offers everything the account can send #205

Closed
opened 2026-09-12 11:03:51 -04:00 by Salastil · 3 comments
Owner

The emote picker offers what the conversation on screen has. That is the wrong
question on every service moho speaks, because on all of them an account can use
emotes from places it is not currently looking at.

  • Discord: with Nitro, every emoji from every guild you are in, anywhere.
  • Kick: a channel you are subscribed to lets you use its emotes across the site,
    not only in that channel.

So the picker should offer everything the account can actually send, not everything
the current room happens to own.

Agreed design (2026-09-12)

One long scrollable list of everything available, with a row of icons along the top
that jump to a section - the shape Kick's own picker uses.

Sections, in order:

  1. Frequently used - what this account actually reaches for.
  2. Per source, one section each with its own icon: a Discord guild by its icon, a Kick
    channel by its avatar, Sneedchat's smilies, 7TV global.
  3. Unicode emoji last, by their existing groups.

Each entry knows whether it can be sent here: a guild emoji is sendable in that
guild for everyone, and anywhere for Nitro; a Kick subscriber emote is sendable
site-wide while subscribed. Ones that cannot be sent in the current conversation are
still shown - the picker is also how somebody finds out what subscribing or Nitro
would buy - but marked, the way emotes.rs already marks a locked Kick emote.

Why this is not just a UI change

listBufferEmoji answers for one buffer, and Kick's arm gates on that channel's
subscribed flag. The question becomes "what can this account send", which is an
account-level answer assembled from every guild and every subscribed channel, so the
daemon side changes as much as the window does.

Related: #195 added 7TV emotes, which are per channel and word-keyed rather than
id-keyed - they belong in this list too, and are the case that proves entries need to
carry where they came from rather than just a name and a picture.

The emote picker offers what the conversation on screen has. That is the wrong question on every service moho speaks, because on all of them an account can use emotes from places it is not currently looking at. - **Discord:** with Nitro, every emoji from every guild you are in, anywhere. - **Kick:** a channel you are subscribed to lets you use its emotes across the site, not only in that channel. So the picker should offer everything the account can actually send, not everything the current room happens to own. ## Agreed design (2026-09-12) One long scrollable list of everything available, with a row of icons along the top that jump to a section - the shape Kick's own picker uses. Sections, in order: 1. **Frequently used** - what this account actually reaches for. 2. Per source, one section each with its own icon: a Discord guild by its icon, a Kick channel by its avatar, Sneedchat's smilies, 7TV global. 3. Unicode emoji last, by their existing groups. Each entry knows whether it can be *sent here*: a guild emoji is sendable in that guild for everyone, and anywhere for Nitro; a Kick subscriber emote is sendable site-wide while subscribed. Ones that cannot be sent in the current conversation are still shown - the picker is also how somebody finds out what subscribing or Nitro would buy - but marked, the way `emotes.rs` already marks a locked Kick emote. ## Why this is not just a UI change `listBufferEmoji` answers for one buffer, and Kick's arm gates on that channel's `subscribed` flag. The question becomes "what can this account send", which is an account-level answer assembled from every guild and every subscribed channel, so the daemon side changes as much as the window does. Related: #195 added 7TV emotes, which are per channel and word-keyed rather than id-keyed - they belong in this list too, and are the case that proves entries need to carry where they came from rather than just a name and a picture.
Salastil added the feature label 2026-09-12 11:03:51 -04:00
Author
Owner

Daemon half done — https://git.salastil.com/Salastil/nobilis/commit/c74ff95. The window half (the picker itself) is next.

listAllEmoji({bufferId}) answers with every source this account can reach, each
carrying usableHere. Shape per source:

id, name, service, iconUrl?, buffers[], sendableAnywhere, usableHere,
emoji: [{ id, name, url?, animated, locked }]

id on an entry is what goes in the message, already in the service's own form —
Kick's [emote:id:name], a Discord id, a Sneedchat shortcode — so the picker inserts
it without knowing per-service rules.

Sources registered so far: Discord guilds (with icon), Kick channels (with avatar),
Sneedchat's table. Not yet: 7TV, which #195 stores per buffer as word → url and which
should become a source too.

Both subscriptions are read rather than assumed:

  • Discord Nitro from premium_type in READY, the only place the payload carries
    it. Classic, Nitro and Basic all buy cross-guild use; 0 or absent is none.
  • Kick per-channel standing, already fetched to decide which emotes the picker
    offers. A subscription sets sendableAnywhere, so those emotes are offered in every
    Kick buffer rather than only that channel's.

locked entries come back present rather than filtered, the way Kick's list already
worked — the picker is how somebody learns what subscribing would buy.

Remaining for the window: the scrollable list, the jump strip keyed on iconUrl,
search across all sources, and marking !usableHere / locked entries as
look-don't-send. EmojiPicker.tsx is 406 lines today and is a rewrite rather than an
edit.

Daemon half done — https://git.salastil.com/Salastil/nobilis/commit/c74ff95. The window half (the picker itself) is next. `listAllEmoji({bufferId})` answers with every source this account can reach, each carrying `usableHere`. Shape per source: ``` id, name, service, iconUrl?, buffers[], sendableAnywhere, usableHere, emoji: [{ id, name, url?, animated, locked }] ``` `id` on an entry is **what goes in the message**, already in the service's own form — Kick's `[emote:id:name]`, a Discord id, a Sneedchat shortcode — so the picker inserts it without knowing per-service rules. Sources registered so far: Discord guilds (with icon), Kick channels (with avatar), Sneedchat's table. Not yet: 7TV, which #195 stores per buffer as word → url and which should become a source too. Both subscriptions are read rather than assumed: - **Discord Nitro** from `premium_type` in READY, the only place the payload carries it. Classic, Nitro and Basic all buy cross-guild use; 0 or absent is none. - **Kick per-channel standing**, already fetched to decide which emotes the picker offers. A subscription sets `sendableAnywhere`, so those emotes are offered in every Kick buffer rather than only that channel's. `locked` entries come back present rather than filtered, the way Kick's list already worked — the picker is how somebody learns what subscribing would buy. Remaining for the window: the scrollable list, the jump strip keyed on `iconUrl`, search across all sources, and marking `!usableHere` / `locked` entries as look-don't-send. `EmojiPicker.tsx` is 406 lines today and is a rewrite rather than an edit.
Author
Owner

Finished — https://git.salastil.com/Salastil/nobilis/commit/c74ff95, https://git.salastil.com/Salastil/nobilis/commit/197cfd3, https://git.salastil.com/Salastil/nobilis/commit/2a0e2a9, https://git.salastil.com/Salastil/moho/commit/f35839c

The picker asks the daemon what this account can send and where, and draws a section
per source with a strip of icons above to move between them.

Sources: Discord guilds (icon), Kick channels (avatar), Kick 7TV sets, Sneedchat's
table, Matrix emoticon packs. Unicode last.

Both subscriptions are read, not assumed. Discord premium_type from READY —
Classic, Nitro and Basic all buy cross-guild use. Kick per-channel standing, already
fetched to decide what the picker offers, now sets sendableAnywhere so a subscribed
channel's emotes appear in every Kick buffer.

Nothing in the window knows a service by name. A backend calls set_emoji_source
and the thing appears with its own section and jump icon. The contract is kind:
"text" writes the entry's id into the composer, "event" sends it on its own (a
Matrix sticker). Each backend puts id in the form its own service reads, so the
picker inserts it without per-service rules. Adding a service is adding a source.

Two things found while finishing:

  • Matrix was invisible because is_sticker threw emoticons away. One im.ponies
    pack carries both and they are different things — a sticker is an event, an emoticon
    is an image in a line of text. The pack now feeds both pickers.
  • Matrix emoticons would have sent as literal :name:. They were registered with
    nothing to convert them, so inline_emoticons now does it on the way out, into the
    formatted body only — the plain body keeps :shortcode:, which is exactly the
    fallback a client with no images should show. Matched with its colons, unlike 7TV
    where the bare word is the emote.

Sources that reach the conversation sort first; the rest are shown but disabled, since
that is how somebody learns what a subscription would buy. The two refusals are told
apart: locked is "not paid for", not-usable-here is "paid for, wrong room".

Layout measured rather than eyeballed: the jump strip scrolls sideways rather than
wrapping — 17 tabs stay on one 36px row and the 320×360 picker does not grow or
overflow. An account in thirty guilds would otherwise push the emoji off the bottom.

564 tests green. Deployed.

Finished — https://git.salastil.com/Salastil/nobilis/commit/c74ff95, https://git.salastil.com/Salastil/nobilis/commit/197cfd3, https://git.salastil.com/Salastil/nobilis/commit/2a0e2a9, https://git.salastil.com/Salastil/moho/commit/f35839c The picker asks the daemon what this account can send and where, and draws a section per source with a strip of icons above to move between them. **Sources:** Discord guilds (icon), Kick channels (avatar), Kick 7TV sets, Sneedchat's table, Matrix emoticon packs. Unicode last. **Both subscriptions are read, not assumed.** Discord `premium_type` from READY — Classic, Nitro and Basic all buy cross-guild use. Kick per-channel standing, already fetched to decide what the picker offers, now sets `sendableAnywhere` so a subscribed channel's emotes appear in every Kick buffer. **Nothing in the window knows a service by name.** A backend calls `set_emoji_source` and the thing appears with its own section and jump icon. The contract is `kind`: `"text"` writes the entry's `id` into the composer, `"event"` sends it on its own (a Matrix sticker). Each backend puts `id` in the form its own service reads, so the picker inserts it without per-service rules. Adding a service is adding a source. Two things found while finishing: - **Matrix was invisible** because `is_sticker` threw emoticons away. One `im.ponies` pack carries both and they are different things — a sticker is an event, an emoticon is an image in a line of text. The pack now feeds both pickers. - **Matrix emoticons would have sent as literal `:name:`.** They were registered with nothing to convert them, so `inline_emoticons` now does it on the way out, into the formatted body only — the plain body keeps `:shortcode:`, which is exactly the fallback a client with no images should show. Matched with its colons, unlike 7TV where the bare word is the emote. Sources that reach the conversation sort first; the rest are shown but disabled, since that is how somebody learns what a subscription would buy. The two refusals are told apart: `locked` is "not paid for", not-usable-here is "paid for, wrong room". Layout measured rather than eyeballed: the jump strip scrolls sideways rather than wrapping — 17 tabs stay on one 36px row and the 320×360 picker does not grow or overflow. An account in thirty guilds would otherwise push the emoji off the bottom. 564 tests green. Deployed.
Author
Owner

Two follow-ups since closing.

7TV is no longer a source (https://git.salastil.com/Salastil/nobilis/commit/7e15621) — see #195. It does not render in Kick's own
chat, so offering it meant sending words most of a channel reads as words.

Somewhere else's emoji are no longer shown (https://git.salastil.com/Salastil/moho/commit/b7ebb31, https://git.salastil.com/Salastil/nobilis/commit/0a0ee0f). A source that
belongs to another room and cannot be sent from here is now dropped rather than greyed:
it is not an offer, it is a list of things that would not work, and on an account in
thirty guilds that list is the whole picker. A locked entry inside a source that does
belong here still shows greyed — a subscriber emote in the channel you are reading is a
real offer, and greying it is how somebody learns what subscribing would buy.

That change uncovered a bug that would have made the rule impossible: buffers on
Discord emoji sources was left empty, with a comment promising it would be filled in
"below" that never was. Empty means "anywhere", so usableHere was always true and
every guild's emoji read as usable in every conversation — Nitro or not. Now registered
at the end of the channel walk, where the buffer ids are actually known.

Two follow-ups since closing. **7TV is no longer a source** (https://git.salastil.com/Salastil/nobilis/commit/7e15621) — see #195. It does not render in Kick's own chat, so offering it meant sending words most of a channel reads as words. **Somewhere else's emoji are no longer shown** (https://git.salastil.com/Salastil/moho/commit/b7ebb31, https://git.salastil.com/Salastil/nobilis/commit/0a0ee0f). A source that belongs to another room and cannot be sent from here is now dropped rather than greyed: it is not an offer, it is a list of things that would not work, and on an account in thirty guilds that list is the whole picker. A locked entry inside a source that *does* belong here still shows greyed — a subscriber emote in the channel you are reading is a real offer, and greying it is how somebody learns what subscribing would buy. That change uncovered a bug that would have made the rule impossible: `buffers` on Discord emoji sources was left empty, with a comment promising it would be filled in "below" that never was. Empty means "anywhere", so `usableHere` was always true and every guild's emoji read as usable in every conversation — Nitro or not. Now registered at the end of the channel walk, where the buffer ids are actually known.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#205