Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f5ece32
feat(emojis): add built-in emoji dataset and search index
oliverlaz Jul 2, 2026
27eab23
feat(emojis): render a native React emoji picker panel
oliverlaz Jul 2, 2026
7d672bc
feat(emojis): style the native emoji picker panel
oliverlaz Jul 2, 2026
9362f63
feat(emojis): add in-picker search and empty state
oliverlaz Jul 2, 2026
9a30e5b
feat(emojis): add skin-tone selector and frequently-used section
oliverlaz Jul 2, 2026
3fd1f0d
perf(emojis): virtualize the emoji grid by category
oliverlaz Jul 2, 2026
61f0ac9
feat(emojis): keyboard navigation and accessibility for the picker
oliverlaz Jul 2, 2026
1ae3205
feat(emojis): add emoji picker i18n strings for all 12 locales
oliverlaz Jul 2, 2026
334f6fb
feat(emojis): remove emoji-mart peer dependencies
oliverlaz Jul 2, 2026
2236157
Merge remote-tracking branch 'origin/master' into feat/replace-emoji-…
oliverlaz Jul 2, 2026
e61c8c5
fix(emojis): persist skin tone and frequently-used across picker open…
oliverlaz Jul 2, 2026
ae560f8
fix(emojis): stop silently ignoring unsupported EmojiPicker pickerProps
oliverlaz Jul 2, 2026
d856bd8
docs(emojis): import emoji-picker.css where the picker is used
oliverlaz Jul 3, 2026
05dba1a
fix(emojis): make EmojiPicker theme='light' override a dark ancestor
oliverlaz Jul 3, 2026
ce7ea1a
fix(emojis): recover from failed dataset loads and drop invalid grid …
oliverlaz Jul 3, 2026
ce86bdc
fix(emojis): match the built-in picker's styling to emoji-mart
oliverlaz Jul 3, 2026
37a14c5
Merge remote-tracking branch 'origin/master' into feat/replace-emoji-…
oliverlaz Jul 3, 2026
a20dd46
fix(emojis): remove doubled search-field padding so the icon aligns
oliverlaz Jul 3, 2026
737a03a
feat(emojis): show emoji shortcode in preview and keep frequently-use…
oliverlaz Jul 3, 2026
044e97a
feat(emojis): restore reacting with any emoji via loadDefaultExtended…
oliverlaz Jul 3, 2026
79b2a23
fix(emojis): let keyboard navigation cross virtualized category bound…
oliverlaz Jul 3, 2026
c375e7d
fix(emojis): correct three picker option/state bugs
oliverlaz Jul 3, 2026
97b285a
perf(emojis): drive picker keyboard navigation from the category model
oliverlaz Jul 3, 2026
e539909
fix(emojis): mark the last category active when scrolled to the bottom
oliverlaz Jul 3, 2026
98ba7ff
perf(emojis): debounce picker search and share one search index
oliverlaz Jul 3, 2026
8376d53
fix(emojis): improve picker keyboard accessibility
oliverlaz Jul 3, 2026
0e4025b
feat(emojis): support curated emoji-mart pickerProps options
oliverlaz Jul 6, 2026
bf7575b
feat(emojis): add StreamEmojiPicker and deprecate emoji-mart EmojiPicker
oliverlaz Jul 7, 2026
5ddc4bd
fix(emojis): keep the vite example emoji preview pinned to the right
oliverlaz Jul 7, 2026
9055a79
chore: sync i18n strings
oliverlaz Jul 7, 2026
5adf3af
Merge origin/master into feat/replace-emoji-mart (resolve i18n confli…
oliverlaz Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ examples/**/serviceWorker.ts
.agents/
.claude/skills/
skills-lock.json

# Vendored emoji dataset (snapshot of @emoji-mart/data β€” kept minified; regenerate
# via `node scripts/vendor-emoji-data.mjs`)
src/plugins/Emojis/data/emoji-data.json
103 changes: 83 additions & 20 deletions AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,26 +241,89 @@ const CustomMessage = () => {

**User intent**: "Add emoji picker and autocomplete"

Emoji support is built into the SDK β€” the **`StreamEmojiPicker`** needs no `emoji-mart`
packages or `init()` call.

> **Deprecation:** the `EmojiPicker` export from `stream-chat-react/emojis` is the legacy
> **emoji-mart-backed** picker. It still works unchanged for backwards compatibility, but
> is **deprecated and will be removed in v15** and logs a one-time console warning. New
> integrations should use `StreamEmojiPicker`; existing emoji-mart integrations keep
> working until you migrate (see _Migrating from emoji-mart_ below).

**Steps**:

1. Install emoji packages: `npm install emoji-mart @emoji-mart/react @emoji-mart/data`
2. Initialize emoji data: `init({ data })` from `emoji-mart`
3. Import `EmojiPicker` from `stream-chat-react/emojis`
4. Pass `EmojiPicker` and `emojiSearchIndex={SearchIndex}` to `Channel`
1. Import `StreamEmojiPicker` from `stream-chat-react/emojis` and pass it to `Channel`.
2. Import the emoji picker's stylesheet: `import 'stream-chat-react/css/emoji-picker.css'`.
It is intentionally **not** part of `index.css` (so apps that don't use the picker
ship no emoji CSS); without it the picker panel renders unstyled.
3. (Optional) For `:shortcode` autocomplete + emoticon replacement, register the
emoji middleware on the message composer's text composer with
`createTextComposerEmojiMiddleware()` (no argument β€” it uses the built-in index).

```tsx
import { EmojiPicker } from 'stream-chat-react/emojis';
import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data';
import { StreamEmojiPicker } from 'stream-chat-react/emojis';
import 'stream-chat-react/css/emoji-picker.css';

init({ data });

<Channel EmojiPicker={EmojiPicker} emojiSearchIndex={SearchIndex}>
{/* ... */}
</Channel>;
<Channel EmojiPicker={StreamEmojiPicker}>{/* ... */}</Channel>;
```

**Note**: For React 19, may need package.json overrides for `@emoji-mart/react`
**Notes**:

- Passing a custom `emojiSearchIndex` (including emoji-mart's `SearchIndex`) is
still supported for advanced use.
- On `StreamEmojiPicker`, skin tone and "frequently used" are integrator-managed props
(`skinTone`/`onSkinToneChange`, `frequentlyUsedEmoji`/`onFrequentlyUsedChange`);
the SDK does not persist them. See `examples/vite/` for a localStorage example.
- On `StreamEmojiPicker`, `pickerProps` accepts a curated set of emoji-mart-compatible `Picker` options (plus
`theme` and `style`):
- **Layout**: `navPosition` / `previewPosition` (`'top' | 'bottom' | 'none'`),
`searchPosition` (`'sticky' | 'static' | 'none'`), `skinTonePosition`
(`'preview' | 'search' | 'none'`).
- **Grid & content**: `perLine` (default `9`), `categories` (filter + reorder;
`'frequent'` always prepends), `maxFrequentRows` (default `1`).
- **Filtering & polish**: `exceptEmojis`, `emojiVersion`, `noCountryFlags`,
`previewEmoji`, `noResultsEmoji`, `autoFocus`, `onClickOutside`.

Divergences from emoji-mart's defaults: `autoFocus` defaults to `true`; `emojiVersion`
is unfiltered by default (the bundled set 15); `previewEmoji` / `noResultsEmoji` default
to the SDK's placeholder / empty state; `noCountryFlags` is opt-in (no Windows
auto-detect); `categories` cannot reposition `'frequent'`.

Not supported (rejected by the type; ignored with a console warning at runtime): image
sets (`set`, `getSpritesheetURL`), `custom` emoji, `data`, `i18n` / `locale`,
`dynamicWidth`, `icons`, `categoryIcons`. Sizing knobs (`emojiSize`, `emojiButtonSize`,
…) are `--str-chat__emoji-picker-*` CSS variables, not props. Skin tone uses the
first-class `skinTone` / `defaultSkinTone` props (not emoji-mart's `skin`). Try these
live in the "Emoji Picker" settings tab of `examples/vite/`.

- To let users **react with any emoji** (the reaction selector's `+` button), fill
`reactionOptions.extended` with the full emoji set. It also gates display β€” a reaction
whose type isn't in `quick`/`extended` is not rendered. Load it lazily from the emojis
entry (the dataset is code-split, so this adds nothing to your initial bundle):

```tsx
import { defaultReactionOptions, type ReactionOptions } from 'stream-chat-react';
import { loadDefaultExtendedReactionOptions } from 'stream-chat-react/emojis';

const [reactionOptions, setReactionOptions] =
useState<ReactionOptions>(defaultReactionOptions);
useEffect(() => {
loadDefaultExtendedReactionOptions().then((extended) =>
setReactionOptions({ ...defaultReactionOptions, extended }),
);
}, []);

<Channel reactionOptions={reactionOptions}>{/* ... */}</Channel>;
```

For a curated subset instead, build the `extended` map yourself with `mapEmojiMartData`.

- **Migrating from emoji-mart** (the deprecated `EmojiPicker`): swap `EmojiPicker` β†’
`StreamEmojiPicker`, then remove the `emoji-mart` / `@emoji-mart/react` /
`@emoji-mart/data` installs and any `init({ data })` call. The deprecated `EmojiPicker`
still accepts raw emoji-mart `pickerProps` (e.g. `set`, `emojiSize`); `StreamEmojiPicker`
uses the curated `pickerProps` above. Autocomplete (`createTextComposerEmojiMiddleware`)
and reactions (`mapEmojiMartData`) are engine-agnostic and need no changes.

**Reference**: See `examples/tutorial/src/6-emoji-picker/`

Expand Down Expand Up @@ -365,9 +428,11 @@ body {

**Solution**:

- Ensure emoji packages are installed
- Initialize with `init({ data })` before rendering
- For React 19, add package.json overrides if needed
- Ensure `EmojiPicker` from `stream-chat-react/emojis` is passed to `Channel` (or set
via `ComponentContext`)
- Import the emoji picker CSS: `import 'stream-chat-react/css/emoji-picker.css'`
- For `:shortcode` autocomplete, register `createTextComposerEmojiMiddleware()` on
the composer's text composer

## Resources

Expand All @@ -386,10 +451,8 @@ body {
- `react`: ^19.0.0 || ^18.0.0 || ^17.0.0
- `react-dom`: ^19.0.0 || ^18.0.0 || ^17.0.0
- `stream-chat`: ^9.27.2
- **Optional Dependencies** (for emoji support):
- `emoji-mart`: ^5.4.0
- `@emoji-mart/react`: ^1.1.0
- `@emoji-mart/data`: ^1.1.0
- **Emoji support**: built in via the `stream-chat-react/emojis` entry point β€” no
`emoji-mart` packages required.

## Best Practices

Expand Down
2 changes: 0 additions & 2 deletions examples/tutorial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
"hoistingLimits": "workspaces"
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"emoji-mart": "^5.6.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "^9.49.0",
Expand Down
11 changes: 3 additions & 8 deletions examples/tutorial/src/6-emoji-picker/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import {
Window,
WithComponents,
} from 'stream-chat-react';
import { EmojiPicker } from 'stream-chat-react/emojis';

import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data';
import { StreamEmojiPicker } from 'stream-chat-react/emojis';

import './layout.css';
import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials';
Expand All @@ -32,8 +29,6 @@ const filters: ChannelFilters = {
members: { $in: [userId] },
};

init({ data });

const App = () => {
const [isReady, setIsReady] = useState(false);
const client = useCreateChatClient({
Expand Down Expand Up @@ -66,13 +61,13 @@ const App = () => {

return (
<Chat client={client}>
<WithComponents overrides={{ EmojiPicker }}>
<WithComponents overrides={{ EmojiPicker: StreamEmojiPicker }}>
<ChannelList filters={filters} sort={sort} />
<Channel>
<Window>
<ChannelHeader />
<MessageList />
<MessageComposer emojiSearchIndex={SearchIndex} />
<MessageComposer />
</Window>
<Thread />
</Channel>
Expand Down
4 changes: 4 additions & 0 deletions examples/tutorial/src/6-emoji-picker/layout.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@layer stream, stream-overrides;
@import 'stream-chat-react/dist/css/index.css' layer(stream);
/* The emoji picker ships its own opt-in stylesheet (kept out of index.css so apps
that don't use it pay no CSS cost). This step renders <EmojiPicker>, so it must
be imported or the picker panel renders unstyled. */
@import 'stream-chat-react/dist/css/emoji-picker.css' layer(stream);

@layer stream-overrides {
.custom-theme {
Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const steps: TutorialStep[] = [
id: 'emoji-picker',
title: '6. Emoji Picker',
description:
'Wire a custom EmojiPicker into MessageComposer with emoji-mart search support.',
'Wire a custom EmojiPicker into MessageComposer with built-in emoji search support.',
Component: EmojiPickerStep,
},
{
Expand Down
1 change: 1 addition & 0 deletions examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"clsx": "^2.1.1",
"emoji-mart": "^5.6.0",
"human-id": "^4.1.3",
Expand Down
99 changes: 80 additions & 19 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { type CSSProperties, useCallback, useEffect, useMemo, useRef } from 'react';
import {
type CSSProperties,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type {
ChannelFilters,
ChannelOptions,
Expand All @@ -22,7 +29,6 @@ import {
ChatView,
defaultReactionOptions,
DialogManagerProvider,
mapEmojiMartData,
MessageReactions,
NotificationList,
type NotificationListProps,
Expand All @@ -32,9 +38,12 @@ import {
useCreateChatClient,
WithComponents,
} from 'stream-chat-react';
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data/sets/14/native.json';
import {
createTextComposerEmojiMiddleware,
EmojiPicker,
loadDefaultExtendedReactionOptions,
StreamEmojiPicker,
} from 'stream-chat-react/emojis';
import { humanId } from 'human-id';

import { appSettingsStore, useAppSettingsSelector } from './AppSettings';
Expand Down Expand Up @@ -73,8 +82,6 @@ import { CommandModeAttachmentSelector } from './CommandModeAttachmentSelector.t

const PUBLIC_VITE_EXAMPLE_API_KEY = 'xzwhhgtazy6h';

init({ data });

const parseUserIdFromToken = (token: string): string | undefined => {
try {
const [, payload] = token.split('.');
Expand Down Expand Up @@ -109,11 +116,6 @@ const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
// @ts-expect-error ai_generated isn't on LocalMessage's public type yet
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;

const newReactionOptions: ReactionOptions = {
...defaultReactionOptions,
extended: mapEmojiMartData(data),
};

const useUser = () => {
const searchParams = useMemo(() => new URLSearchParams(window.location.search), []);

Expand Down Expand Up @@ -180,18 +182,65 @@ const CustomMessageReactions = (props: React.ComponentProps<typeof MessageReacti

const CustomChannelSearch = () => <Search exitSearchOnInputBlur />;

// Demonstrates integrator-owned persistence of the picker's skin tone and
// frequently-used emoji via localStorage. The SDK itself never touches storage β€”
// it exposes these as controlled props so apps own where the state lives.
const EMOJI_SKIN_TONE_KEY = 'vite-example/emoji-skin-tone';
const EMOJI_FREQUENTLY_USED_KEY = 'vite-example/emoji-frequently-used';

const readStored = <T,>(key: string, fallback: T): T => {
try {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : fallback;
} catch {
return fallback;
}
};

const writeStored = (key: string, value: unknown) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// ignore storage failures (e.g. private browsing)
}
};

const EmojiPickerWithCustomOptions = (
props: React.ComponentProps<typeof EmojiPicker>,
props: React.ComponentProps<typeof StreamEmojiPicker>,
) => {
const { mode } = useAppSettingsSelector((state) => state.theme);
const { engine, ...pickerOptions } = useAppSettingsSelector(
(state) => state.emojiPicker,
);
const [skinTone, setSkinTone] = useState(() => readStored(EMOJI_SKIN_TONE_KEY, 0));
const [frequentlyUsedEmoji, setFrequentlyUsedEmoji] = useState(() =>
readStored<string[]>(EMOJI_FREQUENTLY_USED_KEY, []),
);

// The deprecated emoji-mart picker self-manages skin tone + frequently-used and
// accepts the same emoji-mart-compatible option names, so it only needs pickerProps.
if (engine === 'emoji-mart') {
return <EmojiPicker pickerProps={{ ...pickerOptions, theme: mode }} />;
}

return (
<EmojiPicker
<StreamEmojiPicker
{...props}
frequentlyUsedEmoji={frequentlyUsedEmoji}
onFrequentlyUsedChange={(ids) => {
setFrequentlyUsedEmoji(ids);
writeStored(EMOJI_FREQUENTLY_USED_KEY, ids);
}}
onSkinToneChange={(tone) => {
setSkinTone(tone);
writeStored(EMOJI_SKIN_TONE_KEY, tone);
}}
pickerProps={{
...props.pickerProps,
...pickerOptions,
theme: mode,
}}
skinTone={skinTone}
/>
);
};
Expand Down Expand Up @@ -226,6 +275,21 @@ const CustomAttachmentWithActions = (props: AttachmentProps) => (

const App = () => {
const { tokenProvider, userId, userImage, userName } = useUser();
// Restore "react with any emoji": lazily load the full emoji set and expose it as the
// extended reaction options (the reaction selector's "+" list). The dataset loads on
// demand via the same code-split chunk the picker uses; until it resolves only the
// quick reactions show.
const [reactionOptions, setReactionOptions] =
useState<ReactionOptions>(defaultReactionOptions);
useEffect(() => {
let active = true;
loadDefaultExtendedReactionOptions().then((extended) => {
if (active) setReactionOptions({ ...defaultReactionOptions, extended });
});
return () => {
active = false;
};
}, []);
const chatView = useAppSettingsSelector((state) => state.chatView);
const { mode: themeMode } = useAppSettingsSelector((state) => state.theme);
const initialSearchParams = useMemo(
Expand Down Expand Up @@ -357,9 +421,7 @@ const App = () => {
});

composer.textComposer.middlewareExecutor.insert({
middleware: [
createTextComposerEmojiMiddleware(SearchIndex) as TextComposerMiddleware,
],
middleware: [createTextComposerEmojiMiddleware() as TextComposerMiddleware],
position: { before: 'stream-io/text-composer/mentions-middleware' },
unique: true,
});
Expand Down Expand Up @@ -414,11 +476,10 @@ const App = () => {
return (
<WithComponents
overrides={{
emojiSearchIndex: SearchIndex,
EmojiPicker: EmojiPickerWithCustomOptions,
NotificationList: ConfigurableNotificationList,
MessageReactions: CustomMessageReactions,
reactionOptions: newReactionOptions,
reactionOptions,
Search: CustomChannelSearch,
HeaderEndContent: SidebarToggle,
HeaderStartContent: SidebarToggle,
Expand Down
Loading
Loading