Skip to content

Commit

Permalink
fix(emoji-mart): simplify EmojiPicker & EmojiIndex integration (#2117)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `EmojiPicker` and `EmojiIndex` have changed, see release guides in #2117 for more information
  • Loading branch information
arnautov-anton committed Oct 27, 2023
1 parent b829605 commit 60c24b8
Show file tree
Hide file tree
Showing 54 changed files with 578 additions and 828 deletions.
79 changes: 79 additions & 0 deletions docusaurus/docs/React/release-guides/emoji-picker-v11.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
id: new-emoji-picker-integration-v11
sidebar_position: 3
title: EmojiPicker Integration (11.0.0)
keywords: [migration guide, upgrade, emoji picker, breaking changes, v11]
---

import GHComponentLink from '../_docusaurus-components/GHComponentLink';

## Dropping support for built-in `EmojiPicker`

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.

## Changes to the default component composition (architecture)

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).

{/_ TODO: extend once the component is fully ready _/}

## Switching to opt-in mechanism (BREAKING CHANGE)

We made `emoji-mart` package in our SDK completely optional which means that `EmojiPicker` component is now disabled by default.

### Reinstate the `EmojiPicker` component

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:

```bash
yarn add emoji-mart@^5.5.2 @emoji-mart/data@^1.1.2 @emoji-mart/react@^1.1.1
```

\\Import `EmojiPicker` component from the `stream-chat-react` package:

```tsx
import { Channel } from 'stream-chat-react';
import { EmojiPicker } from 'stream-chat-react/emojis';

// and apply it to the Channel (component context)

const WrappedChannel = ({ children }) => {
return <Channel EmojiPicker={EmojiPicker}>{children}</Channel>;
};
```

### Build your custom `EmojiPicker` (example)

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:

```tsx
import EmojiPicker from 'emoji-picker-react';
import { useMessageInputContext } from 'stream-chat-react';

export const CustomEmojiPicker = () => {
const [open, setOpen] = useState(false);

const { insertText, textareaRef } = useMessageInputContext();

return (
<>
<button onClick={() => setOpen((cv) => !cv)}>Open EmojiPicker</button>

{open && (
<EmojiPicker
onEmojiClick={(emoji, event) => {
insertText(e.native);
textareaRef.current?.focus(); // returns focus back to the message input element
}}
/>
)}
</>
);
};

// and pass it down to the `Channel` component
```

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.

{/_ TODO: mention EmojiContext removal _/}
133 changes: 133 additions & 0 deletions docusaurus/docs/React/release-guides/emoji-search-index-v11.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
id: emoji-search-index-integration-v11
sidebar_position: 4
title: Emoji Search Index Integration (11.0.0)
keywords: [migration guide, upgrade, emoji search index, breaking changes, v11]
---

## Dropping support for built-in `EmojiIndex`

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`.

### Reinstate emoji autocomplete behavior (search for emojis with `:`)

Add `emoji-mart` to your packages and make sure the package versions fit our peer-dependency requirements:

```bash
yarn add emoji-mart@^5.5.2 @emoji-mart/data@^1.1.2
```

\Import `SearchIndex` and `data` from `emoji-mart`, initiate these data and then and pass `SearchIndex` to our `MessageInput` component:

```tsx
import { MessageInput } from 'stream-chat-react';
import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data';

init({ data });

export const WrappedMessageInput = () => {
return <MessageInput emojiSearchIndex={SearchIndex} focus />;
};
```

### Build your custom `emojiSearchIndex`

## Prerequisities

Your data returned from the `search` method should have _at least_ these three properies which our SDK relies on:

- name - display name for the emoji, ie: `"Smile"`
- id - unique emoji identificator
- skins - an array of emojis with different skins (our SDK uses only the first one in this array), ie: `[{ native: "😄" }]`

Optional properties:

- emoticons - an array of strings to match substitutions with, ie: `[":D", ":-D", ":d"]`
- native - native emoji string (old `emoji-mart` API), ie: `"😄"` - will be prioritized if specified

## Example

```tsx
import search from '@jukben/emoji-search';

const emoticonMap: Record<string, string[]> = {
'😃': [':D', ':-D'],
'😑': ['-_-'],
'😢': [":'("],
};

const emojiSearchIndex: EmojiSearchIndex = {
search: (query) => {
const results = search(query);

return results.slice(0, 15).map((data) => ({
emoticons: emoticonMap[data.name],
id: data.name,
name: data.keywords.slice(1, data.keywords.length).join(', '),
native: data.name,
skins: [],
}));
},
};

export const WrappedChannel = ({ children }) => (
<Channel emojiSearchIndex={emojiSearchIndex}>{children}</Channel>
);
```

### Migrate from `v10` to `v11` (`EmojiIndex` becomes `emojiSearchIndex`)

`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:

Your old code:

```tsx
import { Channel, MessageInput } from 'stream-chat-react';
// arbitrary import
import { CustomEmojiIndex } from './CustomEmojiIndex';

const App = () => {
return (
<Channel EmojiIndex={CustomEmojiIndex}>
{/* other components */}
<MessageInput />
</Channel>
);
};
```

Should newly look like this:

```tsx
import { Channel, MessageInput } from 'stream-chat-react';
// arbitrary import
import { CustomEmojiIndex } from './CustomEmojiIndex';

const App = () => {
return (
<Channel emojiSearchIndex={CustomEmojiIndex}>
{/* other components */}
<MessageInput />
</Channel>
);
};
```

Or enable it in either of the `MessageInput` components individually:

```tsx
import { Channel, MessageInput } from 'stream-chat-react';
// arbitrary import
import { CustomEmojiIndex } from './CustomEmojiIndex';

const App = () => {
return (
<Channel>
{/* other components */}
<MessageInput emojiSearchIndex={CustomEmojiIndex} />
<Thread additionalMessageInputProps={{ emojiSearchIndex: CustomEmojiIndex }} />
</Channel>
);
};
```
39 changes: 33 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,28 @@
"type": "git",
"url": "https://github.com/GetStream/stream-chat-react.git"
},
"typings": "dist/index.d.ts",
"types": "dist/index.d.ts",
"main": "dist/index.cjs.js",
"module": "dist/index.js",
"jsnext:main": "dist/index.js",
"jsdelivr": "./dist/browser.full-bundle.min.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs.js",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./emojis": {
"types": "./dist/components/Emojis/index.d.ts",
"require": "./dist/components/Emojis/index.cjs.js",
"import": "./dist/components/Emojis/index.js",
"default": "./dist/components/Emojis/index.js"
}
},
"style": "dist/css/v2/index.css",
"sideEffects": [
"*.css"
],
"source": "src/index.tsx",
"jsdelivr": "./dist/browser.full-bundle.min.js",
"keywords": [
"chat",
"messaging",
Expand All @@ -33,7 +45,6 @@
"@stream-io/stream-chat-css": "^3.13.0",
"clsx": "^2.0.0",
"dayjs": "^1.10.4",
"emoji-mart": "3.0.1",
"emoji-regex": "^9.2.0",
"hast-util-find-and-replace": "^4.1.2",
"i18next": "^21.6.14",
Expand Down Expand Up @@ -65,10 +76,24 @@
"mml-react": "^0.4.7"
},
"peerDependencies": {
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"emoji-mart": "^5.5.2",
"react": "^18.0.0 || ^17.0.0 || ^16.8.0",
"react-dom": "^18.0.0 || ^17.0.0 || ^16.8.0",
"stream-chat": "^8.0.0"
},
"peerDependenciesMeta": {
"emoji-mart": {
"optional": true
},
"@emoji-mart/data": {
"optional": true
},
"@emoji-mart/react": {
"optional": true
}
},
"files": [
"dist",
"package.json",
Expand All @@ -86,6 +111,8 @@
"@babel/preset-typescript": "^7.12.7",
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@ladle/react": "^0.16.0",
"@playwright/test": "^1.29.1",
"@rollup/plugin-babel": "^5.2.1",
Expand All @@ -103,7 +130,6 @@
"@testing-library/react-hooks": "^8.0.0",
"@types/deep-equal": "^1.0.1",
"@types/dotenv": "^8.2.0",
"@types/emoji-mart": "^3.0.9",
"@types/hast": "^2.3.4",
"@types/linkifyjs": "^2.1.3",
"@types/lodash.debounce": "^4.0.7",
Expand All @@ -127,6 +153,7 @@
"codecov": "^3.8.1",
"core-js": "^3.6.5",
"css-loader": "^5.0.1",
"emoji-mart": "^5.5.2",
"eslint": "7.14.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^6.15.0",
Expand Down
26 changes: 16 additions & 10 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable sort-keys */
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import image from '@rollup/plugin-image';
Expand All @@ -20,8 +21,10 @@ process.env.NODE_ENV = 'production';

const baseConfig = {
cache: false,
inlineDynamicImports: true,
input: 'src/index.ts',
input: {
index: 'src/index.ts',
'components/Emojis/index': 'src/components/Emojis/index.ts',
},
watch: {
chokidar: false,
},
Expand Down Expand Up @@ -89,30 +92,33 @@ const basePlugins = ({ useBrowserResolve = false }) => [
verbose: process.env.VERBOSE,
watch: process.env.ROLLUP_WATCH,
}),
// Json to ES modules conversion
// JSON to ES modules conversion
json({ compact: true }),
process.env.BUNDLE_SIZE ? visualizer() : null,
];

const normalBundle = {
...baseConfig,
external: externalDependencies,
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
},
],
output: {
dir: 'dist',
format: 'cjs',
entryFileNames: '[name].[format].js',
},
plugins: [...basePlugins({ useBrowserResolve: false })],
};

// TODO: multiple separate bundles
const fullBrowserBundle = ({ min } = { min: false }) => ({
...baseConfig,
inlineDynamicImports: true,
// includes EmojiPicker
input: 'src/index_UMD.ts',
output: [
{
file: min ? pkg.jsdelivr : pkg.jsdelivr.replace('.min', ''),
format: 'iife',
// TODO: figure out emoji-mart globals
globals: {
react: 'React',
'react-dom': 'ReactDOM',
Expand Down
13 changes: 7 additions & 6 deletions src/components/AutoCompleteTextarea/Item.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const Item = React.forwardRef(function Item(props, innerRef) {

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

const selectItem = useCallback(() => onSelectHandler(item), [item, onClickHandler]);
const handleSelect = useCallback(() => onSelectHandler(item), [item, onSelectHandler]);
const handleClick = useCallback((event) => onClickHandler(event, item), [item, onClickHandler]);

if (themeVersion === '2')
return (
Expand All @@ -27,8 +28,8 @@ export const Item = React.forwardRef(function Item(props, innerRef) {
<a
href=''
onClick={onClickHandler}
onFocus={selectItem}
onMouseEnter={selectItem}
onFocus={handleSelect}
onMouseEnter={handleSelect}
ref={innerRef}
>
<Component entity={item} selected={selected} />
Expand All @@ -40,9 +41,9 @@ export const Item = React.forwardRef(function Item(props, innerRef) {
<li className={clsx('rta__item', className)} style={style}>
<button
className={clsx('rta__entity', { 'rta__entity--selected': selected })}
onClick={onClickHandler}
onFocus={selectItem}
onMouseEnter={selectItem}
onClick={handleClick}
onFocus={handleSelect}
onMouseEnter={handleSelect}
ref={innerRef}
>
<div tabIndex={-1}>
Expand Down
Loading

0 comments on commit 60c24b8

Please sign in to comment.