fix(desktop): robust emoji picker — unify picker + fix custom emoji in editing, status, reactions#837
Merged
Merged
Conversation
Unify the four drifting emoji-picker instantiations behind a single `<EmojiPicker>` that always wires the workspace custom palette internally and normalizes selection to a `native` glyph or `:shortcode:`. This fixes the headline bug — custom emoji could not be picked when reacting to a *system* message, because `SystemMessageRow`'s picker had no `custom` prop and read `emoji.native` only. Sites swapped: composer, regular-message reactions, system-message reactions, and the status dialog. Also drops the `customEmoji` prop-drilling chain (MessageComposer -> toolbar -> ComposerEmojiPicker) since the picker self-fetches. Status now accepts custom emoji everywhere. A status emoji is a bare string (no companion emojiUrl like reactions), so it was rendered raw and a custom `:shortcode:` would have shown as literal text in five places. Add a single `<StatusEmoji>` that resolves a known shortcode to its workspace image via the localhost media proxy (matching reactions' EmojiGlyph), and render it at all five status spots: sidebar, profile popover, profile panel, user-profile popover, and the dialog trigger. Verified: typecheck, biome lint, vite build, and the custom-emoji e2e specs (4 passing) — including the reaction-via-media-proxy test that exercises the exact shortcode->emojiUrl resolution the system-message picker now shares. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Two distinct bugs when editing a message containing custom emoji:
1. The edit composer showed raw `:shortcode:` instead of the emoji image.
The Tiptap `customEmoji` atom node only materialized via an input rule
(live keystrokes); its markdown `parse` rule was empty, so loading a
message via `setContent` left `:shortcode:` as plain text. Add a
markdown-it inline rule (`registerCustomEmojiMarkdownIt`) wired through the
node's `parse.setup` that emits `<img data-custom-emoji>` for *known*
shortcodes — the same shape `renderHTML` produces — so the node's existing
`parseHTML` materializes it on load. Registered once per markdown-it
instance; matching reads the live shortcode set lazily.
2. Adding a custom emoji while editing saved as a bare `:shortcode:`.
The send path attaches NIP-30 `["emoji", shortcode, url]` tags so the event
is self-contained; the edit path never did, so an edited body shipped with
no emoji tags and the receiver couldn't resolve the shortcode. Thread emoji
tags through the whole edit path:
- composer builds + merges them (parity with send);
- the edit mutation splits the merged set and routes emoji to a dedicated
arg (emoji must NOT ride the imeta-only channel — the Rust `imeta_tags`
guard rejects non-imeta prefixes);
- `editMessage` (tauri) + the `edit_message` command + `build_message_edit`
gain an `emoji_tags` param, validated by the existing `emoji_tags`
guard, mirroring `build_message`;
- `applyEditTagOverlay` now overlays `emoji` tags from the edit too (not
just `imeta`), so both the optimistic cache update AND the receiver
render path (formatTimelineMessages) reflect the edited custom-emoji set.
Verified: 473 JS unit tests (incl. new emoji-overlay cases), typecheck, biome
lint, vite build, the 4 custom-emoji e2e specs, and `cargo check` + events
tests on the Rust side. No edit-flow e2e (the mock bridge has no edit_message
seam today); the tag-overlay logic is unit-covered and the markdown-parse path
is the kind of thing to confirm by eye in the running app.
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…tions The custom-emoji PR changed behavior on three surfaces the e2e suite never exercised: editing a message that contains a custom emoji, adding a custom emoji while editing, and reacting to a system message. All three were unit-covered or spot-checked only. Extend the mock bridge so a real interactive flow can drive them: - handleEditMessage mirrors the real edit_message command (build_message_edit): emit a kind:40003 edit event carrying ["e", target] plus the new content, imeta tags, and NIP-30 emoji tags. The timeline overlays it via applyEditTagOverlay — the same path the relay drives — so recording + emitting the edit event is all the bridge needs. - seed a kind:40099 join event in general (distinct 64-hex id) as a reactable system-message target. It renders as system-message-row, not message-row, so it leaves every existing message-row index assertion untouched. Add three specs to custom-emoji.spec.ts: - Bug 1: editing a message with a custom emoji shows the inline image, not the literal :shortcode: (guards the markdown parse-on-load fix). - Bug 2: adding a custom emoji while editing keeps the image after save (guards the edit-path emoji-tag attachment + overlay). - a system message accepts a custom-emoji reaction. Test-only; no production code changed. 7 custom-emoji e2e pass; full mock-bridge e2e set (messaging, identity-archive, mentions, custom-emoji) green; typecheck + lint + 473 JS unit + file-size guard clean. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…us spacing, shortcode boundaries
Pinky's adversarial review (corroborated independently by Codex) surfaced two
CHANGE-level issues and one boundary nit on the emoji-picker branch. All three
fixed here.
CHANGE 1 — edited legacy/cross-client messages could lose emoji resolution.
applyEditTagOverlay unconditionally dropped the original's NIP-30 emoji tags
whenever any kind:40003 edit existed, then re-added only the edit's. An edit
from an older build (before edits carried emoji tags) or a client that doesn't
know the emoji_tags path has only h/e tags — so an unrelated text edit would
strip the only shortcode->url mapping and re-break a :catjam: that the original
rendered fine. Now: emoji tags are replaced only when the edit actually
supplies some; a tag-less edit PRESERVES the original's emoji tags. This is
strictly safe — an orphaned emoji tag whose shortcode is no longer in the body
resolves nothing, so preserve-on-empty can't cause a stale render. imeta tags
keep their always-replace semantics (the composer re-emits the full set).
Updated the unit test that encoded the buggy "strip on empty" assumption and
added two cases pinning the new asymmetry.
CHANGE 2 — native status emoji lost spacing. StatusEmoji's native-glyph/unknown
fallback returned <span>{value}</span>, dropping the caller's className; every
display site passes spacing classes (mr-1 h-3.5 w-3.5), so native statuses sat
flush against the status text while image statuses had spacing. Thread
className through the fallback span (the image branch already does).
NIT — the edit-composer markdown-it rule matched mid-word/inside URLs:
not:sprout: and http://x:y:sprout: both materialized an inner image. Added a
word-boundary guard (bail when the colon is glued to a preceding [0-9A-Za-z_]),
plus unit cases driving the actual registered rule for boundary, glued-word,
URL, and punctuation-boundary positions.
479 JS unit pass; typecheck + lint clean; custom-emoji + messaging +
identity-archive + mentions e2e green.
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
michaelneale
added a commit
that referenced
this pull request
Jun 4, 2026
* origin/main: (36 commits) fix: use immutable commit-SHA URLs in screenshot PR comments (#842) feat(mobile+desktop): two-tier Slack-style app icon badge (#802) chore: simplify file-size check to a flat 1000-line limit (#839) fix(desktop): robust emoji picker — unify picker + fix custom emoji in editing, status, reactions (#837) feat(desktop): reusable screenshot workflow for agents (#826) desktop(mesh-llm): let a serving node route a different model (#833) chore(release): release version 0.3.9 (#832) fix: native arbitrary-file download + image context-menu flash (#830) fix(desktop): custom emoji reaction rendering + picker autofocus (#831) Mesh-LLM v1: relay-gated direct-iroh inference between users (WAN) (#822) chore(release): release version 0.3.8 (#829) chore(release): release version 0.3.7 (#825) feat: code block rendering, syntax highlighting, and compose fixes (#803) feat: custom emoji — user-owned NIP-30 sets with a client-side union (#816) Install sprout-cli skill at repo root + fix desktop clippy (#818) fix(desktop): use public re-export path for ensure_client_node_for_model (#824) refactor(desktop): feature-gate mesh-llm-sdk behind optional Cargo feature (#823) fix(desktop): align workflow read/save commands to the frontend contract (#820) fix(desktop): disable mesh-llm auto-build to prevent git config corruption (#819) fix(desktop): clear clippy lints in agents/mesh_llm commands (#817) ... # Conflicts: # Cargo.lock # desktop/scripts/check-file-sizes.mjs # desktop/src-tauri/Cargo.toml # desktop/src/app/AppShell.tsx # desktop/src/app/AppTopChrome.tsx # desktop/src/features/messages/hooks.ts # desktop/src/features/workspaces/useWorkspaceInit.ts # desktop/src/shared/api/tauri.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unifies the emoji picker into one component and fixes custom-emoji rendering across status, reactions, and message editing. Originally a single picker-unification change; review and testing surfaced edit-flow bugs that are fixed here too.
What changed
Picker unification — consolidated the multiple emoji-picker call sites (composer, reactions, status dialogs) into one
EmojiPickercomponent, plus custom-emoji rendering in status indicators.Edit-flow fixes (custom emoji)
:shortcode:as text. ThecustomEmojiTiptap node only materialized via the live input rule; loading a message viasetContent(edit-open) left shortcodes as literal text. Added a markdown-it inline rule (wired viaparse.setup) so known shortcodes become image nodes on load. Unknown shortcodes stay text.:shortcode:. The send path attached NIP-30["emoji",…]tags; the edit path didn't. Now the edit-save path builds + routes emoji tags through a dedicatedemoji_tagsarg (editMessage→edit_message→build_message_edit), validated by the existing Rust emoji guard, and overlaid byapplyEditTagOverlayon both the optimistic and receiver render paths.Review fixes (caught by adversarial second pass + Codex)
applyEditTagOverlaynow replaces emoji tags only when the edit supplies some; a tag-less edit (older build or cross-client) preserves the original's, so an unrelated text edit can't re-break a rendered:shortcode:. Strictly safe — an orphaned emoji tag whose shortcode isn't in the body resolves to nothing.StatusEmoji's native/fallback span now threads the caller'sclassName, matching the image branch.not:sprout:,http://x:y:sprout:); boundary cases (:sprout:,:sprout:,(:sprout:)) still do.Testing
custom-emoji.spec.ts) — edit-with-emoji renders the image (Bug 1), add-emoji-on-edit survives save (Bug 2), and system-message reactions; via a newedit_messagemock-bridge seam.🤖 Reviewed adversarially by Pinky (second-agent pass) + Codex.