Skip to content

Commit bf7575b

Browse files
committed
feat(emojis): add StreamEmojiPicker and deprecate emoji-mart EmojiPicker
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.
1 parent 0e4025b commit bf7575b

14 files changed

Lines changed: 570 additions & 213 deletions

File tree

AI.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,18 @@ const CustomMessage = () => {
241241

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

244-
Emoji support is built into the SDK — no `emoji-mart` packages or `init()` call are
245-
required.
244+
Emoji support is built into the SDK — the **`StreamEmojiPicker`** needs no `emoji-mart`
245+
packages or `init()` call.
246+
247+
> **Deprecation:** the `EmojiPicker` export from `stream-chat-react/emojis` is the legacy
248+
> **emoji-mart-backed** picker. It still works unchanged for backwards compatibility, but
249+
> is **deprecated and will be removed in v15** and logs a one-time console warning. New
250+
> integrations should use `StreamEmojiPicker`; existing emoji-mart integrations keep
251+
> working until you migrate (see _Migrating from emoji-mart_ below).
246252
247253
**Steps**:
248254

249-
1. Import `EmojiPicker` from `stream-chat-react/emojis` and pass it to `Channel`.
255+
1. Import `StreamEmojiPicker` from `stream-chat-react/emojis` and pass it to `Channel`.
250256
2. Import the emoji picker's stylesheet: `import 'stream-chat-react/css/emoji-picker.css'`.
251257
It is intentionally **not** part of `index.css` (so apps that don't use the picker
252258
ship no emoji CSS); without it the picker panel renders unstyled.
@@ -255,20 +261,20 @@ required.
255261
`createTextComposerEmojiMiddleware()` (no argument — it uses the built-in index).
256262

257263
```tsx
258-
import { EmojiPicker } from 'stream-chat-react/emojis';
264+
import { StreamEmojiPicker } from 'stream-chat-react/emojis';
259265
import 'stream-chat-react/css/emoji-picker.css';
260266

261-
<Channel EmojiPicker={EmojiPicker}>{/* ... */}</Channel>;
267+
<Channel EmojiPicker={StreamEmojiPicker}>{/* ... */}</Channel>;
262268
```
263269

264270
**Notes**:
265271

266272
- Passing a custom `emojiSearchIndex` (including emoji-mart's `SearchIndex`) is
267273
still supported for advanced use.
268-
- Skin tone and "frequently used" are integrator-managed props on `EmojiPicker`
274+
- On `StreamEmojiPicker`, skin tone and "frequently used" are integrator-managed props
269275
(`skinTone`/`onSkinToneChange`, `frequentlyUsedEmoji`/`onFrequentlyUsedChange`);
270276
the SDK does not persist them. See `examples/vite/` for a localStorage example.
271-
- `pickerProps` accepts a curated set of emoji-mart-compatible `Picker` options (plus
277+
- On `StreamEmojiPicker`, `pickerProps` accepts a curated set of emoji-mart-compatible `Picker` options (plus
272278
`theme` and `style`):
273279
- **Layout**: `navPosition` / `previewPosition` (`'top' | 'bottom' | 'none'`),
274280
`searchPosition` (`'sticky' | 'static' | 'none'`), `skinTonePosition`
@@ -312,6 +318,13 @@ import 'stream-chat-react/css/emoji-picker.css';
312318

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

321+
- **Migrating from emoji-mart** (the deprecated `EmojiPicker`): swap `EmojiPicker`
322+
`StreamEmojiPicker`, then remove the `emoji-mart` / `@emoji-mart/react` /
323+
`@emoji-mart/data` installs and any `init({ data })` call. The deprecated `EmojiPicker`
324+
still accepts raw emoji-mart `pickerProps` (e.g. `set`, `emojiSize`); `StreamEmojiPicker`
325+
uses the curated `pickerProps` above. Autocomplete (`createTextComposerEmojiMiddleware`)
326+
and reactions (`mapEmojiMartData`) are engine-agnostic and need no changes.
327+
315328
**Reference**: See `examples/tutorial/src/6-emoji-picker/`
316329

317330
## TypeScript Setup

examples/tutorial/src/6-emoji-picker/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Window,
1313
WithComponents,
1414
} from 'stream-chat-react';
15-
import { EmojiPicker } from 'stream-chat-react/emojis';
15+
import { StreamEmojiPicker } from 'stream-chat-react/emojis';
1616

1717
import './layout.css';
1818
import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials';
@@ -61,7 +61,7 @@ const App = () => {
6161

6262
return (
6363
<Chat client={client}>
64-
<WithComponents overrides={{ EmojiPicker }}>
64+
<WithComponents overrides={{ EmojiPicker: StreamEmojiPicker }}>
6565
<ChannelList filters={filters} sort={sort} />
6666
<Channel>
6767
<Window>

examples/vite/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12+
"@emoji-mart/data": "^1.2.1",
13+
"@emoji-mart/react": "^1.1.1",
1214
"clsx": "^2.1.1",
15+
"emoji-mart": "^5.6.0",
1316
"human-id": "^4.1.3",
1417
"modern-normalize": "^3.0.1",
1518
"react": "^19.2.6",

examples/vite/src/App.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
createTextComposerEmojiMiddleware,
4343
EmojiPicker,
4444
loadDefaultExtendedReactionOptions,
45+
StreamEmojiPicker,
4546
} from 'stream-chat-react/emojis';
4647
import { humanId } from 'human-id';
4748

@@ -198,17 +199,25 @@ const writeStored = (key: string, value: unknown) => {
198199
};
199200

200201
const EmojiPickerWithCustomOptions = (
201-
props: React.ComponentProps<typeof EmojiPicker>,
202+
props: React.ComponentProps<typeof StreamEmojiPicker>,
202203
) => {
203204
const { mode } = useAppSettingsSelector((state) => state.theme);
204-
const emojiPicker = useAppSettingsSelector((state) => state.emojiPicker);
205+
const { engine, ...pickerOptions } = useAppSettingsSelector(
206+
(state) => state.emojiPicker,
207+
);
205208
const [skinTone, setSkinTone] = useState(() => readStored(EMOJI_SKIN_TONE_KEY, 0));
206209
const [frequentlyUsedEmoji, setFrequentlyUsedEmoji] = useState(() =>
207210
readStored<string[]>(EMOJI_FREQUENTLY_USED_KEY, []),
208211
);
209212

213+
// The deprecated emoji-mart picker self-manages skin tone + frequently-used and
214+
// accepts the same emoji-mart-compatible option names, so it only needs pickerProps.
215+
if (engine === 'emoji-mart') {
216+
return <EmojiPicker pickerProps={{ ...pickerOptions, theme: mode }} />;
217+
}
218+
210219
return (
211-
<EmojiPicker
220+
<StreamEmojiPicker
212221
{...props}
213222
frequentlyUsedEmoji={frequentlyUsedEmoji}
214223
onFrequentlyUsedChange={(ids) => {
@@ -221,7 +230,7 @@ const EmojiPickerWithCustomOptions = (
221230
}}
222231
pickerProps={{
223232
...props.pickerProps,
224-
...emojiPicker,
233+
...pickerOptions,
225234
theme: mode,
226235
}}
227236
skinTone={skinTone}

examples/vite/src/AppSettings/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type ChatViewSettingsState = {
1313

1414
export type EmojiPickerSettingsState = {
1515
autoFocus: boolean;
16+
engine: 'emoji-mart' | 'stream';
1617
maxFrequentRows: number;
1718
navPosition: 'top' | 'bottom' | 'none';
1819
noCountryFlags: boolean;
@@ -25,6 +26,7 @@ export type EmojiPickerSettingsState = {
2526
// Mirrors the SDK's EmojiPicker defaults; the settings tab resets to this.
2627
export const DEFAULT_EMOJI_PICKER_SETTINGS: EmojiPickerSettingsState = {
2728
autoFocus: true,
29+
engine: 'stream',
2830
maxFrequentRows: 1,
2931
navPosition: 'top',
3032
noCountryFlags: false,

examples/vite/src/AppSettings/tabs/EmojiPicker/EmojiPickerTab.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
import { useState } from 'react';
1+
import EmojiMartPickerImport from '@emoji-mart/react';
2+
import { type ComponentType, useState } from 'react';
23
import { Button } from 'stream-chat-react';
34
import { EmojiPickerPanel, type EmojiSelection } from 'stream-chat-react/emojis';
45
import {
56
appSettingsStore,
67
DEFAULT_EMOJI_PICKER_SETTINGS,
78
type EmojiPickerSettingsState,
9+
useAppSettingsSelector,
810
useAppSettingsState,
911
} from '../../state';
1012
import {
1113
SettingsTabBody,
1214
SettingsTabLayoutHeader,
1315
} from '../SettingsTabLayoutComponents.tsx';
1416

17+
// @emoji-mart/react ships CJS-on-default; unwrap for strict-ESM (Vite 8) interop.
18+
const EmojiMart = ((EmojiMartPickerImport as { default?: unknown }).default ??
19+
EmojiMartPickerImport) as ComponentType<Record<string, unknown>>;
20+
1521
type EmojiPickerTabProps = {
1622
close: () => void;
1723
};
@@ -66,17 +72,32 @@ const numberOptions = (values: number[]) =>
6672
* exercised too.
6773
*/
6874
const EmojiPickerPreview = ({ options }: { options: EmojiPickerSettingsState }) => {
75+
const { mode } = useAppSettingsSelector((state) => state.theme);
76+
const { engine, ...pickerOptions } = options;
6977
const [skinTone, setSkinTone] = useState(0);
7078
const [frequentlyUsedIds, setFrequentlyUsedIds] = useState<string[]>([]);
7179

80+
// The deprecated emoji-mart picker renders inline too and honors the same
81+
// emoji-mart-compatible option names, so the same controls drive both engines.
82+
if (engine === 'emoji-mart') {
83+
return (
84+
<EmojiMart
85+
{...pickerOptions}
86+
data={async () => (await import('@emoji-mart/data')).default}
87+
onEmojiSelect={() => undefined}
88+
theme={mode}
89+
/>
90+
);
91+
}
92+
7293
return (
7394
<EmojiPickerPanel
7495
frequentlyUsedIds={frequentlyUsedIds}
7596
onEmojiSelect={(emoji: EmojiSelection) =>
7697
setFrequentlyUsedIds((ids) => [emoji.id, ...ids.filter((id) => id !== emoji.id)])
7798
}
7899
onSkinToneChange={setSkinTone}
79-
options={{ ...options, exceptEmojis: [] }}
100+
options={{ ...pickerOptions, exceptEmojis: [] }}
80101
skinToneIndex={skinTone}
81102
/>
82103
);
@@ -97,7 +118,7 @@ export const EmojiPickerTab = ({ close }: EmojiPickerTabProps) => {
97118
<div className='app__settings-modal__content-stack'>
98119
<SettingsTabLayoutHeader
99120
close={close}
100-
description='Configure the built-in EmojiPicker via pickerProps. The live preview and the composer’s picker both update instantly.'
121+
description='Switch between the Stream (native) and deprecated emoji-mart pickers, and configure pickerProps. The live preview and the composer’s picker both update instantly.'
101122
title='Emoji Picker'
102123
/>
103124

@@ -117,6 +138,15 @@ export const EmojiPickerTab = ({ close }: EmojiPickerTabProps) => {
117138
Reset to defaults
118139
</Button>
119140
</div>
141+
<Field<EmojiPickerSettingsState['engine']>
142+
label='Picker engine'
143+
onSelect={(engine) => update({ engine })}
144+
options={[
145+
{ label: 'Stream (native)', value: 'stream' },
146+
{ label: 'emoji-mart (deprecated)', value: 'emoji-mart' },
147+
]}
148+
value={emojiPicker.engine}
149+
/>
120150
<Field<EmojiPickerSettingsState['navPosition']>
121151
label='Navigation position'
122152
onSelect={(navPosition) => update({ navPosition })}

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@
105105
},
106106
"peerDependencies": {
107107
"@breezystack/lamejs": "^1.2.7",
108+
"@emoji-mart/data": "^1.1.0",
109+
"@emoji-mart/react": "^1.1.0",
110+
"emoji-mart": "^5.4.0",
108111
"modern-normalize": "^3.0.1",
109112
"react": "^19.0.0 || ^18.0.0 || ^17.0.0",
110113
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0",
@@ -114,6 +117,15 @@
114117
"@breezystack/lamejs": {
115118
"optional": true
116119
},
120+
"@emoji-mart/data": {
121+
"optional": true
122+
},
123+
"@emoji-mart/react": {
124+
"optional": true
125+
},
126+
"emoji-mart": {
127+
"optional": true
128+
},
117129
"modern-normalize": {
118130
"optional": true
119131
}
@@ -129,6 +141,7 @@
129141
"@commitlint/cli": "^21.0.1",
130142
"@commitlint/config-conventional": "^21.0.1",
131143
"@emoji-mart/data": "1.2.1",
144+
"@emoji-mart/react": "^1.1.1",
132145
"@eslint/js": "^9.39.4",
133146
"@semantic-release/changelog": "^6.0.3",
134147
"@semantic-release/git": "^10.0.1",
@@ -151,6 +164,7 @@
151164
"@vitest/eslint-plugin": "^1.6.20",
152165
"concurrently": "^9.2.1",
153166
"conventional-changelog-conventionalcommits": "^9.3.1",
167+
"emoji-mart": "^5.6.0",
154168
"eslint": "^9.39.4",
155169
"eslint-plugin-import": "^2.32.0",
156170
"eslint-plugin-react": "^7.37.5",

0 commit comments

Comments
 (0)