Skip to content

Commit 60c24b8

Browse files
fix(emoji-mart): simplify EmojiPicker & EmojiIndex integration (#2117)
BREAKING CHANGE: `EmojiPicker` and `EmojiIndex` have changed, see release guides in #2117 for more information
1 parent b829605 commit 60c24b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+578
-828
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
id: new-emoji-picker-integration-v11
3+
sidebar_position: 3
4+
title: EmojiPicker Integration (11.0.0)
5+
keywords: [migration guide, upgrade, emoji picker, breaking changes, v11]
6+
---
7+
8+
import GHComponentLink from '../_docusaurus-components/GHComponentLink';
9+
10+
## Dropping support for built-in `EmojiPicker`
11+
12+
By default - our SDK would ship with `emoji-mart` dependency on top of which our `EmojiPicker` component is built. And since the SDK is using `emoji-mart` for this component - it was also reused for reactions (`ReactionsList` and `ReactionSelector`) and suggestion list (`MessageInput`). This solution proved to be very uncomfortable to work with when it came to replacing either of the mentioned components (or disabling them completely) and the final applications using our SDK would still bundle certain `emoji-mart` parts which weren't needed (or seemingly "disabled") resulting in sub-optimal load times. Maintaining such architecture became a burden so we're switching things a bit.
13+
14+
## Changes to the default component composition (architecture)
15+
16+
SDK's `EmojiPicker` component now comes as two-part "bundle" - a button and an actual picker element. The component now holds its own `open` state which is handled by clicking the button (or anywhere else to close it).
17+
18+
{/_ TODO: extend once the component is fully ready _/}
19+
20+
## Switching to opt-in mechanism (BREAKING CHANGE)
21+
22+
We made `emoji-mart` package in our SDK completely optional which means that `EmojiPicker` component is now disabled by default.
23+
24+
### Reinstate the `EmojiPicker` component
25+
26+
To reinstate the previous behavior you'll have to add `emoji-mart` to your packages and make sure the package versions fit our peer-dependency requirements:
27+
28+
```bash
29+
yarn add emoji-mart@^5.5.2 @emoji-mart/data@^1.1.2 @emoji-mart/react@^1.1.1
30+
```
31+
32+
\\Import `EmojiPicker` component from the `stream-chat-react` package:
33+
34+
```tsx
35+
import { Channel } from 'stream-chat-react';
36+
import { EmojiPicker } from 'stream-chat-react/emojis';
37+
38+
// and apply it to the Channel (component context)
39+
40+
const WrappedChannel = ({ children }) => {
41+
return <Channel EmojiPicker={EmojiPicker}>{children}</Channel>;
42+
};
43+
```
44+
45+
### Build your custom `EmojiPicker` (example)
46+
47+
If `emoji-mart` is too heavy for your use-case and you'd like to build your own you can certainly do so, here's a simple `EmojiPicker` built using `emoji-picker-react` package:
48+
49+
```tsx
50+
import EmojiPicker from 'emoji-picker-react';
51+
import { useMessageInputContext } from 'stream-chat-react';
52+
53+
export const CustomEmojiPicker = () => {
54+
const [open, setOpen] = useState(false);
55+
56+
const { insertText, textareaRef } = useMessageInputContext();
57+
58+
return (
59+
<>
60+
<button onClick={() => setOpen((cv) => !cv)}>Open EmojiPicker</button>
61+
62+
{open && (
63+
<EmojiPicker
64+
onEmojiClick={(emoji, event) => {
65+
insertText(e.native);
66+
textareaRef.current?.focus(); // returns focus back to the message input element
67+
}}
68+
/>
69+
)}
70+
</>
71+
);
72+
};
73+
74+
// and pass it down to the `Channel` component
75+
```
76+
77+
You can make the component slightly better using [`FloatingUI`](https://floating-ui.com/) by wrapping the actual picker element to make it float perfectly positioned above the button. See the [source of the component (`EmojiPicker`)]() which comes with the SDK for inspiration.
78+
79+
{/_ TODO: mention EmojiContext removal _/}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
id: emoji-search-index-integration-v11
3+
sidebar_position: 4
4+
title: Emoji Search Index Integration (11.0.0)
5+
keywords: [migration guide, upgrade, emoji search index, breaking changes, v11]
6+
---
7+
8+
## Dropping support for built-in `EmojiIndex`
9+
10+
By default, our SDK comes bundled with the `emoji-mart`'s (`emojiIndex`)[https://github.com/missive/emoji-mart/tree/v3.0.1#headless-search]. This index serves as a tool for efficiently searching through the emoji list and returning a subset that matches the search criteria (query). Within our SDK, this functionality is utilized by our autocomplete component, triggered by entering `:<query>` to the meessage input. This functionality will continue to be integrated within our SDK. However, due to our decision to discontinue the use of `emoji-mart` within the SDK, this feature will now be available on an opt-in basis. With the updated types and interface this will also allow integrators to supply their own `emojiSearchIndex` instead of relying exclusively on the one supplied by `emoji-mart`.
11+
12+
### Reinstate emoji autocomplete behavior (search for emojis with `:`)
13+
14+
Add `emoji-mart` to your packages and make sure the package versions fit our peer-dependency requirements:
15+
16+
```bash
17+
yarn add emoji-mart@^5.5.2 @emoji-mart/data@^1.1.2
18+
```
19+
20+
\Import `SearchIndex` and `data` from `emoji-mart`, initiate these data and then and pass `SearchIndex` to our `MessageInput` component:
21+
22+
```tsx
23+
import { MessageInput } from 'stream-chat-react';
24+
import { init, SearchIndex } from 'emoji-mart';
25+
import data from '@emoji-mart/data';
26+
27+
init({ data });
28+
29+
export const WrappedMessageInput = () => {
30+
return <MessageInput emojiSearchIndex={SearchIndex} focus />;
31+
};
32+
```
33+
34+
### Build your custom `emojiSearchIndex`
35+
36+
## Prerequisities
37+
38+
Your data returned from the `search` method should have _at least_ these three properies which our SDK relies on:
39+
40+
- name - display name for the emoji, ie: `"Smile"`
41+
- id - unique emoji identificator
42+
- skins - an array of emojis with different skins (our SDK uses only the first one in this array), ie: `[{ native: "😄" }]`
43+
44+
Optional properties:
45+
46+
- emoticons - an array of strings to match substitutions with, ie: `[":D", ":-D", ":d"]`
47+
- native - native emoji string (old `emoji-mart` API), ie: `"😄"` - will be prioritized if specified
48+
49+
## Example
50+
51+
```tsx
52+
import search from '@jukben/emoji-search';
53+
54+
const emoticonMap: Record<string, string[]> = {
55+
'😃': [':D', ':-D'],
56+
'😑': ['-_-'],
57+
'😢': [":'("],
58+
};
59+
60+
const emojiSearchIndex: EmojiSearchIndex = {
61+
search: (query) => {
62+
const results = search(query);
63+
64+
return results.slice(0, 15).map((data) => ({
65+
emoticons: emoticonMap[data.name],
66+
id: data.name,
67+
name: data.keywords.slice(1, data.keywords.length).join(', '),
68+
native: data.name,
69+
skins: [],
70+
}));
71+
},
72+
};
73+
74+
export const WrappedChannel = ({ children }) => (
75+
<Channel emojiSearchIndex={emojiSearchIndex}>{children}</Channel>
76+
);
77+
```
78+
79+
### Migrate from `v10` to `v11` (`EmojiIndex` becomes `emojiSearchIndex`)
80+
81+
`EmojiIndex` has previously lived in the `EmojiContext` passed to through `Channel` component. But since `EmojiContext` no longer exists in our SDK, the property has been moved to our `ComponentContext` (still passed through `Channel`) and changed its name to `emojiSearchIndex` to properly repesent its funtionality. If your custom `EmojiIndex` worked with our default components in `v10` then it should still work in `v11` without any changes to its `search` method output:
82+
83+
Your old code:
84+
85+
```tsx
86+
import { Channel, MessageInput } from 'stream-chat-react';
87+
// arbitrary import
88+
import { CustomEmojiIndex } from './CustomEmojiIndex';
89+
90+
const App = () => {
91+
return (
92+
<Channel EmojiIndex={CustomEmojiIndex}>
93+
{/* other components */}
94+
<MessageInput />
95+
</Channel>
96+
);
97+
};
98+
```
99+
100+
Should newly look like this:
101+
102+
```tsx
103+
import { Channel, MessageInput } from 'stream-chat-react';
104+
// arbitrary import
105+
import { CustomEmojiIndex } from './CustomEmojiIndex';
106+
107+
const App = () => {
108+
return (
109+
<Channel emojiSearchIndex={CustomEmojiIndex}>
110+
{/* other components */}
111+
<MessageInput />
112+
</Channel>
113+
);
114+
};
115+
```
116+
117+
Or enable it in either of the `MessageInput` components individually:
118+
119+
```tsx
120+
import { Channel, MessageInput } from 'stream-chat-react';
121+
// arbitrary import
122+
import { CustomEmojiIndex } from './CustomEmojiIndex';
123+
124+
const App = () => {
125+
return (
126+
<Channel>
127+
{/* other components */}
128+
<MessageInput emojiSearchIndex={CustomEmojiIndex} />
129+
<Thread additionalMessageInputProps={{ emojiSearchIndex: CustomEmojiIndex }} />
130+
</Channel>
131+
);
132+
};
133+
```

package.json

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,28 @@
88
"type": "git",
99
"url": "https://github.com/GetStream/stream-chat-react.git"
1010
},
11-
"typings": "dist/index.d.ts",
11+
"types": "dist/index.d.ts",
1212
"main": "dist/index.cjs.js",
1313
"module": "dist/index.js",
14-
"jsnext:main": "dist/index.js",
14+
"jsdelivr": "./dist/browser.full-bundle.min.js",
15+
"exports": {
16+
".": {
17+
"types": "./dist/index.d.ts",
18+
"require": "./dist/index.cjs.js",
19+
"import": "./dist/index.js",
20+
"default": "./dist/index.js"
21+
},
22+
"./emojis": {
23+
"types": "./dist/components/Emojis/index.d.ts",
24+
"require": "./dist/components/Emojis/index.cjs.js",
25+
"import": "./dist/components/Emojis/index.js",
26+
"default": "./dist/components/Emojis/index.js"
27+
}
28+
},
1529
"style": "dist/css/v2/index.css",
1630
"sideEffects": [
1731
"*.css"
1832
],
19-
"source": "src/index.tsx",
20-
"jsdelivr": "./dist/browser.full-bundle.min.js",
2133
"keywords": [
2234
"chat",
2335
"messaging",
@@ -33,7 +45,6 @@
3345
"@stream-io/stream-chat-css": "^3.13.0",
3446
"clsx": "^2.0.0",
3547
"dayjs": "^1.10.4",
36-
"emoji-mart": "3.0.1",
3748
"emoji-regex": "^9.2.0",
3849
"hast-util-find-and-replace": "^4.1.2",
3950
"i18next": "^21.6.14",
@@ -65,10 +76,24 @@
6576
"mml-react": "^0.4.7"
6677
},
6778
"peerDependencies": {
79+
"@emoji-mart/data": "^1.1.2",
80+
"@emoji-mart/react": "^1.1.1",
81+
"emoji-mart": "^5.5.2",
6882
"react": "^18.0.0 || ^17.0.0 || ^16.8.0",
6983
"react-dom": "^18.0.0 || ^17.0.0 || ^16.8.0",
7084
"stream-chat": "^8.0.0"
7185
},
86+
"peerDependenciesMeta": {
87+
"emoji-mart": {
88+
"optional": true
89+
},
90+
"@emoji-mart/data": {
91+
"optional": true
92+
},
93+
"@emoji-mart/react": {
94+
"optional": true
95+
}
96+
},
7297
"files": [
7398
"dist",
7499
"package.json",
@@ -86,6 +111,8 @@
86111
"@babel/preset-typescript": "^7.12.7",
87112
"@commitlint/cli": "^16.2.3",
88113
"@commitlint/config-conventional": "^16.2.1",
114+
"@emoji-mart/data": "^1.1.2",
115+
"@emoji-mart/react": "^1.1.1",
89116
"@ladle/react": "^0.16.0",
90117
"@playwright/test": "^1.29.1",
91118
"@rollup/plugin-babel": "^5.2.1",
@@ -103,7 +130,6 @@
103130
"@testing-library/react-hooks": "^8.0.0",
104131
"@types/deep-equal": "^1.0.1",
105132
"@types/dotenv": "^8.2.0",
106-
"@types/emoji-mart": "^3.0.9",
107133
"@types/hast": "^2.3.4",
108134
"@types/linkifyjs": "^2.1.3",
109135
"@types/lodash.debounce": "^4.0.7",
@@ -127,6 +153,7 @@
127153
"codecov": "^3.8.1",
128154
"core-js": "^3.6.5",
129155
"css-loader": "^5.0.1",
156+
"emoji-mart": "^5.5.2",
130157
"eslint": "7.14.0",
131158
"eslint-config-airbnb": "^18.2.1",
132159
"eslint-config-prettier": "^6.15.0",

rollup.config.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable sort-keys */
12
import babel from '@rollup/plugin-babel';
23
import commonjs from '@rollup/plugin-commonjs';
34
import image from '@rollup/plugin-image';
@@ -20,8 +21,10 @@ process.env.NODE_ENV = 'production';
2021

2122
const baseConfig = {
2223
cache: false,
23-
inlineDynamicImports: true,
24-
input: 'src/index.ts',
24+
input: {
25+
index: 'src/index.ts',
26+
'components/Emojis/index': 'src/components/Emojis/index.ts',
27+
},
2528
watch: {
2629
chokidar: false,
2730
},
@@ -89,30 +92,33 @@ const basePlugins = ({ useBrowserResolve = false }) => [
8992
verbose: process.env.VERBOSE,
9093
watch: process.env.ROLLUP_WATCH,
9194
}),
92-
// Json to ES modules conversion
95+
// JSON to ES modules conversion
9396
json({ compact: true }),
9497
process.env.BUNDLE_SIZE ? visualizer() : null,
9598
];
9699

97100
const normalBundle = {
98101
...baseConfig,
99102
external: externalDependencies,
100-
output: [
101-
{
102-
file: pkg.main,
103-
format: 'cjs',
104-
sourcemap: true,
105-
},
106-
],
103+
output: {
104+
dir: 'dist',
105+
format: 'cjs',
106+
entryFileNames: '[name].[format].js',
107+
},
107108
plugins: [...basePlugins({ useBrowserResolve: false })],
108109
};
109110

111+
// TODO: multiple separate bundles
110112
const fullBrowserBundle = ({ min } = { min: false }) => ({
111113
...baseConfig,
114+
inlineDynamicImports: true,
115+
// includes EmojiPicker
116+
input: 'src/index_UMD.ts',
112117
output: [
113118
{
114119
file: min ? pkg.jsdelivr : pkg.jsdelivr.replace('.min', ''),
115120
format: 'iife',
121+
// TODO: figure out emoji-mart globals
116122
globals: {
117123
react: 'React',
118124
'react-dom': 'ReactDOM',

src/components/AutoCompleteTextarea/Item.jsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export const Item = React.forwardRef(function Item(props, innerRef) {
1616

1717
const { themeVersion } = useChatContext('SuggestionItem');
1818

19-
const selectItem = useCallback(() => onSelectHandler(item), [item, onClickHandler]);
19+
const handleSelect = useCallback(() => onSelectHandler(item), [item, onSelectHandler]);
20+
const handleClick = useCallback((event) => onClickHandler(event, item), [item, onClickHandler]);
2021

2122
if (themeVersion === '2')
2223
return (
@@ -27,8 +28,8 @@ export const Item = React.forwardRef(function Item(props, innerRef) {
2728
<a
2829
href=''
2930
onClick={onClickHandler}
30-
onFocus={selectItem}
31-
onMouseEnter={selectItem}
31+
onFocus={handleSelect}
32+
onMouseEnter={handleSelect}
3233
ref={innerRef}
3334
>
3435
<Component entity={item} selected={selected} />
@@ -40,9 +41,9 @@ export const Item = React.forwardRef(function Item(props, innerRef) {
4041
<li className={clsx('rta__item', className)} style={style}>
4142
<button
4243
className={clsx('rta__entity', { 'rta__entity--selected': selected })}
43-
onClick={onClickHandler}
44-
onFocus={selectItem}
45-
onMouseEnter={selectItem}
44+
onClick={handleClick}
45+
onFocus={handleSelect}
46+
onMouseEnter={handleSelect}
4647
ref={innerRef}
4748
>
4849
<div tabIndex={-1}>

0 commit comments

Comments
 (0)