Skip to content

Commit 0ebdbc6

Browse files
authored
feat: show full list of reactions in a modal (#2249)
### 🎯 Goal We're updating UI for displaying reactions. Instead of showing the latest reactions only, we're adding a separate modal window full the full list of reactions. Although not a breaking change in terms of API, it's a breaking change in terms of UI and styling. ### 🛠 Implementation details 1. New component `ReactionsListModal` is added (rendered by default `ReactionsList`). 2. New handler added on message context level: `handleFetchReactions`. It's expected to load all reactions for current message (e.g. using our paged endpoint for fetching reactions). As usual, it can be overriden on `Message` or `ReactionsList` level. 3. Instead of showing counts for the latest reactions only, we show counts for all supported reactions in the `ReactionsList`. ### 🎨 UI Changes ![image](https://github.com/GetStream/stream-chat-react/assets/975978/69b93a0b-571f-4823-9f21-ceb744cfb4af) ### To-Do - [x] Tests for ReactionsListModal - [x] Add translations
1 parent a40809c commit 0ebdbc6

36 files changed

+759
-258
lines changed

docusaurus/docs/React/components/contexts/message-context.mdx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ Function that edits a message.
159159
| ----------------------------------------------------------- |
160160
| (event: React.BaseSyntheticEvent) => Promise<void\> \| void |
161161

162+
### handleFetchReactions
163+
164+
Function that loads the reactions for a message.
165+
166+
| Type |
167+
| ------------------------------------- |
168+
| () => Promise<ReactionResponse[]\> \ |
169+
170+
This function limits the number of loaded reactions to 1200. To customize this behavior, you can pass [a custom `ReactionsList` component](../message-components/reactions.mdx#handlefetchreactions).
171+
162172
### handleFlag
163173

164174
Function that flags a message.
@@ -339,8 +349,8 @@ An array of users that have read the current message.
339349

340350
Custom function to render message text content.
341351

342-
| Type | Default |
343-
| -------- | -------------------------------------------------------------------------------------- |
352+
| Type | Default |
353+
| -------- | ------------------------------------------------------------------------------ |
344354
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |
345355

346356
### setEditingState

docusaurus/docs/React/components/core-components/message-list.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,15 @@ deleted [message object](https://getstream.io/chat/docs/javascript/message_forma
323323
| ---------------------------------- |
324324
| (message: StreamMessage) => string |
325325

326+
### getFetchReactionsErrorNotification
327+
328+
Function that returns the notification text to be displayed when loading message reactions fails. This function receives the
329+
current [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument.
330+
331+
| Type |
332+
| ---------------------------------- |
333+
| (message: StreamMessage) => string |
334+
326335
### getFlagMessageErrorNotification
327336

328337
Function that returns the notification text to be displayed when a flag message request fails. This function receives the

docusaurus/docs/React/components/message-components/message-ui.mdx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,16 @@ Function that edits a message (overrides the function stored in `MessageContext`
287287
| ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
288288
| (event: React.BaseSyntheticEvent) => Promise<void\> \| void | [MessageContextValue['handleEdit']](../contexts/message-context.mdx#handleedit) |
289289

290+
### handleFetchReactions
291+
292+
Function that loads the reactions for a message (overrides the function stored in `MessageContext`).
293+
294+
| Type | Default |
295+
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
296+
| (event: React.BaseSyntheticEvent) => Promise<void\> \| void | [MessageContextValue['handleFetchReactions']](../contexts/message-context.mdx#handlhandlefetchreactions) |
297+
298+
This function limits the number of loaded reactions to 1200. To customize this behavior, you can pass [a custom `ReactionsList` component](./reactions.mdx#handlefetchreactions).
299+
290300
### handleFlag
291301

292302
Function that flags a message (overrides the function stored in `MessageContext`).
@@ -352,6 +362,7 @@ Function that returns whether a message belongs to the current user (overrides t
352362
| () => boolean |
353363

354364
### isReactionEnabled (deprecated)
365+
355366
If true, sending reactions is enabled in the currently active channel (overrides the value stored in `MessageContext`).
356367

357368
| Type | Default |
@@ -458,8 +469,8 @@ An array of users that have read the current message (overrides the value stored
458469

459470
Custom function to render message text content (overrides the function stored in `MessageContext`).
460471

461-
| Type | Default |
462-
| -------- | -------------------------------------------------------------------------------------- |
472+
| Type | Default |
473+
| -------- | ------------------------------------------------------------------------------ |
463474
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |
464475

465476
### setEditingState

docusaurus/docs/React/components/message-components/message.mdx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ deleted [message object](https://getstream.io/chat/docs/javascript/message_forma
147147
| ---------------------------------- |
148148
| (message: StreamMessage) => string |
149149

150+
### getFetchReactionsErrorNotification
151+
152+
Function that returns the notification text to be displayed when loading message reactions fails. This function receives the
153+
current [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument.
154+
155+
| Type |
156+
| ---------------------------------- |
157+
| (message: StreamMessage) => string |
158+
150159
### getFlagMessageErrorNotification
151160

152161
Function that returns the notification text to be displayed when a flag message request fails. This function receives the
@@ -324,8 +333,8 @@ An array of users that have read the current message.
324333

325334
Custom function to render message text content.
326335

327-
| Type | Default |
328-
| -------- | -------------------------------------------------------------------------------------- |
336+
| Type | Default |
337+
| -------- | ------------------------------------------------------------------------------ |
329338
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |
330339

331340
### retrySendMessage

docusaurus/docs/React/components/message-components/reactions.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,29 @@ Additional props to be passed to the [`NimbleEmoji`](https://github.com/missive/
204204
| ------ |
205205
| object |
206206

207+
### handleFetchReactions
208+
209+
Function that loads the message reactions (overrides the function stored in `MessageContext`).
210+
211+
| Type | Default |
212+
| -------------------- | --------------------------------------------------------------------------------------------------- |
213+
| () => Promise<void\> | [MessageContextValue['handleFetchReactions']](../contexts/message-context.mdx#handlefetchreactions) |
214+
215+
The default implementation of `handleFetchReactions`, provided via the [`MessageContext`](../contexts/message-context.mdx#handlefetchreactions), limits the number of loaded reactions to 1200. Use this prop to provide your own loading implementation:
216+
217+
```jsx
218+
const MyCustomReactionsList = (props) => {
219+
const { channel } = useChannelStateContext();
220+
const { message } = useMessageContext();
221+
222+
function fetchReactions() {
223+
return channel.getReactions(message.id, { limit: 42 });
224+
}
225+
226+
return <ReactionsList handleFetchReactions={fetchReactions} />;
227+
};
228+
```
229+
207230
### onClick
208231

209232
Custom on click handler for an individual reaction in the list (overrides the function stored in `MessageContext`).
@@ -263,6 +286,14 @@ Additional props to be passed to the [`NimbleEmoji`](https://github.com/missive/
263286
| ------ |
264287
| object |
265288

289+
### handleFetchReactions
290+
291+
Function that loads the message reactions (overrides the function stored in `MessageContext`).
292+
293+
| Type | Default |
294+
| -------------------- | --------------------------------------------------------------------------------------------------- |
295+
| () => Promise<void\> | [MessageContextValue['handleFetchReactions']](../contexts/message-context.mdx#handlefetchreactions) |
296+
266297
### handleReaction
267298

268299
Function that adds/removes a reaction on a message (overrides the function stored in `MessageContext`).

docusaurus/docs/React/hooks/message-hooks.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,37 @@ const MyCustomMessageComponent = () => {
179179
};
180180
```
181181
182+
### useReactionsFetcher
183+
184+
A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/hooks/useReactionsFetcher.ts) to handle loading message reactions. Returns an async function that loads and returns message reactions.
185+
186+
```jsx
187+
import { useQuery } from 'react-query';
188+
189+
const MyCustomReactionsList = () => {
190+
const { message } = useMessageContext();
191+
const { addNotification } = useChannelActionContext();
192+
const handleFetchReactions = useReactionsFetcher(message, { notify: addNotification });
193+
// This example relies on react-query - but you can use you preferred method
194+
// of fetching data instead
195+
const { data } = useQuery(['message', message.id, 'reactions'], handleFetchReactions);
196+
197+
if (!data) {
198+
return null;
199+
}
200+
201+
return (
202+
<>
203+
{data.map((reaction) => (
204+
<span key={reaction.type}>reaction.type</span>
205+
))}
206+
</>
207+
);
208+
};
209+
```
210+
211+
This function limits the number of loaded reactions to 1200. To customize this behavior, provide [your own loader function](../message-components/reactions.mdx#handlefetchreactions) instead.
212+
182213
### useRetryHandler
183214
184215
A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/hooks/useRetryHandler.ts) to handle the retry of sending a message.

src/components/Message/Message.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
usePinHandler,
1212
useReactionClick,
1313
useReactionHandler,
14+
useReactionsFetcher,
1415
useRetryHandler,
1516
useUserHandler,
1617
useUserRole,
@@ -38,6 +39,7 @@ type MessageContextPropsToPick =
3839
| 'handleOpenThread'
3940
| 'handlePin'
4041
| 'handleReaction'
42+
| 'handleFetchReactions'
4143
| 'handleRetry'
4244
| 'isReactionEnabled'
4345
| 'mutes'
@@ -158,6 +160,7 @@ export const Message = <
158160
closeReactionSelectorOnClick,
159161
disableQuotedMessages,
160162
getDeleteMessageErrorNotification,
163+
getFetchReactionsErrorNotification,
161164
getFlagMessageErrorNotification,
162165
getFlagMessageSuccessNotification,
163166
getMuteUserErrorNotification,
@@ -183,6 +186,11 @@ export const Message = <
183186
const handleRetry = useRetryHandler(propRetrySendMessage);
184187
const userRoles = useUserRole(message, onlySenderCanEdit, disableQuotedMessages);
185188

189+
const handleFetchReactions = useReactionsFetcher(message, {
190+
getErrorNotification: getFetchReactionsErrorNotification,
191+
notify: addNotification,
192+
});
193+
186194
const handleDelete = useDeleteHandler(message, {
187195
getErrorNotification: getDeleteMessageErrorNotification,
188196
notify: addNotification,
@@ -233,6 +241,7 @@ export const Message = <
233241
groupStyles={props.groupStyles}
234242
handleAction={handleAction}
235243
handleDelete={handleDelete}
244+
handleFetchReactions={handleFetchReactions}
236245
handleFlag={handleFlag}
237246
handleMute={handleMute}
238247
handleOpenThread={handleOpenThread}

src/components/Message/__tests__/MessageSimple.test.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
TranslationProvider,
3030
} from '../../../context';
3131
import {
32+
countReactions,
3233
generateChannel,
3334
generateMessage,
3435
generateReaction,
@@ -224,9 +225,10 @@ describe('<MessageSimple />', () => {
224225

225226
// FIXME: test relying on deprecated channel config parameter
226227
it('should render reaction list even though sending reactions is disabled in channel config', async () => {
227-
const bobReaction = generateReaction({ user: bob });
228+
const reactions = [generateReaction({ user: bob })];
228229
const message = generateAliceMessage({
229-
latest_reactions: [bobReaction],
230+
latest_reactions: reactions,
231+
reaction_counts: countReactions(reactions),
230232
text: undefined,
231233
});
232234

@@ -240,9 +242,10 @@ describe('<MessageSimple />', () => {
240242
});
241243

242244
it('should render reaction list with custom component when one is given', async () => {
243-
const bobReaction = generateReaction({ type: 'cool-reaction', user: bob });
245+
const reactions = [generateReaction({ type: 'cool-reaction', user: bob })];
244246
const message = generateAliceMessage({
245-
latest_reactions: [bobReaction],
247+
latest_reactions: reactions,
248+
reaction_counts: countReactions(reactions),
246249
text: undefined,
247250
});
248251
const CustomReactionsList = ({ reactions = [] }) => (

src/components/Message/__tests__/MessageText.test.js

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
TranslationProvider,
1515
} from '../../../context';
1616
import {
17+
countReactions,
1718
generateChannel,
1819
generateMessage,
1920
generateReaction,
@@ -101,7 +102,6 @@ async function renderMessageText({
101102
}
102103

103104
const messageTextTestId = 'message-text-inner-wrapper';
104-
const reactionSelectorTestId = 'reaction-selector';
105105

106106
describe('<MessageText />', () => {
107107
beforeEach(jest.clearAllMocks);
@@ -230,9 +230,10 @@ describe('<MessageText />', () => {
230230
});
231231

232232
it('should show reaction list if message has reactions and detailed reactions are not displayed', async () => {
233-
const bobReaction = generateReaction({ user: bob });
233+
const reactions = [generateReaction({ user: bob })];
234234
const message = generateAliceMessage({
235-
latest_reactions: [bobReaction],
235+
latest_reactions: reactions,
236+
reaction_counts: countReactions(reactions),
236237
});
237238

238239
let container;
@@ -248,9 +249,10 @@ describe('<MessageText />', () => {
248249

249250
// FIXME: test relying on deprecated channel config parameter
250251
it('should show reaction list even though sending reactions is disabled in channelConfig', async () => {
251-
const bobReaction = generateReaction({ user: bob });
252+
const reactions = [generateReaction({ user: bob })];
252253
const message = generateAliceMessage({
253-
latest_reactions: [bobReaction],
254+
latest_reactions: reactions,
255+
reaction_counts: countReactions(reactions),
254256
});
255257
const { container, queryByTestId } = await renderMessageText({
256258
channelCapabilitiesOverrides: { 'send-reaction': false },
@@ -262,24 +264,6 @@ describe('<MessageText />', () => {
262264
expect(results).toHaveNoViolations();
263265
});
264266

265-
it('should show reaction selector when message has reaction and reaction list is clicked', async () => {
266-
const bobReaction = generateReaction({ user: bob });
267-
const message = generateAliceMessage({
268-
latest_reactions: [bobReaction],
269-
});
270-
const { container, getByTestId, queryByTestId } = await renderMessageText({
271-
customProps: { message },
272-
});
273-
expect(queryByTestId(reactionSelectorTestId)).not.toBeInTheDocument();
274-
await act(() => {
275-
fireEvent.click(getByTestId('reaction-list'));
276-
});
277-
278-
expect(getByTestId(reactionSelectorTestId)).toBeInTheDocument();
279-
const results = await axe(container);
280-
expect(results).toHaveNoViolations();
281-
});
282-
283267
it('should render message options', async () => {
284268
const { container } = await renderMessageText();
285269
expect(MessageOptionsMock).toHaveBeenCalledTimes(1);

src/components/Message/__tests__/utils.test.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { generateMessage, generateReaction, generateUser } from 'mock-builders';
2-
import { getTestClientWithUser, mockTranslatorFunction } from '../../../mock-builders';
2+
import {
3+
countReactions,
4+
getTestClientWithUser,
5+
mockTranslatorFunction,
6+
} from '../../../mock-builders';
37
import {
48
areMessagePropsEqual,
59
areMessageUIPropsEqual,
@@ -236,8 +240,10 @@ describe('Message utils', () => {
236240
expect(messageHasReactions(message)).toBe(false);
237241
});
238242
it('should return true if message has reactions', () => {
243+
const reactions = [generateReaction()];
239244
const message = generateMessage({
240-
latest_reactions: [generateReaction()],
245+
latest_reactions: reactions,
246+
reaction_counts: countReactions(reactions),
241247
});
242248
expect(messageHasReactions(message)).toBe(true);
243249
});

0 commit comments

Comments
 (0)