Skip to content

Commit 044e97a

Browse files
committed
feat(emojis): restore reacting with any emoji via loadDefaultExtendedReactionOptions
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.
1 parent 737a03a commit 044e97a

5 files changed

Lines changed: 101 additions & 22 deletions

File tree

AI.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,29 @@ import 'stream-chat-react/css/emoji-picker.css';
271271
- `pickerProps` accepts only `theme` and `style`. emoji-mart `Picker` options
272272
(`data`, `set`, `custom`, `categories`, `perLine`, `emojiVersion`, `locale`, …) are
273273
no longer supported: the type rejects them and any passed via `as` casts are
274-
ignored with a console warning. For custom reaction emoji use `mapEmojiMartData`;
275-
for appearance use the `--str-chat__emoji-picker-*` CSS variables.
274+
ignored with a console warning. For appearance use the `--str-chat__emoji-picker-*`
275+
CSS variables.
276+
- To let users **react with any emoji** (the reaction selector's `+` button), fill
277+
`reactionOptions.extended` with the full emoji set. It also gates display — a reaction
278+
whose type isn't in `quick`/`extended` is not rendered. Load it lazily from the emojis
279+
entry (the dataset is code-split, so this adds nothing to your initial bundle):
280+
281+
```tsx
282+
import { defaultReactionOptions, type ReactionOptions } from 'stream-chat-react';
283+
import { loadDefaultExtendedReactionOptions } from 'stream-chat-react/emojis';
284+
285+
const [reactionOptions, setReactionOptions] =
286+
useState<ReactionOptions>(defaultReactionOptions);
287+
useEffect(() => {
288+
loadDefaultExtendedReactionOptions().then((extended) =>
289+
setReactionOptions({ ...defaultReactionOptions, extended }),
290+
);
291+
}, []);
292+
293+
<Channel reactionOptions={reactionOptions}>{/* ... */}</Channel>;
294+
```
295+
296+
For a curated subset instead, build the `extended` map yourself with `mapEmojiMartData`.
276297

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

examples/vite/src/App.tsx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
ChatView,
3030
defaultReactionOptions,
3131
DialogManagerProvider,
32-
mapEmojiMartData,
3332
MessageReactions,
3433
NotificationList,
3534
type NotificationListProps,
@@ -39,7 +38,11 @@ import {
3938
useCreateChatClient,
4039
WithComponents,
4140
} from 'stream-chat-react';
42-
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
41+
import {
42+
createTextComposerEmojiMiddleware,
43+
EmojiPicker,
44+
loadDefaultExtendedReactionOptions,
45+
} from 'stream-chat-react/emojis';
4346
import { humanId } from 'human-id';
4447

4548
import { appSettingsStore, useAppSettingsSelector } from './AppSettings';
@@ -105,23 +108,6 @@ const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
105108
// @ts-expect-error ai_generated isn't on LocalMessage's public type yet
106109
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;
107110

108-
// A few extra reactions, built from emoji-mart-shaped data via `mapEmojiMartData`.
109-
const extendedReactionData = {
110-
emojis: {
111-
clap: { name: 'Clapping Hands', skins: [{ native: '👏' }] },
112-
eyes: { name: 'Eyes', skins: [{ native: '👀' }] },
113-
hundred: { name: 'Hundred Points', skins: [{ native: '💯' }] },
114-
pray: { name: 'Folded Hands', skins: [{ native: '🙏' }] },
115-
rocket: { name: 'Rocket', skins: [{ native: '🚀' }] },
116-
tada: { name: 'Party Popper', skins: [{ native: '🎉' }] },
117-
},
118-
};
119-
120-
const newReactionOptions: ReactionOptions = {
121-
...defaultReactionOptions,
122-
extended: mapEmojiMartData(extendedReactionData),
123-
};
124-
125111
const useUser = () => {
126112
const searchParams = useMemo(() => new URLSearchParams(window.location.search), []);
127113

@@ -271,6 +257,21 @@ const CustomAttachmentWithActions = (props: AttachmentProps) => (
271257

272258
const App = () => {
273259
const { tokenProvider, userId, userImage, userName } = useUser();
260+
// Restore "react with any emoji": lazily load the full emoji set and expose it as the
261+
// extended reaction options (the reaction selector's "+" list). The dataset loads on
262+
// demand via the same code-split chunk the picker uses; until it resolves only the
263+
// quick reactions show.
264+
const [reactionOptions, setReactionOptions] =
265+
useState<ReactionOptions>(defaultReactionOptions);
266+
useEffect(() => {
267+
let active = true;
268+
loadDefaultExtendedReactionOptions().then((extended) => {
269+
if (active) setReactionOptions({ ...defaultReactionOptions, extended });
270+
});
271+
return () => {
272+
active = false;
273+
};
274+
}, []);
274275
const chatView = useAppSettingsSelector((state) => state.chatView);
275276
const { mode: themeMode } = useAppSettingsSelector((state) => state.theme);
276277
const initialSearchParams = useMemo(
@@ -472,7 +473,7 @@ const App = () => {
472473
EmojiPicker: EmojiPickerWithCustomOptions,
473474
NotificationList: ConfigurableNotificationList,
474475
MessageReactions: CustomMessageReactions,
475-
reactionOptions: newReactionOptions,
476+
reactionOptions,
476477
Search: CustomChannelSearch,
477478
HeaderEndContent: SidebarToggle,
478479
HeaderStartContent: SidebarToggle,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { render } from '@testing-library/react';
2+
import { loadDefaultExtendedReactionOptions } from '../reactions';
3+
import { emojiToUnicode } from '../../../components/Reactions/reactionOptions';
4+
5+
describe('loadDefaultExtendedReactionOptions', () => {
6+
it('builds an extended reaction map covering the full emoji dataset', async () => {
7+
const extended = await loadDefaultExtendedReactionOptions();
8+
9+
// The full vendored set (~1,870 emoji) — far beyond the handful of quick reactions,
10+
// so the picker's "+" list lets you react with essentially any emoji again.
11+
expect(Object.keys(extended).length).toBeGreaterThan(1000);
12+
});
13+
14+
it('keys entries by unicode and renders the native glyph', async () => {
15+
const extended = await loadDefaultExtendedReactionOptions();
16+
const grinningUnicode = emojiToUnicode('😀'); // U+1F600
17+
18+
const entry = extended[grinningUnicode];
19+
expect(entry).toBeDefined();
20+
expect(entry.unicode).toBe(grinningUnicode);
21+
22+
const { container } = render(<entry.Component />);
23+
expect(container).toHaveTextContent('😀');
24+
});
25+
26+
it('memoizes: repeated calls resolve the same map (no rebuild per reaction render)', async () => {
27+
const first = await loadDefaultExtendedReactionOptions();
28+
const second = await loadDefaultExtendedReactionOptions();
29+
30+
expect(first).toBe(second);
31+
});
32+
});

src/plugins/Emojis/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './components';
33
export * from './data';
44
export * from './EmojiPicker';
55
export * from './middleware';
6+
export * from './reactions';
67
export * from './search';

src/plugins/Emojis/reactions.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { mapEmojiMartData } from '../../components/Reactions/reactionOptions';
2+
import { loadEmojiData } from './data';
3+
import { memoizeAsyncWithReset } from './memoizeAsyncWithReset';
4+
5+
/**
6+
* Loads the full set of "extended" reaction options — every vendored emoji, keyed by
7+
* unicode — for use as `reactionOptions.extended`. This restores reacting with (almost)
8+
* any emoji via the reaction selector's "+" button, which previously came from feeding
9+
* the whole `@emoji-mart/data` object into `mapEmojiMartData`.
10+
*
11+
* Both halves of that feature read `reactionOptions.extended`: the selector only shows
12+
* "+" when it is non-empty, and `useProcessReactions` renders a reaction only when its
13+
* type is a known option — so without this map an arbitrary-emoji reaction can be sent
14+
* but not displayed.
15+
*
16+
* The dataset is fetched through the same lazily code-split dynamic import the picker
17+
* uses (and the result is memoized), so importing this loader costs nothing until it is
18+
* actually called and the ~340 KB JSON never lands in an app's initial bundle.
19+
* Integrators call it once — e.g. in an effect — and merge the result into their
20+
* reaction options; see the emoji migration notes in `AI.md`.
21+
*/
22+
export const loadDefaultExtendedReactionOptions = memoizeAsyncWithReset(async () =>
23+
mapEmojiMartData(await loadEmojiData()),
24+
);

0 commit comments

Comments
 (0)