feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236
feat(emojis): add built-in StreamEmojiPicker and deprecate the emoji-mart EmojiPicker#3236oliverlaz wants to merge 31 commits into
Conversation
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).
…mart # Conflicts: # yarn.lock
…/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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (16)
✅ Files skipped from review due to trivial changes (8)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughThis 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. ChangesNative Emoji Picker Implementation
Estimated code review effort: 4 (Complex) | ~75 minutes Example Apps and Documentation Migration
Estimated code review effort: 3 (Moderate) | ~30 minutes Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Size Change: +210 kB (+23.9%) 🚨 Total Size: 1.09 MB 📦 View Changed
ℹ️ View Unchanged
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (7)
src/plugins/Emojis/options.ts (1)
113-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent token map value.
emojiButtonRadiusmaps 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 valuePrefix these emoji-picker labels to avoid collisions
These keys live in the sharedtranslationnamespace, so very generic entries likeDefault,Light,Medium, andDarkare easy to collide with future features. A scoped prefix such asEmojiPicker.Lightwould 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
noResultsGlyphignores the selected skin tone.
idlePreviewEmojiis passed as a full emoji object toPreviewPane, which resolves the native glyph viaskins[skinToneIndex] ?? skins[0](seePreviewPane.tsx).noResultsGlyphinstead hardcodesskins[0], so ifoptions.noResultsEmojihas skin-tone variants, the empty-state glyph won't reflect the user's chosenskinToneIndex, 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 valueCryptic 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 valueConfirm eslint-disable is actually needed.
If the repo's ESLint
no-unused-vars/@typescript-eslint/no-unused-varsconfig usesargsIgnorePattern: '^_'(common convention), the_optionsparam 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 valueStatic 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-suppliedEmojiData(not attacker-controlled runtime input), so real exploitation risk is very low. Still, iffilterEmojiDatais ever exposed to accept arbitrary/custom emoji datasets from less-trusted sources (e.g. user-supplied custom emoji configs), a key like__proto__indata.emojiscould set the prototype of the localemojisobject. 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 winMissing test coverage for the "content fits viewport on mount" scroll-spy edge case.
Consider adding a test asserting
onActiveCategoryChangeis not called with the last category id whenatBottomStateChange(true)fires before anyrangeChanged(simulating short lists that never scroll), to guard the behavior flagged inEmojiGrid.tsxLines 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
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (93)
.prettierignoreAI.mdexamples/tutorial/package.jsonexamples/tutorial/src/6-emoji-picker/App.tsxexamples/tutorial/src/6-emoji-picker/layout.cssexamples/tutorial/src/App.tsxexamples/vite/package.jsonexamples/vite/src/App.tsxexamples/vite/src/AppSettings/AppSettings.scssexamples/vite/src/AppSettings/AppSettings.tsxexamples/vite/src/AppSettings/state.tsexamples/vite/src/AppSettings/tabs/EmojiPicker/EmojiPickerTab.tsxexamples/vite/src/AppSettings/tabs/EmojiPicker/index.tspackage.jsonscripts/vendor-emoji-data.mjssrc/components/MessageComposer/MessageComposer.tsxsrc/context/ComponentContext.tsxsrc/i18n/de.jsonsrc/i18n/en.jsonsrc/i18n/es.jsonsrc/i18n/fr.jsonsrc/i18n/hi.jsonsrc/i18n/it.jsonsrc/i18n/ja.jsonsrc/i18n/ko.jsonsrc/i18n/nl.jsonsrc/i18n/pt.jsonsrc/i18n/ru.jsonsrc/i18n/tr.jsonsrc/plugins/Emojis/EmojiPicker.tsxsrc/plugins/Emojis/StreamEmojiPicker.tsxsrc/plugins/Emojis/__tests__/EmojiPicker.test.tsxsrc/plugins/Emojis/__tests__/StreamEmojiPicker.test.tsxsrc/plugins/Emojis/__tests__/entry-exports.test.tssrc/plugins/Emojis/__tests__/memoizeAsyncWithReset.test.tssrc/plugins/Emojis/__tests__/options.test.tssrc/plugins/Emojis/__tests__/reactions.test.tsxsrc/plugins/Emojis/compat.tssrc/plugins/Emojis/components/CategoryNav.tsxsrc/plugins/Emojis/components/EmojiButton.tsxsrc/plugins/Emojis/components/EmojiGrid.tsxsrc/plugins/Emojis/components/EmojiPickerPanel.tsxsrc/plugins/Emojis/components/EmptyResults.tsxsrc/plugins/Emojis/components/PreviewPane.tsxsrc/plugins/Emojis/components/SearchInput.tsxsrc/plugins/Emojis/components/SkinToneSelector.tsxsrc/plugins/Emojis/components/__tests__/EmojiButton.test.tsxsrc/plugins/Emojis/components/__tests__/EmojiGrid.test.tsxsrc/plugins/Emojis/components/__tests__/EmojiPickerPanel.test.tsxsrc/plugins/Emojis/components/__tests__/PreviewPane.test.tsxsrc/plugins/Emojis/components/__tests__/SkinToneSelector.test.tsxsrc/plugins/Emojis/components/__tests__/frequentlyUsed.test.tssrc/plugins/Emojis/components/categories.tssrc/plugins/Emojis/components/frequentlyUsed.tssrc/plugins/Emojis/components/index.tssrc/plugins/Emojis/components/skinTones.tssrc/plugins/Emojis/context/EmojiPickerContext.tsxsrc/plugins/Emojis/data/LICENSEsrc/plugins/Emojis/data/__tests__/emoji-data.test.tssrc/plugins/Emojis/data/__tests__/filterEmojiData.test.tssrc/plugins/Emojis/data/emoji-data.jsonsrc/plugins/Emojis/data/filterEmojiData.tssrc/plugins/Emojis/data/index.tssrc/plugins/Emojis/data/types.tssrc/plugins/Emojis/hooks/__tests__/gridNavigation.test.tssrc/plugins/Emojis/hooks/__tests__/pickerLayout.test.tssrc/plugins/Emojis/hooks/__tests__/useDebouncedValue.test.tssrc/plugins/Emojis/hooks/__tests__/useEmojiPickerState.test.tssrc/plugins/Emojis/hooks/__tests__/useFrequentlyUsedEmoji.test.tssrc/plugins/Emojis/hooks/__tests__/useGridKeyboardNav.test.tsxsrc/plugins/Emojis/hooks/__tests__/useSkinTone.test.tssrc/plugins/Emojis/hooks/gridNavigation.tssrc/plugins/Emojis/hooks/pickerLayout.tssrc/plugins/Emojis/hooks/useDebouncedValue.tssrc/plugins/Emojis/hooks/useEmojiPickerState.tssrc/plugins/Emojis/hooks/useFrequentlyUsedEmoji.tssrc/plugins/Emojis/hooks/useGridKeyboardNav.tssrc/plugins/Emojis/hooks/useSkinTone.tssrc/plugins/Emojis/index.tssrc/plugins/Emojis/memoizeAsyncWithReset.tssrc/plugins/Emojis/middleware/__tests__/textComposerEmojiMiddleware.test.tssrc/plugins/Emojis/middleware/textComposerEmojiMiddleware.tssrc/plugins/Emojis/options.tssrc/plugins/Emojis/reactions.tssrc/plugins/Emojis/search/EmojiSearchIndex.tssrc/plugins/Emojis/search/__tests__/EmojiSearchIndex.test.tssrc/plugins/Emojis/search/__tests__/buildEmojiSearchData.test.tssrc/plugins/Emojis/search/__tests__/search.test.tssrc/plugins/Emojis/search/buildEmojiSearchData.tssrc/plugins/Emojis/search/index.tssrc/plugins/Emojis/search/search.tssrc/plugins/Emojis/styling/EmojiPicker.scsssrc/styling/_emoji-replacement.scss
💤 Files with no reviewable changes (1)
- examples/tutorial/package.json
| export const defaultEmojiSearchIndex: EmojiSearchIndex = { | ||
| search: async (query: string) => { | ||
| if (!query || !query.trim()) return []; | ||
| const results = runSearch(await getIndex(), query); | ||
| return results ? results.map(toResult) : []; | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "EmojiSearchIndex.ts" -o -name "textComposerEmojiMiddleware*" -o -name "reactions.ts" | head -20Repository: GetStream/stream-chat-react
Length of output: 536
🏁 Script executed:
fd -type f "EmojiSearchIndex|textComposerEmojiMiddleware|reactions\.ts" --extension ts --extension tsxRepository: 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 -5Repository: 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 -5Repository: GetStream/stream-chat-react
Length of output: 213
🏁 Script executed:
find . -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "memoizeAsyncWithReset" | head -10Repository: 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.tsRepository: GetStream/stream-chat-react
Length of output: 1896
🏁 Script executed:
cat -n src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts | head -100Repository: GetStream/stream-chat-react
Length of output: 3896
🏁 Script executed:
cat -n src/plugins/Emojis/memoizeAsyncWithReset.tsRepository: GetStream/stream-chat-react
Length of output: 1025
🏁 Script executed:
cat -n src/plugins/Emojis/reactions.ts | head -80Repository: GetStream/stream-chat-react
Length of output: 1676
🏁 Script executed:
cat -n src/plugins/Emojis/middleware/textComposerEmojiMiddleware.tsRepository: GetStream/stream-chat-react
Length of output: 7808
🏁 Script executed:
cat -n src/plugins/Emojis/search/__tests__/EmojiSearchIndex.test.ts | head -80Repository: GetStream/stream-chat-react
Length of output: 1712
🏁 Script executed:
cat -n src/plugins/Emojis/__tests__/memoizeAsyncWithReset.test.tsRepository: GetStream/stream-chat-react
Length of output: 1774
🏁 Script executed:
grep -n "catch\|try\|error\|reject" src/plugins/Emojis/search/__tests__/EmojiSearchIndex.test.tsRepository: 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 -40Repository: GetStream/stream-chat-react
Length of output: 1008
🏁 Script executed:
grep -n "BaseSearchSource\|query\(" src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts | head -20Repository: 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 -3Repository: 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.tsRepository: 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")
PYRepository: 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 -5Repository: GetStream/stream-chat-react
Length of output: 302
🏁 Script executed:
cat -n src/plugins/Emojis/middleware/__tests__/textComposerEmojiMiddleware.test.tsRepository: 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.
| 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.
| 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> | ||
| )} |
There was a problem hiding this comment.
🎯 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.tsxRepository: 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (12)
src/i18n/de.jsonsrc/i18n/en.jsonsrc/i18n/es.jsonsrc/i18n/fr.jsonsrc/i18n/hi.jsonsrc/i18n/it.jsonsrc/i18n/ja.jsonsrc/i18n/ko.jsonsrc/i18n/nl.jsonsrc/i18n/pt.jsonsrc/i18n/ru.jsonsrc/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
| "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", |
There was a problem hiding this comment.
🎯 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.
| "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.
🎯 Goal
The SDK's emoji functionality is built on the unmaintained
emoji-martfamily (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 picker —
StreamEmojiPicker— as the successor, and deprecates the emoji-mart-basedEmojiPickerrather 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):<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).SearchIndexranking verbatim.@emoji-mart/datasnapshot, loaded via a memoized dynamicimport()so the ~340 KB JSON stays a separate async chunk (fetched only when the picker/search run).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):@deprecatedand logs a one-time console warning pointing atStreamEmojiPicker. Its widepickerPropstype is preserved, so existing TypeScript integrations don't break.@emoji-mart/data,@emoji-mart/react,emoji-martreturn 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'sSearchIndexstill works.loadDefaultExtendedReactionOptions()— no emoji-mart install required, code-split, and still opt-in (default reactions are unchanged).mapEmojiMartDatais retained.stream-chat-react(index) imports none of the emoji code — bundle optionality preserved (verified: the core bundle has zero@emoji-martreferences).Examples & docs:
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.mddocuments 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, swapEmojiPicker→StreamEmojiPickerand drop theemoji-mart/@emoji-mart/react/@emoji-mart/datainstalls and anyinit({ data }).Verification:
yarn types+ stricttsconfig.lib.jsonbuild,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
StreamEmojiPickeris 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