Skip to content

Commit 67da540

Browse files
committed
feat: add customizable reaction details sorting
1 parent fdd4e1a commit 67da540

File tree

15 files changed

+138
-13
lines changed

15 files changed

+138
-13
lines changed

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

+8
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,14 @@ When true, show the reactions list component.
377377
| ------- |
378378
| boolean |
379379

380+
### sortReactionDetails
381+
382+
Comparator function to sort the list of reacted users. Should have the same signature as an array's `sort` method.
383+
384+
| Type | Default |
385+
| ---------------------------------------------------------- | ------------------ |
386+
| (this: ReactionResponse, that: ReactionResponse) => number | alphabetical order |
387+
380388
### sortReactions
381389

382390
Comparator function to sort reactions. Should have the same signature as an array's `sort` method.

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

+8
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,14 @@ is shown only when viewing unread messages.
621621
| ------- | ------- |
622622
| boolean | false |
623623

624+
### sortReactionDetails
625+
626+
Comparator function to sort the list of reacted users. Should have the same signature as an array's `sort` method.
627+
628+
| Type | Default |
629+
| ---------------------------------------------------------- | ------------------ |
630+
| (this: ReactionResponse, that: ReactionResponse) => number | alphabetical order |
631+
624632
### sortReactions
625633

626634
Comparator function to sort reactions. Should have the same signature as the `sort` method for a string array.

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

+8
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ The scroll-to behavior when new messages appear. Use `'smooth'` for regular chat
264264
| ------------------ | -------- |
265265
| 'smooth' \| 'auto' | 'smooth' |
266266

267+
### sortReactionDetails
268+
269+
Comparator function to sort the list of reacted users. Should have the same signature as an array's `sort` method.
270+
271+
| Type | Default |
272+
| ---------------------------------------------------------- | ------------------ |
273+
| (this: ReactionResponse, that: ReactionResponse) => number | alphabetical order |
274+
267275
### sortReactions
268276

269277
Comparator function to sort reactions. Should have the same signature as an array's `sort` method.

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

+8
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ Custom action handler to retry sending a message after a failed request.
363363
| -------- | -------------------------------------------------------------------------------------------------------- |
364364
| function | [ChannelActionContextValue['retrySendMessage']](../contexts/channel-action-context.mdx#retrysendmessage) |
365365

366+
### sortReactionDetails
367+
368+
Comparator function to sort the list of reacted users. Should have the same signature as an array's `sort` method.
369+
370+
| Type | Default |
371+
| ---------------------------------------------------------- | ------------------ |
372+
| (this: ReactionResponse, that: ReactionResponse) => number | alphabetical order |
373+
366374
### sortReactions
367375

368376
Comparator function to sort reactions. Should have the same signature as the `sort` method for a string array.

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

+12-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const CustomReactionsList = (props) => {
121121

122122
## Sorting reactions
123123

124-
By default, reactions are sorted alphabetically by type. You can change this behavior by passing a `sortReactions` prop to the `MessageList` (or `VirtualizedMessageList`).
124+
By default, reactions are sorted alphabetically by type. You can change this behavior by passing the `sortReactions` prop to the `MessageList` (or `VirtualizedMessageList`).
125125

126126
In this example, we sort the reactions in the descending order by the number of users:
127127

@@ -142,7 +142,9 @@ function sortByReactionCount(a, b) {
142142
</Chat>;
143143
```
144144

145-
For better performance, keep this function memoized with `useCallback`, or declare it in either global or module scope.
145+
Similarly, the `sortReactionDetails` prop can be passed to the `MessageList` (or `VirtualizedMessageList`) to sort the list of reacted users. The default implementation used by the reactions list modal dialog sorts users alphabetically by name.
146+
147+
For better performance, keep the sorting functions memoized with `useCallback`, or declare it in either global or module scope.
146148

147149
## ReactionSelector Props
148150

@@ -301,6 +303,14 @@ If true, adds a CSS class that reverses the horizontal positioning of the select
301303
| ------- | ------- |
302304
| boolean | false |
303305

306+
### sortReactionDetails
307+
308+
Comparator function to sort the list of reacted users. Should have the same signature as an array's `sort` method. This prop overrides the function stored in `MessageContext`.
309+
310+
| Type | Default |
311+
| ---------------------------------------------------------- | ------------------ |
312+
| (this: ReactionResponse, that: ReactionResponse) => number | alphabetical order |
313+
304314
### sortReactions
305315

306316
Comparator function to sort reactions. Should have the same signature as an array's `sort` method. This prop overrides the function stored in `MessageContext`.

src/components/Message/Message.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ type MessageContextPropsToPick =
5151
| 'onReactionListClick'
5252
| 'reactionSelectorRef'
5353
| 'showDetailedReactions'
54-
| 'sortReactions';
54+
| 'sortReactions'
55+
| 'sortReactionDetails';
5556

5657
type MessageWithContextProps<
5758
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
@@ -208,6 +209,7 @@ export const Message = <
208209
openThread: propOpenThread,
209210
pinPermissions,
210211
retrySendMessage: propRetrySendMessage,
212+
sortReactionDetails,
211213
sortReactions,
212214
} = props;
213215

@@ -310,6 +312,7 @@ export const Message = <
310312
readBy={props.readBy}
311313
renderText={props.renderText}
312314
showDetailedReactions={showDetailedReactions}
315+
sortReactionDetails={sortReactionDetails}
313316
sortReactions={sortReactions}
314317
threadList={props.threadList}
315318
unsafeHTML={props.unsafeHTML}

src/components/Message/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { MessageActionsArray } from './utils';
66

77
import type { GroupStyle } from '../MessageList/utils';
88
import type { MessageInputProps } from '../MessageInput/MessageInput';
9-
import type { ReactionsComparator } from '../Reactions/types';
9+
import type { ReactionDetailsComparator, ReactionsComparator } from '../Reactions/types';
1010

1111
import type { ChannelActionContextValue } from '../../context/ChannelActionContext';
1212
import type { StreamMessage } from '../../context/ChannelStateContext';
@@ -98,6 +98,8 @@ export type MessageProps<
9898
) => JSX.Element | null;
9999
/** Custom retry send message handler to override default in [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) */
100100
retrySendMessage?: ChannelActionContextValue<StreamChatGenerics>['retrySendMessage'];
101+
/** Comparator function to sort the list of reacted users, defaults to alphabetical order */
102+
sortReactionDetails?: ReactionDetailsComparator;
101103
/** Comparator function to sort reactions, defaults to alphabetical order */
102104
sortReactions?: ReactionsComparator;
103105
/** Whether the Message is in a Thread */

src/components/MessageList/MessageList.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ type PropsDrilledToMessage =
292292
| 'renderText'
293293
| 'retrySendMessage'
294294
| 'sortReactions'
295+
| 'sortReactionDetails'
295296
| 'unsafeHTML';
296297

297298
export type MessageListProps<

src/components/MessageList/VirtualizedMessageList.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type VirtualizedMessageListPropsForContext =
6565
| 'messageActions'
6666
| 'shouldGroupByUser'
6767
| 'sortReactions'
68+
| 'sortReactionDetails'
6869
| 'threadList';
6970

7071
/**
@@ -195,6 +196,7 @@ const VirtualizedMessageListWithContext = <
195196
separateGiphyPreview = false,
196197
shouldGroupByUser = false,
197198
showUnreadNotificationAlways,
199+
sortReactionDetails,
198200
sortReactions,
199201
stickToBottomScrollBehavior = 'smooth',
200202
suppressAutoscroll,
@@ -445,6 +447,7 @@ const VirtualizedMessageListWithContext = <
445447
ownMessagesReadByOthers,
446448
processedMessages,
447449
shouldGroupByUser,
450+
sortReactionDetails,
448451
sortReactions,
449452
threadList,
450453
unreadMessageCount: channelUnreadUiState?.unread_messages,
@@ -491,7 +494,8 @@ type PropsDrilledToMessage =
491494
| 'additionalMessageInputProps'
492495
| 'customMessageActions'
493496
| 'messageActions'
494-
| 'sortReactions';
497+
| 'sortReactions'
498+
| 'sortReactionDetails';
495499

496500
export type VirtualizedMessageListProps<
497501
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics

src/components/MessageList/VirtualizedMessageListComponents.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export const messageRenderer = <
137137
ownMessagesReadByOthers,
138138
processedMessages: messageList,
139139
shouldGroupByUser,
140+
sortReactionDetails,
140141
sortReactions,
141142
unreadMessageCount = 0,
142143
UnreadMessagesSeparator,
@@ -190,6 +191,7 @@ export const messageRenderer = <
190191
Message={MessageUIComponent}
191192
messageActions={messageActions}
192193
readBy={ownMessagesReadByOthers[message.id] || []}
194+
sortReactionDetails={sortReactionDetails}
193195
sortReactions={sortReactions}
194196
/>
195197
{showUnreadSeparator && (

src/components/Reactions/ReactionsList.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useProcessReactions } from './hooks/useProcessReactions';
88
import type { ReactEventHandler } from '../Message/types';
99
import type { DefaultStreamChatGenerics } from '../../types/types';
1010
import type { ReactionOptions } from './reactionOptions';
11-
import type { ReactionsComparator } from './types';
11+
import type { ReactionDetailsComparator, ReactionsComparator } from './types';
1212
import { ReactionsListModal } from './ReactionsListModal';
1313
import { MessageContextValue, useTranslationContext } from '../../context';
1414
import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks';
@@ -28,6 +28,8 @@ export type ReactionsListProps<
2828
reactions?: ReactionResponse<StreamChatGenerics>[];
2929
/** Display the reactions in the list in reverse order, defaults to false */
3030
reverse?: boolean;
31+
/** Comparator function to sort the list of reacted users, defaults to alphabetical order */
32+
sortReactionDetails?: ReactionDetailsComparator;
3133
/** Comparator function to sort reactions, defaults to alphabetical order */
3234
sortReactions?: ReactionsComparator;
3335
};
@@ -37,7 +39,7 @@ const UnMemoizedReactionsList = <
3739
>(
3840
props: ReactionsListProps<StreamChatGenerics>,
3941
) => {
40-
const { handleFetchReactions, reverse = false, ...rest } = props;
42+
const { handleFetchReactions, reverse = false, sortReactionDetails, ...rest } = props;
4143
const { existingReactions, hasReactions, totalReactionCount } = useProcessReactions(rest);
4244
const [selectedReactionType, setSelectedReactionType] = useState<string | null>(null);
4345
const { t } = useTranslationContext('ReactionsList');
@@ -104,6 +106,7 @@ const UnMemoizedReactionsList = <
104106
open={selectedReactionType !== null}
105107
reactions={existingReactions}
106108
selectedReactionType={selectedReactionType}
109+
sortReactionDetails={sortReactionDetails}
107110
/>
108111
</>
109112
);

src/components/Reactions/ReactionsListModal.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React, { useMemo } from 'react';
22
import clsx from 'clsx';
33

4+
import type { ReactionDetailsComparator, ReactionSummary } from './types';
5+
46
import { Modal, ModalProps } from '../Modal';
5-
import { ReactionSummary } from './types';
67
import { useFetchReactions } from './hooks/useFetchReactions';
78
import { LoadingIndicator } from '../Loading';
89
import { Avatar } from '../Avatar';
@@ -16,13 +17,21 @@ type ReactionsListModalProps<
1617
reactions: ReactionSummary[];
1718
selectedReactionType: string | null;
1819
onSelectedReactionTypeChange?: (reactionType: string) => void;
20+
sortReactionDetails?: ReactionDetailsComparator;
1921
};
2022

23+
const defaultSortReactionDetails: ReactionDetailsComparator = (a, b) => {
24+
const aName = a.user?.name ?? a.user?.id;
25+
const bName = b.user?.name ?? b.user?.id;
26+
return aName ? (bName ? aName.localeCompare(bName, 'en') : 1) : -1;
27+
};
28+
2129
export function ReactionsListModal({
2230
handleFetchReactions,
2331
onSelectedReactionTypeChange,
2432
reactions,
2533
selectedReactionType,
34+
sortReactionDetails = defaultSortReactionDetails,
2635
...modalProps
2736
}: ReactionsListModalProps) {
2837
const selectedReaction = reactions.find(
@@ -38,10 +47,12 @@ export function ReactionsListModal({
3847
return [];
3948
}
4049

41-
return allReactions.filter(
50+
const unsortedCurrentReactions = allReactions.filter(
4251
(reaction) => reaction.type === selectedReactionType && reaction.user,
4352
);
44-
}, [allReactions, selectedReactionType]);
53+
54+
return unsortedCurrentReactions.sort(sortReactionDetails);
55+
}, [allReactions, selectedReactionType, sortReactionDetails]);
4556

4657
return (
4758
<Modal {...modalProps}>

src/components/Reactions/__tests__/ReactionsListModal.test.js

+52-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('ReactionsListModal', () => {
152152
).toStrictEqual(Node.DOCUMENT_POSITION_FOLLOWING);
153153
});
154154

155-
it('should use custom comparator if provided', async () => {
155+
it('should use custom reactions comparator if provided', async () => {
156156
const reactionCounts = {
157157
haha: 2,
158158
like: 8,
@@ -180,4 +180,55 @@ describe('ReactionsListModal', () => {
180180
),
181181
).toStrictEqual(Node.DOCUMENT_POSITION_FOLLOWING);
182182
});
183+
184+
it('should order reacted users alphabetically by default', async () => {
185+
const reactionCounts = {
186+
haha: 3,
187+
};
188+
const reactions = generateReactionsFromReactionCounts(reactionCounts).reverse();
189+
const fetchReactions = jest.fn(() => Promise.resolve(reactions));
190+
const { getByTestId, getByText } = renderComponent({
191+
handleFetchReactions: fetchReactions,
192+
reaction_counts: reactionCounts,
193+
reactions,
194+
});
195+
196+
await act(() => {
197+
fireEvent.click(getByTestId('reactions-list-button-haha'));
198+
});
199+
200+
expect(
201+
getByText('Mark Number 0').compareDocumentPosition(getByText('Mark Number 1')),
202+
).toStrictEqual(Node.DOCUMENT_POSITION_FOLLOWING);
203+
204+
expect(
205+
getByText('Mark Number 1').compareDocumentPosition(getByText('Mark Number 2')),
206+
).toStrictEqual(Node.DOCUMENT_POSITION_FOLLOWING);
207+
});
208+
209+
it('should use custom reaction details comparator if provided', async () => {
210+
const reactionCounts = {
211+
haha: 3,
212+
};
213+
const reactions = generateReactionsFromReactionCounts(reactionCounts).reverse();
214+
const fetchReactions = jest.fn(() => Promise.resolve(reactions));
215+
const { getByTestId, getByText } = renderComponent({
216+
handleFetchReactions: fetchReactions,
217+
reaction_counts: reactionCounts,
218+
reactions,
219+
sortReactionDetails: (a, b) => -a.user.name.localeCompare(b.user.name),
220+
});
221+
222+
await act(() => {
223+
fireEvent.click(getByTestId('reactions-list-button-haha'));
224+
});
225+
226+
expect(
227+
getByText('Mark Number 2').compareDocumentPosition(getByText('Mark Number 1')),
228+
).toStrictEqual(Node.DOCUMENT_POSITION_FOLLOWING);
229+
230+
expect(
231+
getByText('Mark Number 1').compareDocumentPosition(getByText('Mark Number 0')),
232+
).toStrictEqual(Node.DOCUMENT_POSITION_FOLLOWING);
233+
});
183234
});

src/components/Reactions/types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ComponentType } from 'react';
1+
import type { ComponentType } from 'react';
2+
import type { ReactionResponse } from 'stream-chat';
23

34
export interface ReactionSummary {
45
EmojiComponent: ComponentType | null;
@@ -9,3 +10,5 @@ export interface ReactionSummary {
910
}
1011

1112
export type ReactionsComparator = (a: ReactionSummary, b: ReactionSummary) => number;
13+
14+
export type ReactionDetailsComparator = (a: ReactionResponse, b: ReactionResponse) => number;

src/context/MessageContext.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { ReactEventHandler } from '../components/Message/types';
1111
import type { MessageActionsArray } from '../components/Message/utils';
1212
import type { MessageInputProps } from '../components/MessageInput/MessageInput';
1313
import type { GroupStyle } from '../components/MessageList/utils';
14-
import type { ReactionsComparator } from '../components/Reactions/types';
14+
import type { ReactionDetailsComparator, ReactionsComparator } from '../components/Reactions/types';
1515

1616
import type { RenderTextOptions } from '../components/Message/renderText';
1717
import type { DefaultStreamChatGenerics, UnknownType } from '../types/types';
@@ -123,6 +123,9 @@ export type MessageContextValue<
123123
mentioned_users?: UserResponse<StreamChatGenerics>[],
124124
options?: RenderTextOptions,
125125
) => JSX.Element | null;
126+
/** Comparator function to sort the list of reacted users, defaults to alphabetical order */
127+
sortReactionDetails?: ReactionDetailsComparator;
128+
/** Comparator function to sort reactions, defaults to alphabetical order */
126129
sortReactions?: ReactionsComparator;
127130
/** Whether or not the Message is in a Thread */
128131
threadList?: boolean;

0 commit comments

Comments
 (0)