Skip to content

Fix custom emoji rendering#33

Merged
cbl merged 2 commits into
mainfrom
fix/emojis
Apr 22, 2026
Merged

Fix custom emoji rendering#33
cbl merged 2 commits into
mainfrom
fix/emojis

Conversation

@cbl

@cbl cbl commented Apr 22, 2026

Copy link
Copy Markdown
Member

Root cause

AccountDisplayName.vue (the emoji-aware renderer) wasn't used consistently. Some call sites went through it; most didn't. Its hardcoded styling also made it awkward to drop into places that wanted different typography — so people interpolated manually and the shortcodes leaked.

Approach

Make the wrong thing hard to reach. One pure helper, reused everywhere.

  • utils/customEmojis.ts — new renderCustomEmojis(html, emojis, imgClass?). Pipeline: sanitize → dedupe emojis (first-wins) → substitute → strip unresolved shortcodes. HTML-escapes the alt attribute (fixes a theoretical XSS when a shortcode contains "). Single source of truth.
  • AccountDisplayName.vue refactored to minimal API: name, emojis?, class?. Dropped the asLink/href link-wrapper (caller's concern) and hardcoded font classes (caller owns visual style). Uses the shared helper.
  • RichText.vue — inline loop deleted, calls the same helper. Public API unchanged.
  • Page header pipelineusePageHeader composable and useNavigationStore (web + mobile) both extended with titleEmojis. MobileHeader, DesktopFeedHeader, AppHeader render the title via renderCustomEmojis + v-html. Callers that set an account-based title (@[acct].vue, @[acct]/[id]/index.vue, messages/[id].vue, messages/new.vue, mobile MessageThread) now pass emojis.

Fixed

  • 17 leaky display-name sites swapped to <AccountDisplayName>: ProfileInformation (profile display name), Status.vue x3 (author header, parent-context preview, reblog indicator), StatusQuote, AccountList, ShareStatusForm x2, ChatHeader, MentionList, Take, search.vue, messages/new.vue. Each caller supplies its own class string.
  • Profile field name and value (ProfileInformation.vue) — render via helper with account.emojis. extractText still strips HTML first; emojis are substituted on the plain result.
  • Poll options (PollDisplay.vue) — option.title rendered via helper with option.emojis (masto.js types put emojis per-option).
  • Chat sender name (MessageBubble.vue) — added senderEmojis prop; caller threads it through from the conversation account. Also sharedStatus.authorEmojis for the shared-post preview shape.
  • Chat list group names + sender prefix (ChatListItem.vue) — plain-text context, strips unresolved shortcodes before joining.
  • Broken image fallbacks (separate but surfaced in the same audit): Avatar.vue and ProfileHeader.vue now hide <img> on @error so the muted-circle / gradient fallback shows cleanly instead of a broken-image icon + alt text.

Supporting fixes from the same audit

  • Alt text in the media lightbox switched from always-visible overlay to a compact ALT pill that opens a bottom-sheet dialog (long descriptions no longer cover the image).
  • Status.vue vertical rhythm: pt-3 pb-1 when actions render (balances the button's pb-2 tap target), and pt-1 when a Pinned/reblog indicator is present so the label reads as a caption attached to the post.

Tests

  • New utils/__tests__/customEmojis.test.ts — 9 cases covering resolved substitution, unresolved strip, timestamp-unaffected, dedupe of duplicate shortcodes, alt-attribute HTML escaping, empty cases, ReDoS-style input, custom img class.

Exports

  • renderCustomEmojis and stripUnresolvedEmojiShortcodes re-exported from @repo/ui so app layers can use them for plain-text contexts (nav page titles, aria-announcements, comma-joined group names).

@cbl cbl merged commit 44f6b20 into main Apr 22, 2026
2 checks passed
@cbl cbl deleted the fix/emojis branch April 22, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant