Skip to content

Commit 95014eb

Browse files
Initial commit
1 parent 1695a85 commit 95014eb

File tree

6 files changed

+248
-66
lines changed

6 files changed

+248
-66
lines changed

examples/vite/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ import {
2929
ReactionsList,
3030
WithDragAndDropUpload,
3131
useChatContext,
32+
defaultReactionOptions,
33+
ReactionOptions,
34+
mapEmojiMartData,
3235
} from 'stream-chat-react';
3336
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
3437
import { init, SearchIndex } from 'emoji-mart';
35-
import data from '@emoji-mart/data';
38+
import data from '@emoji-mart/data/sets/14/native.json';
3639
import { humanId } from 'human-id';
3740
import { chatViewSelectorItemSet } from './Sidebar/ChatViewSelectorItemSet.tsx';
3841
import { useAppSettingsState } from './AppSettings';
@@ -69,6 +72,11 @@ const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
6972
// @ts-ignore
7073
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;
7174

75+
const newReactionOptions: ReactionOptions = {
76+
...defaultReactionOptions,
77+
extended: mapEmojiMartData(data),
78+
};
79+
7280
const useUser = () => {
7381
const userId = useMemo(() => {
7482
return (
@@ -184,6 +192,7 @@ const App = () => {
184192
emojiSearchIndex: SearchIndex,
185193
EmojiPicker,
186194
ReactionsList: CustomMessageReactions,
195+
reactionOptions: newReactionOptions,
187196
}}
188197
>
189198
<Chat client={chatClient} isMessageAIGenerated={isMessageAIGenerated}>

src/components/Reactions/ReactionSelector.tsx

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import clsx from 'clsx';
33

44
import { useDialog } from '../Dialog';
@@ -24,6 +24,7 @@ const stableOwnReactions: ReactionResponse[] = [];
2424

2525
const UnMemoizedReactionSelector = (props: ReactionSelectorProps) => {
2626
const { handleReaction: propHandleReaction, own_reactions: propOwnReactions } = props;
27+
const [extendedListOpen, setExtendedListOpen] = useState(false);
2728

2829
const { reactionOptions = defaultReactionOptions } =
2930
useComponentContext('ReactionSelector');
@@ -49,40 +50,88 @@ const UnMemoizedReactionSelector = (props: ReactionSelectorProps) => {
4950
return map;
5051
}, [ownReactions]);
5152

53+
const adjustedQuickReactionOptions = useMemo(() => {
54+
if (Array.isArray(reactionOptions)) return reactionOptions;
55+
56+
return Object.entries(reactionOptions.quick).map(
57+
([type, { Component, name, unicode }]) => ({
58+
Component,
59+
name,
60+
type,
61+
unicode,
62+
}),
63+
);
64+
}, [reactionOptions]);
65+
5266
return (
5367
<div className='str-chat__reaction-selector' data-testid='reaction-selector'>
54-
<ul className='str-chat__reaction-selector-list'>
55-
{reactionOptions.map(({ Component, name: reactionName, type: reactionType }) => (
56-
<li className='str-chat__reaction-list-selector__item' key={reactionType}>
57-
<button
58-
aria-label={`Select Reaction: ${reactionName || reactionType}`}
59-
aria-pressed={typeof ownReactionByType[reactionType] !== 'undefined'}
60-
className={clsx('str-chat__reaction-selector-list__item-button')}
61-
data-testid='select-reaction-button'
62-
data-text={reactionType}
63-
onClick={(event) => {
64-
handleReaction(reactionType, event);
65-
if (closeReactionSelectorOnClick) {
66-
dialog.close();
67-
}
68-
}}
69-
>
70-
<span className='str-chat__reaction-icon'>
71-
<Component />
72-
</span>
73-
</button>
74-
</li>
75-
))}
76-
</ul>
77-
<Button
78-
appearance='outline'
79-
circular
80-
className='str-chat__reaction-selector__add-button'
81-
size='sm'
82-
variant='secondary'
83-
>
84-
<IconPlusLarge />
85-
</Button>
68+
{!extendedListOpen && (
69+
<>
70+
<ul className='str-chat__reaction-selector-list'>
71+
{adjustedQuickReactionOptions.map(
72+
({ Component, name: reactionName, type: reactionType }) => (
73+
<li className='str-chat__reaction-list-selector__item' key={reactionType}>
74+
<button
75+
aria-label={`Select Reaction: ${reactionName || reactionType}`}
76+
aria-pressed={typeof ownReactionByType[reactionType] !== 'undefined'}
77+
className={clsx('str-chat__reaction-selector-list__item-button')}
78+
data-testid='select-reaction-button'
79+
data-text={reactionType}
80+
onClick={(event) => {
81+
handleReaction(reactionType, event);
82+
if (closeReactionSelectorOnClick) {
83+
dialog.close();
84+
}
85+
}}
86+
>
87+
<span className='str-chat__reaction-icon'>
88+
<Component />
89+
</span>
90+
</button>
91+
</li>
92+
),
93+
)}
94+
</ul>
95+
<Button
96+
appearance='outline'
97+
circular
98+
className='str-chat__reaction-selector__add-button'
99+
onClick={() => setExtendedListOpen(true)}
100+
size='sm'
101+
variant='secondary'
102+
>
103+
<IconPlusLarge />
104+
</Button>
105+
</>
106+
)}
107+
{extendedListOpen &&
108+
!Array.isArray(reactionOptions) &&
109+
reactionOptions.extended && (
110+
<div className='str-chat__reaction-selector-extended-list'>
111+
{Object.entries(reactionOptions.extended).map(
112+
([reactionType, { Component, name: reactionName }]) => (
113+
<button
114+
aria-label={`Select Reaction: ${reactionName || reactionType}`}
115+
aria-pressed={typeof ownReactionByType[reactionType] !== 'undefined'}
116+
className='str-chat__reaction-selector-extended-list__button str-chat__button str-chat__button--ghost str-chat__button--size-sm str-chat__button--circular'
117+
data-testid='select-reaction-button'
118+
data-text={reactionType}
119+
key={reactionType}
120+
onClick={(event) => {
121+
handleReaction(reactionType, event);
122+
if (closeReactionSelectorOnClick) {
123+
dialog.close();
124+
}
125+
}}
126+
>
127+
<span className='str-chat__reaction-icon'>
128+
<Component />
129+
</span>
130+
</button>
131+
),
132+
)}
133+
</div>
134+
)}
86135
</div>
87136
);
88137
};

src/components/Reactions/ReactionSelectorWithButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const ReactionSelectorWithButton = ({
4141
placement={isMyMessage() ? 'top-end' : 'top-start'}
4242
referenceElement={buttonRef.current}
4343
trapFocus
44+
updatePositionOnContentResize
4445
>
4546
<ReactionSelector />
4647
</DialogAnchor>

src/components/Reactions/hooks/useProcessReactions.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,35 @@ export const useProcessReactions = (params: UseProcessReactionsParams) => {
5252
);
5353

5454
const getEmojiByReactionType = useCallback(
55-
(reactionType: string) =>
56-
reactionOptions.find(({ type }) => type === reactionType)?.Component ?? null,
55+
(reactionType: string) => {
56+
if (Array.isArray(reactionOptions)) {
57+
return (
58+
reactionOptions.find(({ type }) => type === reactionType)?.Component ?? null
59+
);
60+
}
61+
62+
return (
63+
reactionOptions.quick[reactionType]?.Component ??
64+
reactionOptions.extended?.[reactionType]?.Component ??
65+
null
66+
);
67+
},
5768
[reactionOptions],
5869
);
5970

6071
const isSupportedReaction = useCallback(
61-
(reactionType: string) =>
62-
reactionOptions.some((reactionOption) => reactionOption.type === reactionType),
72+
(reactionType: string) => {
73+
if (Array.isArray(reactionOptions)) {
74+
return reactionOptions.some(
75+
(reactionOption) => reactionOption.type === reactionType,
76+
);
77+
}
78+
79+
return (
80+
typeof reactionOptions.quick[reactionType] !== 'undefined' ||
81+
typeof reactionOptions.extended?.[reactionType] !== 'undefined'
82+
);
83+
},
6384
[reactionOptions],
6485
);
6586

src/components/Reactions/reactionOptions.tsx

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,107 @@
22

33
import React from 'react';
44

5-
export type ReactionOptions = Array<{
5+
type LegacyReactionOptions = Array<{
66
Component: React.ComponentType;
77
type: string;
88
name?: string;
99
}>;
1010

11-
export const defaultReactionOptions: ReactionOptions = [
12-
{
13-
type: 'haha',
14-
Component: () => <>😂</>,
15-
name: 'Joy',
16-
},
17-
{
18-
type: 'like',
19-
Component: () => <>👍</>,
20-
name: 'Thumbs up',
21-
},
22-
{
23-
type: 'love',
24-
Component: () => <>❤️</>,
25-
name: 'Heart',
26-
},
27-
{ type: 'sad', Component: () => <>😔</>, name: 'Sad' },
28-
{
29-
type: 'wow',
30-
Component: () => <>😮</>,
31-
name: 'Astonished',
32-
},
33-
{
34-
type: 'fire',
35-
Component: () => <>🔥</>,
36-
name: 'Fire',
11+
type ReactionOptionData = {
12+
Component: React.ComponentType;
13+
name?: string;
14+
unicode?: string;
15+
};
16+
17+
export type ReactionOptions =
18+
| LegacyReactionOptions
19+
| {
20+
quick: {
21+
[key: string]: ReactionOptionData;
22+
};
23+
extended?: {
24+
[key: string]: ReactionOptionData;
25+
};
26+
};
27+
28+
export const mapEmojiMartData = (
29+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30+
emojiMartData: any,
31+
): NonNullable<Exclude<ReactionOptions, LegacyReactionOptions>['extended']> => {
32+
if (!emojiMartData || !emojiMartData.emojis) {
33+
return {};
34+
}
35+
36+
const newMap: ReturnType<typeof mapEmojiMartData> = {};
37+
38+
for (const emojiId in emojiMartData.emojis) {
39+
const emojiData = emojiMartData.emojis[emojiId];
40+
const [firstEmoji] = emojiData.skins;
41+
42+
if (!firstEmoji || !firstEmoji.native) continue;
43+
44+
const nativeEmoji = firstEmoji.native as string;
45+
46+
const unicode = emojiToUnicode(nativeEmoji);
47+
48+
newMap[unicode] = {
49+
Component: () => <>{nativeEmoji}</>,
50+
name: emojiData.name,
51+
};
52+
}
53+
54+
return newMap;
55+
};
56+
57+
export type AdvancedReactionOptions = {
58+
quick: ReactionOptions;
59+
extended: ReactionOptions;
60+
};
61+
62+
export const emojiToUnicode = (emoji: string) => {
63+
const unicodeStrings = [];
64+
for (const c of emoji) {
65+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
66+
const codePoint = c.codePointAt(0)!;
67+
unicodeStrings.push(`U+${codePoint.toString(16).toUpperCase().padStart(4, '0')}`);
68+
}
69+
70+
return unicodeStrings.join('-');
71+
};
72+
73+
export const unicodeToEmoji = (unicode: string) =>
74+
unicode
75+
.split('-')
76+
.map((code) => String.fromCodePoint(parseInt(code.replace('U+', ''), 16)))
77+
.join('');
78+
79+
export const defaultReactionOptions: ReactionOptions = {
80+
quick: {
81+
haha: {
82+
Component: () => <>😂</>,
83+
name: 'Joy',
84+
unicode: emojiToUnicode('😂'),
85+
},
86+
like: {
87+
Component: () => <>👍</>,
88+
name: 'Thumbs up',
89+
unicode: emojiToUnicode('👍'),
90+
},
91+
love: {
92+
Component: () => <>❤️</>,
93+
name: 'Heart',
94+
unicode: emojiToUnicode('❤️'),
95+
},
96+
sad: { Component: () => <>😔</>, name: 'Sad', unicode: emojiToUnicode('😔') },
97+
wow: {
98+
Component: () => <>😮</>,
99+
name: 'Astonished',
100+
unicode: emojiToUnicode('😮'),
101+
},
102+
fire: {
103+
Component: () => <>🔥</>,
104+
name: 'Fire',
105+
unicode: emojiToUnicode('🔥'),
106+
},
37107
},
38-
];
108+
};

src/components/Reactions/styling/ReactionSelector.scss

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
/* shadow/ios/light/elevation-3 */
1414
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.16);
1515

16+
&:has(.str-chat__reaction-selector-extended-list) {
17+
padding: 0;
18+
display: block;
19+
overflow-y: auto;
20+
scrollbar-width: none;
21+
border-radius: var(--radius-lg);
22+
max-height: 250px;
23+
}
24+
1625
.str-chat__reaction-selector__add-button {
1726
width: 32px;
1827
aspect-ratio: 1/1;
@@ -23,6 +32,29 @@
2332
}
2433
}
2534

35+
.str-chat__reaction-selector-extended-list {
36+
display: grid;
37+
grid-template-columns: repeat(7, 1fr);
38+
height: 100%;
39+
overflow-y: auto;
40+
padding-block: var(--spacing-md);
41+
padding-inline: var(--spacing-sm);
42+
43+
.str-chat__reaction-selector-extended-list__button {
44+
.str-chat__reaction-icon {
45+
height: 24px;
46+
width: 24px;
47+
font-size: 24px;
48+
letter-spacing: 0;
49+
line-height: 0;
50+
font-family: system-ui;
51+
display: flex;
52+
justify-content: center;
53+
align-items: center;
54+
}
55+
}
56+
}
57+
2658
.str-chat__reaction-selector-list {
2759
list-style: none;
2860
margin: var(--spacing-none, 0);

0 commit comments

Comments
 (0)