Skip to content

feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236

Open
oliverlaz wants to merge 31 commits into
masterfrom
feat/replace-emoji-mart
Open

feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236
oliverlaz wants to merge 31 commits into
masterfrom
feat/replace-emoji-mart

Conversation

@oliverlaz

@oliverlaz oliverlaz commented Jul 7, 2026

Copy link
Copy Markdown
Member

🎯 Goal

The SDK's emoji functionality is built on the unmaintained emoji-mart family (emoji-mart, @emoji-mart/react, @emoji-mart/data) — which blocks React 19 without peer-dependency overrides and carries ongoing CJS/ESM interop pain.

This introduces a built-in, dependency-free emoji pickerStreamEmojiPicker — as the successor, and deprecates the emoji-mart-based EmojiPicker rather than removing it. Existing integrations keep working with zero code changes; the deprecated picker and the emoji-mart peer dependencies are scheduled for removal in v15.

Everything here is additive or a soft-deprecation → semver minor. No breaking changes.

🛠 Implementation details

StreamEmojiPicker — the new built-in picker (stream-chat-react/emojis):

  • Native React panel replacing <em-emoji-picker>: category nav + scroll-spy, react-virtuoso grid, in-picker search + empty state, skin tones, frequently-used, preview pane, full keyboard a11y (2D roving focus, Escape, focus return).
  • In-house search index porting emoji-mart's SearchIndex ranking verbatim.
  • Vendored @emoji-mart/data snapshot, loaded via a memoized dynamic import() so the ~340 KB JSON stays a separate async chunk (fetched only when the picker/search run).
  • Curated emoji-mart-compatible pickerProps: navPosition, previewPosition, searchPosition, skinTonePosition, perLine, categories, maxFrequentRows, exceptEmojis, emojiVersion, noCountryFlags, previewEmoji, noResultsEmoji, autoFocus, onClickOutside (+ theme/style). Skin tone & frequently-used are integrator-managed props — the SDK persists nothing.

EmojiPicker — kept, deprecated (removed in v15):

  • Still the emoji-mart engine, unchanged behavior; marked @deprecated and logs a one-time console warning pointing at StreamEmojiPicker. Its wide pickerProps type is preserved, so existing TypeScript integrations don't break.
  • @emoji-mart/data, @emoji-mart/react, emoji-mart return as optional peer dependencies (removed entirely in v15).

Adjacent APIs (backwards compatible):

  • createTextComposerEmojiMiddleware() now defaults to a built-in search index (no argument, no emoji-mart needed); passing emoji-mart's SearchIndex still works.
  • Message reactions are unaffected. The extended "react with any emoji" set loads from the vendored dataset via loadDefaultExtendedReactionOptions() — no emoji-mart install required, code-split, and still opt-in (default reactions are unchanged). mapEmojiMartData is retained.
  • Core stream-chat-react (index) imports none of the emoji code — bundle optionality preserved (verified: the core bundle has zero @emoji-mart references).

Examples & docs:

  • Tutorial and vite examples use StreamEmojiPicker. The vite settings pane gains a picker-engine toggle (Stream ↔ emoji-mart) that drives both the composer picker and a live preview, plus an "Emoji Picker" options playground.
  • AI.md documents the successor, the deprecation, and the migration.

Migration (no-op for existing users): keep using EmojiPicker — it works and warns. To move off emoji-mart, swap EmojiPickerStreamEmojiPicker and drop the emoji-mart / @emoji-mart/react / @emoji-mart/data installs and any init({ data }).

Verification: yarn types + strict tsconfig.lib.json build, yarn lint, full test suite (2705 passing), tutorial + vite example type-checks, and bundle-optionality checks (core entry emoji-free, dataset code-split) all pass.

🎨 UI Changes

StreamEmojiPicker is visually matched to the emoji-mart picker — panel, category tabs with an active underline, search field, preview pane, and skin-tone selector — themed via the SDK's --str-chat__emoji-picker-* tokens (light/dark inherited from the ancestor .str-chat__theme-*). No change to the composer button or popover placement.

The vite example's Emoji Picker settings tab adds a Picker engine toggle (Stream / emoji-mart) at the top; the live preview and the composer's picker both switch engines instantly, and the shared option controls exercise both.

Screenshots to attach: composer picker open (light + dark), search + empty state, skin-tone selector, and the settings-tab engine toggle with its live preview.

Summary by CodeRabbit

  • New Features
    • Added the built-in emoji picker with searchable results, skin-tone selection, frequently used emojis, keyboard navigation, and a live selection preview.
    • Updated demos to use the new picker (including new settings tab + live preview and refreshed picker styling/layout).
    • Expanded emoji picker translations across multiple languages with improved accessibility/ARIA text.
  • Bug Fixes
    • Improved emoji dataset loading with vendored snapshots, retry on failure, and safer emoji picker compatibility/interop with one-time legacy deprecation messaging.
    • Refined text-composer emoji autocomplete defaults and composer integration guidance.
  • Documentation
    • Updated emoji support/migration guides and issue instructions to use the built-in picker approach.
  • Tests
    • Added coverage for picker behavior, search, navigation, state retention, and loader/retry logic.
  • Chores
    • Updated formatting ignore rules for the vendored emoji dataset snapshot.

oliverlaz added 28 commits July 2, 2026 14:30
Vendor a snapshot of @emoji-mart/data (native set 15, sheet stripped) into
src/plugins/Emojis/data and add an in-house search index that replicates
emoji-mart's SearchIndex.search ranking, so ":shortcode" autocomplete and
emoticon replacement work without the emoji-mart packages.

- scripts/vendor-emoji-data.mjs regenerates the vendored JSON + MIT LICENSE
- loadEmojiData() lazily code-splits the dataset (never in the core bundle)
- createTextComposerEmojiMiddleware() now defaults to the built-in index
- deprecated no-op init() shim eases migration off emoji-mart
Replace emoji-mart's <em-emoji-picker> web component with an in-house React
panel that loads the vendored dataset and renders category navigation, a
grouped emoji grid, and a preview pane. The EmojiPicker shell keeps its popover,
toggle button, click-outside and insert behaviour and public props unchanged.

Removes the @emoji-mart/react import and its CJS interop shim; no runtime
emoji-mart imports remain in src. Styling, in-picker search, skin tones,
virtualization and full keyboard a11y follow in subsequent phases.
Rewrite EmojiPicker.scss with class-based, tokenized styles for the React panel
(category nav, grid, emoji cells, preview) using component tokens resolved from
semantic tokens, so light/dark theming comes for free via the inherited
.str-chat__theme-* ancestor. Drop the emoji-mart <em-emoji-picker> rules.

Update _emoji-replacement.scss to apply the Windows flag-font replacement to the
new .str-chat__emoji-picker__emoji / __preview-emoji glyphs instead of the dead
.emoji-mart-emoji-native selector.
Add a SearchInput (icon + labelled input + clear button, mirroring the SDK
SearchBar) and an EmptyResults state. The panel builds an in-memory search index
from the loaded dataset and swaps the grouped category grid for a flat results
grid while a query is active; navigating a category exits search. Styling for the
search box and empty state added to EmojiPicker.scss.
Add a footer skin-tone selector and a synthetic "frequently used" category. Both
are integrator-managed via new optional props — skinTone/defaultSkinTone/
onSkinToneChange and frequentlyUsedEmoji/onFrequentlyUsedChange — with ephemeral
in-memory defaults. The SDK never touches browser storage; integrators own any
persistence (demonstrated in the vite example in a later phase).

useSkinTone and useFrequentlyUsedEmoji implement the controlled-or-uncontrolled
pattern and are unit-tested.
Render the category-grouped grid with react-virtuoso (already a dependency),
virtualizing at the category level so only categories in/near the viewport mount
— keeping picker open fast without giving up section headers, scroll-spy
(rangeChanged) or per-category scrolling (scrollToIndex via an imperative
scrollToCategory handle). Search results remain a small, non-virtualized grid.
- Escape closes the picker and returns focus to the toggle button (onClose)
- the search input receives focus on open; ArrowDown moves into the grid
- 2D roving-tabindex grid navigation (useGridKeyboardNav): Left/Right in reading
  order, Up/Down to the geometrically nearest cell on the adjacent row (robust
  across category headers and virtualization)
- category tabs get roving Left/Right/Home/End navigation
- roles/labels: dialog, tablist/tab, grid/row/gridcell, radiogroup skin tones,
  aria-live preview
Add translations for the picker's search placeholder, clear/skin-tone aria
labels, empty state, the eight category labels + "Frequently used", and the six
skin-tone labels across de/en/es/fr/hi/it/ja/ko/nl/pt/ru/tr. Category and
skin-tone labels are looked up via t() with dynamic keys, so they are declared
directly here rather than via extraction. validate-translations passes.
Emoji support is now fully built into the stream-chat-react/emojis entry point.
Drop @emoji-mart/data, @emoji-mart/react and emoji-mart from peer/optional/dev
dependencies (keeping @emoji-mart/data pinned as a devDependency for the data
vendoring script) and sync the lockfile. Update AI.md and the tutorial + vite
examples (the vite example now demonstrates persisting skin tone and
frequently-used to localStorage via the new props) and refresh the
emojiSearchIndex / pickerProps docs.

Verified via the production build: the index bundle references no emoji picker
code or the emoji-data chunk; the dataset is a separate async chunk loaded only
by the emojis entry.

Migration: emoji-mart, @emoji-mart/react and @emoji-mart/data are no longer
peer dependencies and init() is no longer required. Import EmojiPicker from
stream-chat-react/emojis and register createTextComposerEmojiMiddleware() (no
argument) for autocomplete; passing emoji-mart's SearchIndex as emojiSearchIndex
still works. pickerProps beyond theme/style and emoji-mart CSS variables are no
longer honored; theme: 'auto' now inherits the ancestor .str-chat__theme-* rather
than prefers-color-scheme; the SDK no longer persists skin tone or frequently-used
(use the new props).
…/close

The picker panel is mounted only while open, but it previously owned the
uncontrolled skin-tone and frequently-used state, so closing the picker
(and closeOnEmojiSelect in particular) discarded both and the
frequently-used section never accumulated across openings.

Hoist that state into the always-mounted EmojiPicker shell via
useSkinTone/useFrequentlyUsedEmoji and pass the panel controlled
skinToneIndex / frequentlyUsedIds; the shell records usage on select. Adds a
regression test covering select -> close -> reopen.

Addresses adversarial-review finding: frequently-used and skin-tone state
discarded whenever the picker closes.
pickerProps used a Record<string, unknown> catch-all, so emoji-mart Picker options
(data, set, custom, categories, perLine, emojiVersion, locale, ...) type-checked
but were silently ignored — customers could lose branded emoji or platform
filtering with no signal.

Tighten pickerProps to the supported shape (theme + style) so unsupported options
are now a compile error, and warn at runtime (matching the SDK's other misuse
warnings) when unknown keys reach the picker via `as` casts. Document the supported
surface + migration in AI.md and add a test asserting the type rejects emoji-mart
options and the runtime warning fires.

Addresses adversarial-review finding: legacy pickerProps customizations silently
stop working.

Migration: EmojiPicker pickerProps only accepts theme and style. emoji-mart
Picker options are no longer accepted by the type and are ignored at runtime with a
console warning.
The emoji picker ships its own stylesheet (dist/css/emoji-picker.css), kept
out of index.css on purpose so apps that don't use the picker pay no emoji CSS
cost — the same opt-in model as channel-detail.css. But the tutorial's emoji
step and AI.md's Scenario 6 happy path never imported it, so anyone following
them saw an unstyled picker panel and had to discover the fix in Troubleshooting.

- examples/tutorial/src/6-emoji-picker: import emoji-picker.css in layout.css
  (the vite example already imports it via index.scss).
- AI.md Scenario 6: add the stylesheet import as an explicit step and to the
  code snippet, with a note on why it's separate from index.css.

The stylesheet stays a separate opt-in import rather than being folded into
index.css, preserving the SDK's bundle/CSS optionality.

Addresses adversarial-review finding: emoji picker unusable without the
separate emoji-picker.css.
The SDK's global theme rules define light as the `.str-chat` default and apply
dark via `.str-chat__theme-dark`; there is no variable set bound to a
`.str-chat__theme-light` class. So when the picker was forced to `theme='light'`
inside a `.str-chat__theme-dark` subtree, its panel still inherited the ancestor's
dark tokens and the forced light theme was a no-op. (`theme='dark'` already worked
because the panel's `.str-chat__theme-dark` class matches the global dark rule.)

Re-assert the full light variable set on the forced-light panel
(`.str-chat__emoji-picker.str-chat__theme-light`), mirroring how variable-tokens.scss
already emits `light.variables` for the `.str-chat__theme-dark .str-chat__theme-inverse`
("light inside dark") case. This adds ~4KB gzipped to the opt-in emoji-picker.css,
which only loads with the picker. `theme='auto'` still inherits the ancestor theme.

Export `themeClassName` and add a unit test locking the theme-to-class contract the
CSS override depends on.

Addresses adversarial-review finding: theme='light' does not override a dark chat
ancestor.
…ARIA

Addresses the two medium findings from the adversarial review.

1. Recoverable dataset loading
   loadEmojiData memoized its promise unconditionally, so a single chunk-load
   failure (offline, or a stale chunk after a deploy) was cached forever: the
   picker stayed permanently aria-busy across close/reopen and the shared search
   index used by the composer middleware was poisoned too, with no way back short
   of a page reload.
   - Add memoizeAsyncWithReset: memoize the in-flight/resolved promise but drop it
     on rejection, so the next call retries. Use it for both loadEmojiData and the
     search index's getIndex — the middleware retries on the next keystroke.
   - useEmojiPickerState now exposes { data, error, retry }; the panel renders an
     announced error (role="alert") with a Retry button instead of a stuck spinner.
   - New i18n keys "Failed to load emojis" and "Retry" across all 12 locales.

2. Valid emoji-grid accessibility
   The virtualized category view rendered role="row"/role="gridcell" with no
   owning role="grid" (Virtuoso's root has no grid role), so assistive tech saw
   gridcells with no grid — invalid ARIA. A valid virtualized grid/row/gridcell
   tree with correct rowindex/rowcount metadata isn't practical here, so use plain
   native <button> semantics in labeled category regions instead. Keyboard
   navigation is unaffected: useGridKeyboardNav selects cells by class, and roving
   tabIndex still drives 2D arrow-key movement. Grid roles removed from the search
   view too, for consistent semantics across both views.

Tests: memoizeAsyncWithReset (memoize/concurrent/reset), useEmojiPickerState
(error then retry), EmojiPickerPanel (error-with-retry vs loading), and an EmojiGrid
accessibility regression asserting no orphaned grid/row/gridcell roles in the
non-search view.
Restyle the EmojiPicker panel so the drop-in replacement looks like the
emoji-mart picker it replaced:

- Reorder to category nav on top, search below (emoji-mart layout).
- Search: one filled grey field with the icon inside, smaller input text and a
  compact fixed height (was a bordered box with the icon outside).
- Active category marked with an accent underline instead of a filled background.
- Smaller, medium-weight, secondary-colour section headers.
- Softer search/footer separators (a subtle hairline, not the strong border).
- Larger panel (360x440) with a roomier ~9-per-row grid and real gaps.
- Footer shows a "Pick an emoji…" placeholder (with a pointer glyph) when nothing
  is hovered.

Adds the "Pick an emoji…" i18n key to all 12 locales.
The search field set `padding-inline` on top of the flex `gap` that already
follows the (empty) visually-hidden label — the first flex child — so the search
icon was inset twice (~16px) and looked misaligned with the content column below.
Drop the inline padding; the icon's inset now comes solely from the gap (~8px).

Follow-up to the emoji styling pass.
…d to one row

Polish two details of the built-in emoji picker:

- The footer preview now shows the emoji's `:shortcode:` beneath its name (the
  same token that drives `:` autocomplete), so the preview doubles as a hint.
- The "frequently used" section is capped to a single row: its ids are sliced
  to one row's worth (resolveFrequentlyUsedEmoji) and the frequent grid is
  pinned to a fixed column count, so it no longer wraps to extra rows as more
  emoji are used.

Also adds EmojiButton skin-tone tests documenting that skin tone applies only
to emoji that have skin variants (hands/people) — not the faces shown by
default — which is why changing the tone appears to have no effect there.
…ReactionOptions

The reaction selector's "+" (react with any emoji) was driven by
`reactionOptions.extended`, which the examples used to populate from the full
`@emoji-mart/data` via `mapEmojiMartData`. Removing emoji-mart left `extended`
empty, so only the handful of quick reactions showed — and, because
`useProcessReactions` renders a reaction only when its type is a known option,
arbitrary-emoji reactions could no longer be displayed either.

Add `loadDefaultExtendedReactionOptions()` to the `stream-chat-react/emojis`
entry: it builds the full extended map (every vendored emoji, keyed by unicode)
from the lazily code-split dataset, so importing it costs nothing until called
and the dataset never enters an app's initial bundle. Core stays emoji-free —
the loader lives in the opt-in emojis entry and only depends on the pure
`mapEmojiMartData` from core.

Wire it into the vite example (load + merge into reactionOptions.extended) and
document the pattern in AI.md.
…aries

The emoji grid virtualizes at the category level, but useGridKeyboardNav only
considered the cells currently mounted. At the edge of the mounted window
ArrowRight clamped to the current cell and ArrowDown found nothing, with no way
to scroll an unmounted category in — so keyboard users could not reach the full
emoji set.

Give the hook the ordered categories and a scrollToCategory callback. Within the
mounted window it behaves as before; when a move leaves that window it scrolls
the neighbouring category into view, waits (MutationObserver, with a timeout
fallback) for its cells to mount, then focuses the target — first/last cell for
Left/Right, same-column first/last row for Up/Down. Home/End now reach the
absolute first/last emoji. Crossing is suppressed in the search view, whose flat
results have no category and are all mounted.
- Text-composer middleware: spread options into a fresh object instead of
  mutating the shared DEFAULT_OPTIONS, so options no longer leak between
  createTextComposerEmojiMiddleware() calls — the no-arg default path kept the
  wrong trigger/minChars after any earlier customized call.
- EmojiPicker: record a "frequently used" emoji only after confirming there is a
  textarea to insert into, so a no-op selection isn't tracked as used.
- useFrequentlyUsedEmoji: build each update from a latest-value ref so two
  selections in one tick both survive instead of the second dropping the first.
Replace the DOM-geometry grid navigation (getBoundingClientRect + row-epsilon
tolerance + center-x scoring) with a pure navigateGrid() over the known category
model; only the current column count is read from layout (via offsetTop, once
per keypress). This makes Up/Down navigation unit-testable (previously
impossible in jsdom, so it was uncovered) and removes the duplicated
row-extraction helpers.

Also track the roving cell in a ref so moving focus flips two tabIndex values
instead of sweeping every mounted cell, and reconcile the roving cell in the
render effect only when it has actually detached.
The grid's scroll-spy only used Virtuoso's startIndex, so a short final category
that never reaches the top of the viewport never lit its nav tab. Pin the last
category active while at the bottom (and keep range changes from stealing it
back). The Virtuoso mock previously rendered all items and never fired the
scroll callbacks, so this was untested — the mock now captures them.

Also harden two search tests that passed vacuously: the localeCompare tie-break
fixture now actually ties (equal-length names put the shared keyword at the same
offset), and the AND-semantics assertion checks comma-anchored token matches
rather than a loose substring.
- Debounce the query that drives in-panel search (via a version-safe
  useDebouncedValue hook — useDeferredValue is React 18+), so a broad query no
  longer re-scans the ~1,900-entry index and re-renders up to 90 result cells on
  every keystroke; clearing the field still exits search immediately.
- Memoize buildEmojiSearchData by its data object so the picker panel and the
  composer middleware share a single index build instead of computing it twice.
- Skin-tone selector: expose it as a proper WAI-ARIA radiogroup — focus moves
  onto the checked tone when it opens, a roving tabindex keeps one tone
  tab-reachable, arrow/Home/End keys move the selection (selection follows
  focus), and Escape collapses back to the toggle without closing the picker.
- Mark the emoji toggle button with aria-haspopup="dialog".
Widen EmojiPicker's `pickerProps` (emoji-mart-compatible names) with layout
positions (navPosition, previewPosition, searchPosition, skinTonePosition),
grid/content (perLine, categories, maxFrequentRows), and filtering + polish
(exceptEmojis, emojiVersion, noCountryFlags, previewEmoji, noResultsEmoji,
autoFocus, onClickOutside). All optional and backwards-compatible — defaults
reproduce today's picker, and unsupported emoji-mart options still warn.

Add an "Emoji Picker" playground tab to the vite example — live preview,
reset-to-defaults, and all options wired — and make the settings modal body
scroll so tall tabs no longer clip.
Introduce `StreamEmojiPicker`, the built-in (emoji-mart-free) picker, as the
recommended successor exported from `stream-chat-react/emojis`. The existing
`EmojiPicker` keeps rendering the emoji-mart picker unchanged for backwards
compatibility — now marked `@deprecated` with a one-time console warning. It
and the optional emoji-mart peer dependencies are scheduled for removal in v15.

- `EmojiPicker.tsx` remains the emoji-mart engine (history preserved) and warns
  once, pointing at `StreamEmojiPicker`.
- `StreamEmojiPicker.tsx` is the built-in successor (new file): native React
  panel, in-house search index, vendored dataset, curated `pickerProps`.
- Re-add `@emoji-mart/data`, `@emoji-mart/react`, `emoji-mart` as OPTIONAL peer
  dependencies (removed entirely in v15).
- vite example: the settings pane gains a picker-engine toggle (Stream vs
  emoji-mart) that drives both the composer picker and the live preview; the
  shared option controls exercise both engines.
- Examples and `AI.md` recommend `StreamEmojiPicker` and document the migration.

Everything here is additive or a soft-deprecation — no breaking changes; the
breaking removal lands in v15. Message reactions are unaffected: the extended
set loads from the vendored dataset via `loadDefaultExtendedReactionOptions`
(no emoji-mart install required), code-split and fetched on demand.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7e371a3-2d8e-4e11-b7f7-7da8c22c3bdd

📥 Commits

Reviewing files that changed from the base of the PR and between 9055a79 and 5adf3af.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (16)
  • examples/vite/src/App.tsx
  • package.json
  • src/context/ComponentContext.tsx
  • src/i18n/de.json
  • src/i18n/en.json
  • src/i18n/es.json
  • src/i18n/fr.json
  • src/i18n/hi.json
  • src/i18n/it.json
  • src/i18n/ja.json
  • src/i18n/ko.json
  • src/i18n/nl.json
  • src/i18n/pt.json
  • src/i18n/ru.json
  • src/i18n/tr.json
  • src/plugins/Emojis/__tests__/EmojiPicker.test.tsx
✅ Files skipped from review due to trivial changes (8)
  • src/context/ComponentContext.tsx
  • src/i18n/de.json
  • src/i18n/hi.json
  • src/i18n/ko.json
  • src/i18n/pt.json
  • src/i18n/fr.json
  • src/i18n/es.json
  • src/i18n/ja.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • package.json
  • src/plugins/Emojis/tests/EmojiPicker.test.tsx
  • src/i18n/ru.json
  • src/i18n/it.json
  • src/i18n/en.json
  • examples/vite/src/App.tsx

📝 Walkthrough

Walkthrough

This PR adds the StreamEmojiPicker stack, deprecates the legacy emoji-mart picker, updates emoji search and reaction loading, and migrates the tutorial, Vite example, docs, styling, and locale assets to the new emoji picker flow.

Changes

Native Emoji Picker Implementation

Layer / File(s) Summary
Vendored dataset and types
scripts/vendor-emoji-data.mjs, src/plugins/Emojis/data/*, package.json, .prettierignore
Adds vendored emoji data loading/filtering, dataset types, the vendoring script, and related ignore/version updates.
Search index and reactions loader
src/plugins/Emojis/search/*, src/plugins/Emojis/reactions.ts
Adds emoji search indexing, ranked search, default search consumer, and extended reaction option loading with tests.
Picker state and navigation hooks
src/plugins/Emojis/hooks/*
Adds picker state loading, layout resolution, debounce, skin tone, frequently used, and grid navigation hooks with tests.
Emoji picker presentational components
src/plugins/Emojis/components/*, src/plugins/Emojis/context/EmojiPickerContext.tsx
Adds category metadata, picker context, presentational components, picker panel composition, and component-level tests.
Panel, public picker, and deprecation
src/plugins/Emojis/options.ts, src/plugins/Emojis/StreamEmojiPicker.tsx, src/plugins/Emojis/EmojiPicker.tsx, src/plugins/Emojis/compat.ts, src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts, src/plugins/Emojis/index.ts, src/components/MessageComposer/MessageComposer.tsx, src/context/ComponentContext.tsx
Adds option resolution, the public StreamEmojiPicker, legacy picker deprecation, middleware defaults, and barrel export/comment updates.
Styling and localization
src/plugins/Emojis/styling/EmojiPicker.scss, src/styling/_emoji-replacement.scss, src/i18n/*.json
Rewrites picker styling, updates flag-emoji replacement selectors, and adds emoji picker translation keys across locales.

Estimated code review effort: 4 (Complex) | ~75 minutes

Example Apps and Documentation Migration

Layer / File(s) Summary
AI.md documentation
AI.md
Rewrites the emoji integration guide to use StreamEmojiPicker and updates troubleshooting and package notes.
Tutorial example app migration
examples/tutorial/package.json, examples/tutorial/src/6-emoji-picker/*, examples/tutorial/src/App.tsx
Removes emoji-mart dependencies, swaps in StreamEmojiPicker, imports the picker stylesheet, and updates the tutorial step text.
Vite example app emoji integration
examples/vite/package.json, examples/vite/src/App.tsx
Adds lazy-loaded reaction options, supports both picker engines with persisted picker state, and updates composer/component wiring.
Vite settings tab for emoji picker
examples/vite/src/AppSettings/*
Adds emoji picker settings state, a configuration tab, and supporting styles and wiring for previewing picker options.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Suggested labels: released on @latest``

Suggested reviewers: arnautov-anton

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding StreamEmojiPicker and deprecating the emoji-mart EmojiPicker.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/replace-emoji-mart

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Size Change: +210 kB (+23.9%) 🚨

Total Size: 1.09 MB

📦 View Changed
Filename Size Change
dist/cjs/channel-detail.js 22.9 kB -2 B (-0.01%)
dist/cjs/emoji-data.js 83.2 kB +83.2 kB (new file) 🆕
dist/cjs/emojis.js 16.2 kB +13.6 kB (+531.35%) 🆘
dist/cjs/index.js 295 kB +3.05 kB (+1.04%)
dist/cjs/ReactPlayerWrapper.js 545 B -2 B (-0.37%)
dist/cjs/useChannelHeaderOnlineStatus.js 40.8 kB -194 B (-0.47%)
dist/cjs/useMessageComposerController.js 1.96 kB +947 B (+93.95%) 🆘
dist/cjs/useNotificationApi.js 57.7 kB +242 B (+0.42%)
dist/css/emoji-picker.css 8.84 kB +8.66 kB (+4865.17%) 🆘
dist/css/emoji-replacement.css 467 B +11 B (+2.41%)
dist/es/emoji-data.mjs 83.2 kB +83.2 kB (new file) 🆕
dist/es/emojis.mjs 16.1 kB +13.6 kB (+548.63%) 🆘
dist/es/index.mjs 293 kB +3.13 kB (+1.08%)
dist/es/useChannelHeaderOnlineStatus.mjs 40.3 kB -183 B (-0.45%)
dist/es/useMessageComposerController.mjs 1.87 kB +934 B (+99.89%) 🆘
dist/es/useNotificationApi.mjs 56.4 kB +247 B (+0.44%)
ℹ️ View Unchanged
Filename Size
dist/cjs/audioProcessing.js 1.74 kB
dist/cjs/mp3-encoder.js 814 B
dist/css/channel-detail.css 2.84 kB
dist/css/index.css 41.4 kB
dist/es/audioProcessing.mjs 1.65 kB
dist/es/channel-detail.mjs 22.6 kB
dist/es/mp3-encoder.mjs 768 B
dist/es/ReactPlayerWrapper.mjs 485 B

compressed-size-action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
src/plugins/Emojis/options.ts (1)

113-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent token map value.

emojiButtonRadius maps to a token name plus a parenthetical note (--str-chat__radius-4 (on .str-chat__emoji-picker__emoji)) while the other three entries are pure CSS variable names. Since this map is described as "the CSS token that replaces each," consider keeping just the token name and moving the selector context into the warning message text instead.

♻️ Proposed fix
 const STYLING_KNOB_TOKENS: Record<string, string> = {
   emojiButtonColors: '--str-chat__emoji-picker-hover-background-color',
-  emojiButtonRadius: '--str-chat__radius-4 (on .str-chat__emoji-picker__emoji)',
+  emojiButtonRadius: '--str-chat__radius-4',
   emojiButtonSize: '--str-chat__emoji-picker-emoji-size',
   emojiSize: '--str-chat__emoji-picker-emoji-size',
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/options.ts` around lines 113 - 118, The
STYLING_KNOB_TOKENS map in Emojis/options.ts has an inconsistent value for
emojiButtonRadius because it includes a selector note instead of just the CSS
variable name. Update that entry to match the other token mappings, and move the
“on .str-chat__emoji-picker__emoji” context into the warning text or message
built from the map so the token list stays pure and consistent.
src/i18n/en.json (1)

636-650: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Prefix these emoji-picker labels to avoid collisions
These keys live in the shared translation namespace, so very generic entries like Default, Light, Medium, and Dark are easy to collide with future features. A scoped prefix such as EmojiPicker.Light would make the catalog safer to extend.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/en.json` around lines 636 - 650, The emoji-picker labels in the
shared translation catalog use overly generic keys that can collide with future
entries. Update the relevant keys in en.json to use a scoped prefix for the
emoji picker labels, and keep the existing values unchanged; use the same naming
pattern consistently for the entries referenced by the emoji-picker translations
such as Default, Light, Medium-Light, Medium, Medium-Dark, and Dark.
src/plugins/Emojis/components/EmojiPickerPanel.tsx (2)

217-222: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

noResultsGlyph ignores the selected skin tone.

idlePreviewEmoji is passed as a full emoji object to PreviewPane, which resolves the native glyph via skins[skinToneIndex] ?? skins[0] (see PreviewPane.tsx). noResultsGlyph instead hardcodes skins[0], so if options.noResultsEmoji has skin-tone variants, the empty-state glyph won't reflect the user's chosen skinToneIndex, unlike every other emoji rendered in this panel.

🎨 Respect the active skin tone
-  const noResultsGlyph = options.noResultsEmoji
-    ? (filteredData?.emojis[options.noResultsEmoji]?.skins[0]?.native ?? undefined)
-    : undefined;
+  const noResultsGlyph = options.noResultsEmoji
+    ? (filteredData?.emojis[options.noResultsEmoji]?.skins[skinToneIndex]?.native ??
+       filteredData?.emojis[options.noResultsEmoji]?.skins[0]?.native ??
+       undefined)
+    : undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/components/EmojiPickerPanel.tsx` around lines 217 - 222,
The empty-state emoji lookup in EmojiPickerPanel should respect the active skin
tone instead of always using the default variant. Update the noResultsGlyph
logic to resolve the selected emoji’s native glyph using the current
skinToneIndex, matching the behavior used by PreviewPane and other emoji renders
in this panel. Use the existing EmojiPickerPanel symbols such as noResultsGlyph,
options.noResultsEmoji, filteredData?.emojis, and skinToneIndex to locate and
adjust the lookup.

179-188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cryptic string && chaining for search-gating logic.

trimmed = query.trim() && debouncedQuery.trim() relies on JS truthy/falsy string coercion to gate the debounced value behind the live query being non-empty. It's correct (verified against the "clear exits immediately, typing uses debounced value" comment above it), but non-obvious at a glance and easy to "fix" incorrectly later.

♻️ Clarify with an explicit ternary
-    const trimmed = query.trim() && debouncedQuery.trim();
+    // Only take the debounced value once the live query is non-empty; clearing
+    // the field exits search immediately without waiting for the debounce.
+    const trimmed = query.trim() ? debouncedQuery.trim() : '';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/components/EmojiPickerPanel.tsx` around lines 179 - 188,
The search-gating logic in EmojiPickerPanel’s searchedEmojis uses an opaque &&
chain to select the debounced query only when the live query is non-empty.
Replace that truthy/falsy expression with an explicit ternary or equivalent
clear conditional so the intent is obvious while preserving the same behavior
for clearing search immediately and using debounced input during typing.
src/plugins/Emojis/compat.ts (1)

1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm eslint-disable is actually needed.

If the repo's ESLint no-unused-vars/@typescript-eslint/no-unused-vars config uses argsIgnorePattern: '^_' (common convention), the _options param wouldn't be flagged and the inline disable comment (Line 6) is unnecessary. As per coding guidelines, "Do not suppress ESLint/Prettier rules broadly; justify and scope exceptions when necessary" — worth confirming the suppression is required rather than a leftover from an earlier signature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/compat.ts` around lines 1 - 9, The inline eslint-disable
in the Emotions compat init helper may be unnecessary if `_options` is already
ignored by the repo’s `no-unused-vars`/`@typescript-eslint/no-unused-vars`
settings. Check the `init` function in `src/plugins/Emojis/compat.ts` and remove
the suppression if the leading-underscore convention already covers `_options`;
keep it only if the linter still flags the signature.

Source: Coding guidelines

src/plugins/Emojis/data/filterEmojiData.ts (1)

44-47: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Static analysis flagged possible prototype pollution; likely a false positive here.

The emojis[id] = data.emojis[id] copy loop uses keys sourced from the vendored/developer-supplied EmojiData (not attacker-controlled runtime input), so real exploitation risk is very low. Still, if filterEmojiData is ever exposed to accept arbitrary/custom emoji datasets from less-trusted sources (e.g. user-supplied custom emoji configs), a key like __proto__ in data.emojis could set the prototype of the local emojis object. Worth a defensive guard for robustness against future misuse.

🛡️ Optional defensive guard
   const emojis: EmojiData['emojis'] = {};
   for (const id of Object.keys(data.emojis)) {
+    if (id === '__proto__' || id === 'constructor' || id === 'prototype') continue;
     if (keep(data.emojis[id])) emojis[id] = data.emojis[id];
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/data/filterEmojiData.ts` around lines 44 - 47, Add a
defensive guard in filterEmojiData when copying entries from data.emojis into
the local emojis object. The current Object.keys loop in filterEmojiData can
still assign special keys like __proto__ if custom datasets are ever passed in,
so skip unsafe keys before the emojis[id] assignment. Keep the change localized
to filterEmojiData and preserve the existing keep(...) filtering behavior.

Source: Linters/SAST tools

src/plugins/Emojis/components/__tests__/EmojiGrid.test.tsx (1)

92-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test coverage for the "content fits viewport on mount" scroll-spy edge case.

Consider adding a test asserting onActiveCategoryChange is not called with the last category id when atBottomStateChange(true) fires before any rangeChanged (simulating short lists that never scroll), to guard the behavior flagged in EmojiGrid.tsx Lines 62-85.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/components/__tests__/EmojiGrid.test.tsx` around lines 92 -
133, Add test coverage for the scroll-spy case where the content already fits
the viewport on mount and `atBottomStateChange(true)` fires before any
`rangeChanged` in `EmojiGrid.test.tsx`. Extend the existing `EmojiGrid
scroll-spy (active category tracking)` suite to render the `EmojiGrid` via
`renderSpy`, trigger `virtuoso.atBottomStateChange?.(true)` first, and assert
`onActiveCategoryChange` is not called with the last category id until a real
`rangeChanged` occurs. Use the existing `renderSpy`, `virtuoso`, and
`onActiveCategoryChange` symbols to keep the test aligned with the `EmojiGrid`
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/vite/src/AppSettings/AppSettings.scss`:
- Around line 1052-1055: There is a stylelint violation in the AppSettings.scss
modal body styles because a double-slash comment follows declarations without a
required खाली line before it. Update the block around the modal content styles
so the comment about tall tabs appears with a blank line before it, keeping the
surrounding declarations intact and preserving the existing intent in
AppSettings.scss.

In `@src/plugins/Emojis/search/EmojiSearchIndex.ts`:
- Around line 31-37: The defaultEmojiSearchIndex.search flow currently lets
getIndex() rejection escape, which can crash the emoji composer path instead of
failing closed. Update search() in EmojiSearchIndex to wrap the await getIndex()
and runSearch(...) call in a try/catch, and return an empty array on any load or
search error so transient dataset-load failures do not propagate through the
composer middleware.

In `@src/plugins/Emojis/StreamEmojiPicker.tsx`:
- Around line 154-186: The StreamEmojiPicker open flow keeps focus on the toggle
button when search is disabled or autoFocus is false, so keyboard users skip the
mounted picker contents. Update the focus handling in StreamEmojiPicker (and the
EmojiPickerPanel/picker open path it uses) to move initial focus into the picker
grid/panel when searchPosition is none or autoFocus is false, while preserving
the existing button focus behavior for other modes.

In `@src/plugins/Emojis/styling/EmojiPicker.scss`:
- Around line 29-33: Insert a blank line before each `//` comment in
`EmojiPicker.scss` so the SCSS `double-slash-comment-empty-line-before` rule is
satisfied; update the comment blocks around the emoji picker
separator/search/footer styling and the other flagged sections in the same file,
using the nearby declaration groups as anchors.
- Around line 141-143: The SCSS in EmojiPicker.scss has declarations immediately
following `@include` mixins without the required blank line, triggering
declaration-empty-line-before stylelint failures. Update each affected block in
the EmojiPicker styling so any property declarations after mixin calls such as
utils.button-reset are separated by a blank line, and apply the same formatting
in all repeated cases within the category/nav button-related selectors.
- Around line 374-389: The block comment in EmojiPicker.scss contains empty `//`
separator lines that trigger scss/comment-no-empty. Remove the lone `//` lines
and keep the explanation as a continuous multi-line comment near the
`.str-chat__emoji-picker.str-chat__theme-light` rule so the stylelint check
passes.

---

Nitpick comments:
In `@src/i18n/en.json`:
- Around line 636-650: The emoji-picker labels in the shared translation catalog
use overly generic keys that can collide with future entries. Update the
relevant keys in en.json to use a scoped prefix for the emoji picker labels, and
keep the existing values unchanged; use the same naming pattern consistently for
the entries referenced by the emoji-picker translations such as Default, Light,
Medium-Light, Medium, Medium-Dark, and Dark.

In `@src/plugins/Emojis/compat.ts`:
- Around line 1-9: The inline eslint-disable in the Emotions compat init helper
may be unnecessary if `_options` is already ignored by the repo’s
`no-unused-vars`/`@typescript-eslint/no-unused-vars` settings. Check the `init`
function in `src/plugins/Emojis/compat.ts` and remove the suppression if the
leading-underscore convention already covers `_options`; keep it only if the
linter still flags the signature.

In `@src/plugins/Emojis/components/__tests__/EmojiGrid.test.tsx`:
- Around line 92-133: Add test coverage for the scroll-spy case where the
content already fits the viewport on mount and `atBottomStateChange(true)` fires
before any `rangeChanged` in `EmojiGrid.test.tsx`. Extend the existing
`EmojiGrid scroll-spy (active category tracking)` suite to render the
`EmojiGrid` via `renderSpy`, trigger `virtuoso.atBottomStateChange?.(true)`
first, and assert `onActiveCategoryChange` is not called with the last category
id until a real `rangeChanged` occurs. Use the existing `renderSpy`, `virtuoso`,
and `onActiveCategoryChange` symbols to keep the test aligned with the
`EmojiGrid` behavior.

In `@src/plugins/Emojis/components/EmojiPickerPanel.tsx`:
- Around line 217-222: The empty-state emoji lookup in EmojiPickerPanel should
respect the active skin tone instead of always using the default variant. Update
the noResultsGlyph logic to resolve the selected emoji’s native glyph using the
current skinToneIndex, matching the behavior used by PreviewPane and other emoji
renders in this panel. Use the existing EmojiPickerPanel symbols such as
noResultsGlyph, options.noResultsEmoji, filteredData?.emojis, and skinToneIndex
to locate and adjust the lookup.
- Around line 179-188: The search-gating logic in EmojiPickerPanel’s
searchedEmojis uses an opaque && chain to select the debounced query only when
the live query is non-empty. Replace that truthy/falsy expression with an
explicit ternary or equivalent clear conditional so the intent is obvious while
preserving the same behavior for clearing search immediately and using debounced
input during typing.

In `@src/plugins/Emojis/data/filterEmojiData.ts`:
- Around line 44-47: Add a defensive guard in filterEmojiData when copying
entries from data.emojis into the local emojis object. The current Object.keys
loop in filterEmojiData can still assign special keys like __proto__ if custom
datasets are ever passed in, so skip unsafe keys before the emojis[id]
assignment. Keep the change localized to filterEmojiData and preserve the
existing keep(...) filtering behavior.

In `@src/plugins/Emojis/options.ts`:
- Around line 113-118: The STYLING_KNOB_TOKENS map in Emojis/options.ts has an
inconsistent value for emojiButtonRadius because it includes a selector note
instead of just the CSS variable name. Update that entry to match the other
token mappings, and move the “on .str-chat__emoji-picker__emoji” context into
the warning text or message built from the map so the token list stays pure and
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecb6548a-8ec3-4eb5-b03e-f041aa5ae7e9

📥 Commits

Reviewing files that changed from the base of the PR and between c89366b and bf7575b.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (93)
  • .prettierignore
  • AI.md
  • examples/tutorial/package.json
  • examples/tutorial/src/6-emoji-picker/App.tsx
  • examples/tutorial/src/6-emoji-picker/layout.css
  • examples/tutorial/src/App.tsx
  • examples/vite/package.json
  • examples/vite/src/App.tsx
  • examples/vite/src/AppSettings/AppSettings.scss
  • examples/vite/src/AppSettings/AppSettings.tsx
  • examples/vite/src/AppSettings/state.ts
  • examples/vite/src/AppSettings/tabs/EmojiPicker/EmojiPickerTab.tsx
  • examples/vite/src/AppSettings/tabs/EmojiPicker/index.ts
  • package.json
  • scripts/vendor-emoji-data.mjs
  • src/components/MessageComposer/MessageComposer.tsx
  • src/context/ComponentContext.tsx
  • src/i18n/de.json
  • src/i18n/en.json
  • src/i18n/es.json
  • src/i18n/fr.json
  • src/i18n/hi.json
  • src/i18n/it.json
  • src/i18n/ja.json
  • src/i18n/ko.json
  • src/i18n/nl.json
  • src/i18n/pt.json
  • src/i18n/ru.json
  • src/i18n/tr.json
  • src/plugins/Emojis/EmojiPicker.tsx
  • src/plugins/Emojis/StreamEmojiPicker.tsx
  • src/plugins/Emojis/__tests__/EmojiPicker.test.tsx
  • src/plugins/Emojis/__tests__/StreamEmojiPicker.test.tsx
  • src/plugins/Emojis/__tests__/entry-exports.test.ts
  • src/plugins/Emojis/__tests__/memoizeAsyncWithReset.test.ts
  • src/plugins/Emojis/__tests__/options.test.ts
  • src/plugins/Emojis/__tests__/reactions.test.tsx
  • src/plugins/Emojis/compat.ts
  • src/plugins/Emojis/components/CategoryNav.tsx
  • src/plugins/Emojis/components/EmojiButton.tsx
  • src/plugins/Emojis/components/EmojiGrid.tsx
  • src/plugins/Emojis/components/EmojiPickerPanel.tsx
  • src/plugins/Emojis/components/EmptyResults.tsx
  • src/plugins/Emojis/components/PreviewPane.tsx
  • src/plugins/Emojis/components/SearchInput.tsx
  • src/plugins/Emojis/components/SkinToneSelector.tsx
  • src/plugins/Emojis/components/__tests__/EmojiButton.test.tsx
  • src/plugins/Emojis/components/__tests__/EmojiGrid.test.tsx
  • src/plugins/Emojis/components/__tests__/EmojiPickerPanel.test.tsx
  • src/plugins/Emojis/components/__tests__/PreviewPane.test.tsx
  • src/plugins/Emojis/components/__tests__/SkinToneSelector.test.tsx
  • src/plugins/Emojis/components/__tests__/frequentlyUsed.test.ts
  • src/plugins/Emojis/components/categories.ts
  • src/plugins/Emojis/components/frequentlyUsed.ts
  • src/plugins/Emojis/components/index.ts
  • src/plugins/Emojis/components/skinTones.ts
  • src/plugins/Emojis/context/EmojiPickerContext.tsx
  • src/plugins/Emojis/data/LICENSE
  • src/plugins/Emojis/data/__tests__/emoji-data.test.ts
  • src/plugins/Emojis/data/__tests__/filterEmojiData.test.ts
  • src/plugins/Emojis/data/emoji-data.json
  • src/plugins/Emojis/data/filterEmojiData.ts
  • src/plugins/Emojis/data/index.ts
  • src/plugins/Emojis/data/types.ts
  • src/plugins/Emojis/hooks/__tests__/gridNavigation.test.ts
  • src/plugins/Emojis/hooks/__tests__/pickerLayout.test.ts
  • src/plugins/Emojis/hooks/__tests__/useDebouncedValue.test.ts
  • src/plugins/Emojis/hooks/__tests__/useEmojiPickerState.test.ts
  • src/plugins/Emojis/hooks/__tests__/useFrequentlyUsedEmoji.test.ts
  • src/plugins/Emojis/hooks/__tests__/useGridKeyboardNav.test.tsx
  • src/plugins/Emojis/hooks/__tests__/useSkinTone.test.ts
  • src/plugins/Emojis/hooks/gridNavigation.ts
  • src/plugins/Emojis/hooks/pickerLayout.ts
  • src/plugins/Emojis/hooks/useDebouncedValue.ts
  • src/plugins/Emojis/hooks/useEmojiPickerState.ts
  • src/plugins/Emojis/hooks/useFrequentlyUsedEmoji.ts
  • src/plugins/Emojis/hooks/useGridKeyboardNav.ts
  • src/plugins/Emojis/hooks/useSkinTone.ts
  • src/plugins/Emojis/index.ts
  • src/plugins/Emojis/memoizeAsyncWithReset.ts
  • src/plugins/Emojis/middleware/__tests__/textComposerEmojiMiddleware.test.ts
  • src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts
  • src/plugins/Emojis/options.ts
  • src/plugins/Emojis/reactions.ts
  • src/plugins/Emojis/search/EmojiSearchIndex.ts
  • src/plugins/Emojis/search/__tests__/EmojiSearchIndex.test.ts
  • src/plugins/Emojis/search/__tests__/buildEmojiSearchData.test.ts
  • src/plugins/Emojis/search/__tests__/search.test.ts
  • src/plugins/Emojis/search/buildEmojiSearchData.ts
  • src/plugins/Emojis/search/index.ts
  • src/plugins/Emojis/search/search.ts
  • src/plugins/Emojis/styling/EmojiPicker.scss
  • src/styling/_emoji-replacement.scss
💤 Files with no reviewable changes (1)
  • examples/tutorial/package.json

Comment thread examples/vite/src/AppSettings/AppSettings.scss
Comment on lines +31 to +37
export const defaultEmojiSearchIndex: EmojiSearchIndex = {
search: async (query: string) => {
if (!query || !query.trim()) return [];
const results = runSearch(await getIndex(), query);
return results ? results.map(toResult) : [];
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "EmojiSearchIndex.ts" -o -name "textComposerEmojiMiddleware*" -o -name "reactions.ts" | head -20

Repository: GetStream/stream-chat-react

Length of output: 536


🏁 Script executed:

fd -type f "EmojiSearchIndex|textComposerEmojiMiddleware|reactions\.ts" --extension ts --extension tsx

Repository: GetStream/stream-chat-react

Length of output: 336


🏁 Script executed:

git ls-files | grep -E "(EmojiSearchIndex|textComposerEmojiMiddleware|reactions\.ts)"

Repository: GetStream/stream-chat-react

Length of output: 495


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "defaultEmojiSearchIndex" | head -5

Repository: GetStream/stream-chat-react

Length of output: 427


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "getIndex" | grep -i emoji | head -5

Repository: GetStream/stream-chat-react

Length of output: 213


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "memoizeAsyncWithReset" | head -10

Repository: GetStream/stream-chat-react

Length of output: 389


🏁 Script executed:

ls -la src/plugins/Emojis/search/ 2>/dev/null || echo "Directory not found"

Repository: GetStream/stream-chat-react

Length of output: 602


🏁 Script executed:

cat -n src/plugins/Emojis/search/EmojiSearchIndex.ts

Repository: GetStream/stream-chat-react

Length of output: 1896


🏁 Script executed:

cat -n src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts | head -100

Repository: GetStream/stream-chat-react

Length of output: 3896


🏁 Script executed:

cat -n src/plugins/Emojis/memoizeAsyncWithReset.ts

Repository: GetStream/stream-chat-react

Length of output: 1025


🏁 Script executed:

cat -n src/plugins/Emojis/reactions.ts | head -80

Repository: GetStream/stream-chat-react

Length of output: 1676


🏁 Script executed:

cat -n src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts

Repository: GetStream/stream-chat-react

Length of output: 7808


🏁 Script executed:

cat -n src/plugins/Emojis/search/__tests__/EmojiSearchIndex.test.ts | head -80

Repository: GetStream/stream-chat-react

Length of output: 1712


🏁 Script executed:

cat -n src/plugins/Emojis/__tests__/memoizeAsyncWithReset.test.ts

Repository: GetStream/stream-chat-react

Length of output: 1774


🏁 Script executed:

grep -n "catch\|try\|error\|reject" src/plugins/Emojis/search/__tests__/EmojiSearchIndex.test.ts

Repository: GetStream/stream-chat-react

Length of output: 165


🏁 Script executed:

grep -A 10 -B 2 "await.*search\|await.*query" src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts | head -40

Repository: GetStream/stream-chat-react

Length of output: 1008


🏁 Script executed:

grep -n "BaseSearchSource\|query\(" src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts | head -20

Repository: GetStream/stream-chat-react

Length of output: 189


🏁 Script executed:

# Check if the base class has error handling
find . -type f -name "*.d.ts" | xargs grep -l "BaseSearchSource" 2>/dev/null | head -3

Repository: GetStream/stream-chat-react

Length of output: 416


🏁 Script executed:

# Look at how query is being called in the middleware handlers
grep -B 5 -A 15 "getEntityString: async" src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts

Repository: GetStream/stream-chat-react

Length of output: 820


🏁 Script executed:

python3 - <<'PY'
# Verify the behavior of await with ?? operator
# This demonstrates that ?? does NOT catch promise rejections

code = """
// The ?? operator does NOT catch promise rejections
const result = (await somePromiseThatRejects()) ?? [];

// This is what happens:
// 1. await somePromiseThatRejects() throws/rejects
// 2. The rejection propagates (?? never executes)
// 3. If no try/catch above, the rejection is unhandled

// Compare to null coalescing which only handles falsy values
const result2 = null ?? []; // Works fine, returns []
const result3 = undefined ?? []; // Works fine, returns []
// But no try/catch can help a promise rejection
"""

print("The ?? operator behavior:")
print("- Handles null and undefined values")
print("- DOES NOT catch promise rejections")
print("- Promise rejections bypass ?? and propagate to caller")
PY

Repository: GetStream/stream-chat-react

Length of output: 318


🏁 Script executed:

# Check if there are any integration tests that verify error scenarios
find src -name "*.test.ts" -o -name "*.test.tsx" | xargs grep -l "defaultEmojiSearchIndex\|textComposerEmojiMiddleware" | head -5

Repository: GetStream/stream-chat-react

Length of output: 302


🏁 Script executed:

cat -n src/plugins/Emojis/middleware/__tests__/textComposerEmojiMiddleware.test.ts

Repository: GetStream/stream-chat-react

Length of output: 4444


Unhandled promise rejection from dataset load will crash the composer's emoji flow.

getIndex() is memoized with memoizeAsyncWithReset, which catches failures, resets the memo for retry, and rethrows the rejection (see lines 12–14 of memoizeAsyncWithReset.ts). The search() function does not catch this rejection, so it propagates to the middleware's query() method (line 40 of textComposerEmojiMiddleware.ts), which also lacks a try/catch. The ?? [] operator only suppresses null/undefined, not promise rejections—a transient chunk-load error would throw out of the composer's text input flow instead of gracefully returning empty results. This contradicts the stated intent in memoizeAsyncWithReset ("must not permanently disable the picker") and is inconsistent with how loadDefaultExtendedReactionOptions in reactions.ts documents that integrators must handle failures themselves; defaultEmojiSearchIndex is auto-wired, so it should fail closed internally.

Add a try/catch in search() to return [] on any index-load or search error:

Proposed fix
 export const defaultEmojiSearchIndex: EmojiSearchIndex = {
   search: async (query: string) => {
     if (!query || !query.trim()) return [];
-    const results = runSearch(await getIndex(), query);
-    return results ? results.map(toResult) : [];
+    try {
+      const results = runSearch(await getIndex(), query);
+      return results ? results.map(toResult) : [];
+    } catch {
+      return [];
+    }
   },
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const defaultEmojiSearchIndex: EmojiSearchIndex = {
search: async (query: string) => {
if (!query || !query.trim()) return [];
const results = runSearch(await getIndex(), query);
return results ? results.map(toResult) : [];
},
};
export const defaultEmojiSearchIndex: EmojiSearchIndex = {
search: async (query: string) => {
if (!query || !query.trim()) return [];
try {
const results = runSearch(await getIndex(), query);
return results ? results.map(toResult) : [];
} catch {
return [];
}
},
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/search/EmojiSearchIndex.ts` around lines 31 - 37, The
defaultEmojiSearchIndex.search flow currently lets getIndex() rejection escape,
which can crash the emoji composer path instead of failing closed. Update
search() in EmojiSearchIndex to wrap the await getIndex() and runSearch(...)
call in a try/catch, and return an empty array on any load or search error so
transient dataset-load failures do not propagate through the composer
middleware.

Comment on lines +154 to +186
return (
<div className={props.wrapperClassName ?? wrapperClassName}>
{displayPicker && (
<div
className={props.pickerContainerClassName ?? pickerContainerClassName}
ref={setPopperElement}
style={{ left: x ?? 0, position: strategy, top: y ?? 0 }}
>
<EmojiPickerPanel
frequentlyUsedIds={frequentlyUsedIds}
onClose={() => {
setDisplayPicker(false);
referenceElement?.focus();
}}
onEmojiSelect={(emoji) => {
const textarea = textareaRef.current;
if (!textarea) return;
// Record only once we know the emoji is actually being inserted.
recordUse(emoji.id);
textComposer.insertText({ text: emoji.native });
textarea.focus();
if (props.closeOnEmojiSelect) {
setDisplayPicker(false);
}
}}
onSkinToneChange={setSkinTone}
options={options}
skinToneIndex={skinTone}
style={pickerStyle}
theme={props.pickerProps?.theme}
/>
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether EmojiPickerPanel moves focus into itself on open (autoFocus / focus() call)
rg -n "autoFocus|\.focus\(\)" src/plugins/Emojis/components/EmojiPickerPanel.tsx src/plugins/Emojis/components/SearchInput.tsx

Repository: GetStream/stream-chat-react

Length of output: 560


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== EmojiPickerPanel outline =="
ast-grep outline src/plugins/Emojis/components/EmojiPickerPanel.tsx --view expanded

echo
echo "== SearchInput outline =="
ast-grep outline src/plugins/Emojis/components/SearchInput.tsx --view expanded

echo
echo "== Relevant snippets =="
sed -n '1,280p' src/plugins/Emojis/components/EmojiPickerPanel.tsx | cat -n | sed -n '1,280p'
echo
sed -n '1,120p' src/plugins/Emojis/components/SearchInput.tsx | cat -n | sed -n '1,120p'
echo
sed -n '1,260p' src/plugins/Emojis/StreamEmojiPicker.tsx | cat -n | sed -n '1,260p'

Repository: GetStream/stream-chat-react

Length of output: 25384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== picker options and defaults =="
rg -n "autoFocus|searchPosition|resolvePickerLayout|DEFAULT_EMOJI_PICKER_OPTIONS|pickerLayout" src/plugins/Emojis -g '!**/*.snap'

echo
echo "== files with picker layout/options =="
fd -a "options.ts|pickerLayout.ts|useGridKeyboardNav.ts" src/plugins/Emojis

echo
echo "== inspect options =="
sed -n '1,260p' src/plugins/Emojis/options.ts | cat -n | sed -n '1,260p'

echo
echo "== inspect picker layout helper =="
sed -n '1,260p' src/plugins/Emojis/hooks/pickerLayout.ts | cat -n | sed -n '1,260p'

echo
echo "== inspect grid keyboard nav =="
sed -n '1,260p' src/plugins/Emojis/hooks/useGridKeyboardNav.ts | cat -n | sed -n '1,260p'

Repository: GetStream/stream-chat-react

Length of output: 22895


Move initial focus into the picker when search is disabled. When searchPosition: 'none' or autoFocus: false, opening leaves focus on the toggle button, so Tab skips the picker contents because the panel is mounted before the button in DOM. Focus the panel/grid on open in those modes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugins/Emojis/StreamEmojiPicker.tsx` around lines 154 - 186, The
StreamEmojiPicker open flow keeps focus on the toggle button when search is
disabled or autoFocus is false, so keyboard users skip the mounted picker
contents. Update the focus handling in StreamEmojiPicker (and the
EmojiPickerPanel/picker open path it uses) to move initial focus into the picker
grid/panel when searchPosition is none or autoFocus is false, while preserving
the existing button focus behavior for other modes.

Comment thread src/plugins/Emojis/styling/EmojiPicker.scss
Comment thread src/plugins/Emojis/styling/EmojiPicker.scss
Comment thread src/plugins/Emojis/styling/EmojiPicker.scss
Comment thread src/i18n/de.json Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/i18n/en.json`:
- Around line 554-561: The `"Suggest an option"` entry in the English i18n map
has a capitalization mismatch between the key and value. Update the value in
`src/i18n/en.json` so it matches the key text exactly, consistent with the other
identity mappings in this section and the corresponding translations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75c58d8b-941e-4f08-b06c-38ed42a87f92

📥 Commits

Reviewing files that changed from the base of the PR and between 5ddc4bd and 9055a79.

📒 Files selected for processing (12)
  • src/i18n/de.json
  • src/i18n/en.json
  • src/i18n/es.json
  • src/i18n/fr.json
  • src/i18n/hi.json
  • src/i18n/it.json
  • src/i18n/ja.json
  • src/i18n/ko.json
  • src/i18n/nl.json
  • src/i18n/pt.json
  • src/i18n/ru.json
  • src/i18n/tr.json
✅ Files skipped from review due to trivial changes (7)
  • src/i18n/ko.json
  • src/i18n/nl.json
  • src/i18n/hi.json
  • src/i18n/fr.json
  • src/i18n/it.json
  • src/i18n/ru.json
  • src/i18n/ja.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/i18n/pt.json
  • src/i18n/tr.json

Comment thread src/i18n/en.json
Comment on lines +554 to +561
"Smileys & People": "Smileys & People",
"Some of the files will not be accepted": "Some of the files will not be accepted",
"Start typing to search": "Start typing to search",
"Stop sharing": "Stop sharing",
"Submit": "Submit",
"Suggest a new option to add to this poll": "Suggest a new option to add to this poll",
"Suggest an option": "Suggest an Option",
"Symbols": "Symbols",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix capitalization mismatch in "Suggest an option" value.

Every other new English entry here uses an identity mapping (key text === value text, e.g. "Activities": "Activities"), but this one capitalizes "Option" in the value while the key and the es/de translations use lowercase "option".

🐛 Proposed fix
-  "Suggest an option": "Suggest an Option",
+  "Suggest an option": "Suggest an option",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Smileys & People": "Smileys & People",
"Some of the files will not be accepted": "Some of the files will not be accepted",
"Start typing to search": "Start typing to search",
"Stop sharing": "Stop sharing",
"Submit": "Submit",
"Suggest a new option to add to this poll": "Suggest a new option to add to this poll",
"Suggest an option": "Suggest an Option",
"Symbols": "Symbols",
"Smileys & People": "Smileys & People",
"Some of the files will not be accepted": "Some of the files will not be accepted",
"Start typing to search": "Start typing to search",
"Stop sharing": "Stop sharing",
"Submit": "Submit",
"Suggest a new option to add to this poll": "Suggest a new option to add to this poll",
"Suggest an option": "Suggest an option",
"Symbols": "Symbols",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/en.json` around lines 554 - 561, The `"Suggest an option"` entry in
the English i18n map has a capitalization mismatch between the key and value.
Update the value in `src/i18n/en.json` so it matches the key text exactly,
consistent with the other identity mappings in this section and the
corresponding translations.

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.

2 participants