Skip to content

Commit

Permalink
[WIP] Add new EmojiPicker, adjust message inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
arnautov-anton committed Oct 10, 2023
1 parent 22aff77 commit cf71aff
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 14 deletions.
78 changes: 78 additions & 0 deletions docusaurus/docs/React/release-guides/emoji-picker.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
id: new-emoji-picker-integration
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` (with breaking changes)

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)

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 the button.

{/* TODO: extend once you have the component ready */}
{/* TODO: mention that we're dropping the EmojiContext */}

## 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

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, EmojiPicker } from 'stream-chat-react';

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

const ChannelWrapper = ({ 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 } = useMessageInputContext();

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

{open && (
<EmojiPicker
onEmojiClick={(emoji, event) => {
event.preventDefault(); // prevents cursor from changing place in the input
insertText(emoji.emoji);
}}
/>
)}
</>
);
};

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

You can make the component slightly better using `FloatingUI` by wrapping the actual picker element to make it float perfectly positioned above the button. See the source of the component which comes with the SDK for inspiration.
{/* TODO: provide link to the source (once ready) */}
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stream-chat-react",
"version": "0.0.0-development",
"version": "0.0.5-development",
"description": "React components to create chat conversations or livestream style chat",
"author": "GetStream",
"homepage": "https://getstream.io/chat/",
Expand Down Expand Up @@ -67,7 +67,13 @@
"peerDependencies": {
"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"
"stream-chat": "^8.0.0",
"emoji-picker-react": "^4.5.2"
},
"peerDependenciesMeta": {
"emoji-picker-react": {
"optional": true
}
},
"files": [
"dist",
Expand Down Expand Up @@ -127,6 +133,7 @@
"codecov": "^3.8.1",
"core-js": "^3.6.5",
"css-loader": "^5.0.1",
"emoji-picker-react": "^4.5.2",
"eslint": "7.14.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^6.15.0",
Expand Down
100 changes: 100 additions & 0 deletions src/components/MessageInput/EmojiPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useEffect, useState } from 'react';
import { usePopper } from 'react-popper';

// TODO: replace with emoji-mart
import Picker from 'emoji-picker-react';

import {
ThemeVersion,
useChatContext,
useMessageInputContext,
useTranslationContext,
} from '../../context';
import { EmojiIconLarge, EmojiPickerIcon } from './icons';
import { Tooltip } from '../Tooltip';

const classNames: Record<
ThemeVersion,
Record<
'buttonClassName' | 'emojiPickerContainerClassName' | 'wrapperClassName',
string | undefined
>
> = {
1: {
buttonClassName: 'str-chat__input-flat-emojiselect',
emojiPickerContainerClassName: undefined,
wrapperClassName: 'str-chat__emojiselect-wrapper',
},
2: {
buttonClassName: 'str-chat__emoji-picker-button',
emojiPickerContainerClassName: 'str-chat__message-textarea-emoji-picker-container',
wrapperClassName: 'str-chat__message-textarea-emoji-picker',
},
};

// TODO: handle small variant (MessageInputSmall)
export const EmojiPicker = () => {
const { themeVersion } = useChatContext('EmojiPicker');
const { t } = useTranslationContext('EmojiPicker');
const { insertText, textareaRef } = useMessageInputContext('EmojiPicker');
const [displayPicker, setDisplayPicker] = useState(false);
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { attributes, styles } = usePopper(referenceElement, popperElement, {
placement: themeVersion === '2' ? 'top-end' : 'top-start',
});

const { buttonClassName, emojiPickerContainerClassName, wrapperClassName } = classNames[
themeVersion
];

useEffect(() => {
if (!popperElement || !referenceElement) return;

const handlePointerDown = (e: PointerEvent) => {
const target = e.target as HTMLElement;

if (popperElement.contains(target) || referenceElement.contains(target)) return;

setDisplayPicker(false);
};

window.addEventListener('pointerdown', handlePointerDown);
return () => window.removeEventListener('pointerdown', handlePointerDown);
}, [referenceElement, popperElement]);

return (
<div className={wrapperClassName}>
{displayPicker && (
<div
className={emojiPickerContainerClassName}
style={styles.popper}
{...attributes.popper}
ref={setPopperElement}
>
<Picker
lazyLoadEmojis
onEmojiClick={(e) => {
insertText(e.emoji);
textareaRef.current?.focus();
}}
/>
</div>
)}
{themeVersion === '1' && (
<Tooltip>
{displayPicker ? t<string>('Close emoji picker') : t<string>('Open emoji picker')}
</Tooltip>
)}
<button
aria-label='Emoji picker'
className={buttonClassName}
onClick={() => setDisplayPicker((cv) => !cv)}
ref={setReferenceElement}
type='button'
>
{themeVersion === '2' ? <EmojiPickerIcon /> : <EmojiIconLarge />}
</button>
</div>
);
};
8 changes: 2 additions & 6 deletions src/components/MessageInput/MessageInputFlat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,8 @@ const MessageInputV1 = <
<div className='str-chat__input-flat-wrapper'>
{isUploadEnabled && <AttachmentPreviewList />}
<div className='str-chat__input-flat--textarea-wrapper'>
{EmojiPicker && (
<div className='str-chat__emojiselect-wrapper'>
<EmojiPicker />
</div>
)}
{cooldownRemaining && (
{EmojiPicker && <EmojiPicker />}
{!!cooldownRemaining && (
<div className='str-chat__input-flat-cooldown'>
<CooldownTimer
cooldownInterval={cooldownRemaining}
Expand Down
6 changes: 1 addition & 5 deletions src/components/MessageInput/MessageInputSmall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,7 @@ export const MessageInputSmall = <
</FileUploadButton>
</div>
)}
{EmojiPicker && (
<div className='str-chat__emojiselect-wrapper'>
<EmojiPicker />
</div>
)}
{EmojiPicker && <EmojiPicker />}
</>
)}
</div>
Expand Down
40 changes: 40 additions & 0 deletions src/components/MessageInput/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,46 @@ import type { Message } from 'stream-chat';

import type { DefaultStreamChatGenerics } from '../../types/types';

export const EmojiIconLarge = () => {
const { t } = useTranslationContext('EmojiIconLarge');

return (
<svg height='28' width='28' xmlns='http://www.w3.org/2000/svg'>
<title>{t<string>('Open emoji picker')}</title>
<g clipRule='evenodd' fillRule='evenodd'>
<path d='M14 4.4C8.6 4.4 4.4 8.6 4.4 14c0 5.4 4.2 9.6 9.6 9.6c5.4 0 9.6-4.2 9.6-9.6c0-5.4-4.2-9.6-9.6-9.6zM2 14c0-6.6 5.4-12 12-12s12 5.4 12 12s-5.4 12-12 12s-12-5.4-12-12zM12.8 11c0 1-.8 1.8-1.8 1.8s-1.8-.8-1.8-1.8s.8-1.8 1.8-1.8s1.8.8 1.8 1.8zM18.8 11c0 1-.8 1.8-1.8 1.8s-1.8-.8-1.8-1.8s.8-1.8 1.8-1.8s1.8.8 1.8 1.8zM8.6 15.4c.6-.4 1.2-.2 1.6.2c.6.8 1.6 1.8 3 2c1.2.4 2.8.2 4.8-2c.4-.4 1.2-.6 1.6 0c.4.4.6 1.2 0 1.6c-2.2 2.6-4.8 3.4-7 3c-2-.4-3.6-1.8-4.4-3c-.4-.6-.2-1.2.4-1.8z' />
</g>
</svg>
);
};

export const EmojiIconSmall = () => {
const { t } = useTranslationContext('EmojiIconSmall');

return (
<svg height='14' width='14' xmlns='http://www.w3.org/2000/svg'>
<title>{t<string>('Open emoji picker')}</title>
<g clipRule='evenodd' fillRule='evenodd'>
<path d='M6.7 1.42C3.73 1.42 1.42 3.73 1.42 6.7c0 2.97 2.31 5.28 5.28 5.28c2.97 0 5.28-2.31 5.28-5.28c0-2.97-2.31-5.28-5.28-5.28zM.1 6.7c0-3.63 2.97-6.6 6.6-6.6s6.6 2.97 6.6 6.6s-2.97 6.6-6.6 6.6s-6.6-2.97-6.6-6.6zM6.04 5.05c0 .55-.44.99-.99.99s-.99-.44-.99-.99s.44-.99.99-.99s.99.44.99.99zM9.34 5.05c0 .55-.44.99-.99.99s-.99-.44-.99-.99s.44-.99.99-.99s.99.44.99.99zM3.73 7.47c.33-.22.66-.11.88.11c.33.44.88.99 1.65 1.1c.66.22 1.54.11 2.64-1.1c.22-.22.66-.33.88 0c.22.22.33.66 0 .88c-1.21 1.43-2.64 1.87-3.85 1.65c-1.1-.22-1.98-.99-2.42-1.65c-.22-.33-.11-.66.22-.99z' />
</g>
</svg>
);
};

// ThemingV2 icon
export const EmojiPickerIcon = () => (
<svg
preserveAspectRatio='xMinYMin'
viewBox='0 0 28 28'
width='100%'
xmlns='http://www.w3.org/2000/svg'
>
<g clipRule='evenodd' fillRule='evenodd'>
<path d='M14 4.4C8.6 4.4 4.4 8.6 4.4 14c0 5.4 4.2 9.6 9.6 9.6c5.4 0 9.6-4.2 9.6-9.6c0-5.4-4.2-9.6-9.6-9.6zM2 14c0-6.6 5.4-12 12-12s12 5.4 12 12s-5.4 12-12 12s-12-5.4-12-12zM12.8 11c0 1-.8 1.8-1.8 1.8s-1.8-.8-1.8-1.8s.8-1.8 1.8-1.8s1.8.8 1.8 1.8zM18.8 11c0 1-.8 1.8-1.8 1.8s-1.8-.8-1.8-1.8s.8-1.8 1.8-1.8s1.8.8 1.8 1.8zM8.6 15.4c.6-.4 1.2-.2 1.6.2c.6.8 1.6 1.8 3 2c1.2.4 2.8.2 4.8-2c.4-.4 1.2-.6 1.6 0c.4.4.6 1.2 0 1.6c-2.2 2.6-4.8 3.4-7 3c-2-.4-3.6-1.8-4.4-3c-.4-.6-.2-1.2.4-1.8z'></path>
</g>
</svg>
);

export const FileUploadIcon = () => {
const { t } = useTranslationContext('FileUploadIcon');

Expand Down
1 change: 1 addition & 0 deletions src/components/MessageInput/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './MessageInputSmall';
export * from './QuotedMessagePreview';
export * from './UploadsPreview';
export * from './types';
export * from './EmojiPicker';
26 changes: 25 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,14 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"

"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6":
"@babel/runtime@^7.0.0":
version "7.23.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d"
integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==
dependencies:
regenerator-runtime "^0.14.0"

"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
Expand Down Expand Up @@ -4561,6 +4568,11 @@ cloneable-readable@^1.0.0:
process-nextick-args "^2.0.0"
readable-stream "^2.3.5"

clsx@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==

clsx@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
Expand Down Expand Up @@ -5608,6 +5620,13 @@ emoji-mart@3.0.1:
"@babel/runtime" "^7.0.0"
prop-types "^15.6.0"

emoji-picker-react@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.2.tgz#74317db98099ea0e3d42719768efc7a227f152cd"
integrity sha512-08v+yyFizoR9rEtLjuodTdlYQbkk0QWeKm6er0RQjks+b6Bfpw1UX9XheLcXreEAve7VVE7Tcu/kSAIm54V4zA==
dependencies:
clsx "^1.2.1"

emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
Expand Down Expand Up @@ -12354,6 +12373,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==

regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==

regenerator-transform@^0.14.2:
version "0.14.5"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
Expand Down

0 comments on commit cf71aff

Please sign in to comment.