Skip to content

Commit a82bdcb

Browse files
fix: post-review MessageReactionsDetail adjustments (#3082)
## Breaking Changes ### Removed deprecated `sortReactionDetails` prop and `ReactionDetailsComparator` type The `sortReactionDetails` prop has been removed from the following components and contexts: - `MessageProps` (and by extension `Message` component) - `MessageContextValue` (and by extension `MessageContext`) - `MessageReactionsProps` (and by extension `MessageReactions` component) - `MessageReactionsDetailProps` (and by extension `MessageReactionsDetail` component) - `MessageListProps` (and by extension `MessageList` component) - `VirtualizedMessageListProps` (and by extension `VirtualizedMessageList` component) The `ReactionDetailsComparator` type export has been removed from `src/components/Reactions/types.ts`. **Migration:** Replace all usages of `sortReactionDetails` with the `reactionDetailsSort` prop, which accepts a `ReactionSort` object (server-side sort) instead of a client-side comparator function. ### Removed deprecated `reaction_counts` prop from `MessageReactionsProps` The `reaction_counts` prop on `MessageReactions` has been removed. It was previously ignored at runtime. **Migration:** Use `reaction_groups` instead, which provides richer per-reaction-type summary data (including `count`). ### Removed deprecated `reactionOptions` prop from `MessageReactionsProps` The `reactionOptions` prop on `MessageReactions` has been removed. The component now reads `reactionOptions` exclusively from `ComponentContext`. **Migration:** Pass `reactionOptions` via `<WithComponents overrides={{ reactionOptions }}>...</WithComponents>` instead of directly on `<MessageReactions>`. ### `UseProcessReactionsParams` no longer accepts `reaction_counts` or `reactionOptions` The `UseProcessReactionsParams` type (used by the `useProcessReactions` hook) no longer includes `reaction_counts` or `reactionOptions` fields, consistent with the prop removals above. ## New Features ### `capLimit` prop on `MessageReactions` A new optional `capLimit` prop allows configuring how many reaction types are displayed before overflow in each visual style: ```ts capLimit?: { clustered?: number; // default: 5 segmented?: number; // default: 4 }; ``` Previously, only the `segmented` style (in `top` position) capped reactions at a hardcoded limit of 4. Now the `clustered` style also caps displayed reaction types (default: 5), and both limits are configurable. ### `reactionGroups` added to `useProcessReactions` return value The `useProcessReactions` hook now returns `reactionGroups` (the resolved `Record<string, ReactionGroupResponse> | undefined`) alongside the existing return fields. ### `reactionGroups` prop on `MessageReactionsDetail` `MessageReactionsDetail` now accepts a `reactionGroups` prop used to determine reaction counts when removing a reaction. This enables the component to switch to the "all reactions" view when the last reaction of a given type is removed, instead of attempting to refetch an empty list. ### `UseProcessReactionsParams` type is now exported The `UseProcessReactionsParams` type is now exported from `src/components/Reactions/hooks/useProcessReactions.tsx`, making it available for consumers who build custom reaction processing logic.
1 parent 9472f7b commit a82bdcb

15 files changed

+104
-140
lines changed

src/components/Message/Message.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ type MessageContextPropsToPick =
5252
| 'onMentionsClickMessage'
5353
| 'onMentionsHoverMessage'
5454
| 'reactionDetailsSort'
55-
| 'sortReactions'
56-
| 'sortReactionDetails';
55+
| 'sortReactions';
5756

5857
type MessageWithContextProps = Omit<MessageProps, MessagePropsToOmit> &
5958
Pick<MessageContextValue, MessageContextPropsToPick> & {
@@ -211,7 +210,6 @@ export const Message = (props: MessageProps) => {
211210
pinPermissions,
212211
reactionDetailsSort,
213212
retrySendMessage: propRetrySendMessage,
214-
sortReactionDetails,
215213
sortReactions,
216214
} = props;
217215

@@ -316,7 +314,6 @@ export const Message = (props: MessageProps) => {
316314
readBy={props.readBy}
317315
renderText={props.renderText}
318316
returnAllReadData={props.returnAllReadData}
319-
sortReactionDetails={sortReactionDetails}
320317
sortReactions={sortReactions}
321318
threadList={props.threadList}
322319
unsafeHTML={props.unsafeHTML}

src/components/Message/types.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { PinPermissions, UserEventHandler } from './hooks';
55
import type { MessageActionsArray } from './utils';
66
import type { GroupStyle } from '../MessageList/utils';
77
import type { MessageComposerProps } from '../MessageComposer';
8-
import type { ReactionDetailsComparator, ReactionsComparator } from '../Reactions/types';
8+
import type { ReactionsComparator } from '../Reactions/types';
99
import type { ChannelActionContextValue } from '../../context/ChannelActionContext';
1010
import type { ComponentContextValue } from '../../context/ComponentContext';
1111
import type { MessageContextValue } from '../../context/MessageContext';
@@ -101,10 +101,6 @@ export type MessageProps = {
101101
retrySendMessage?: ChannelActionContextValue['retrySendMessage'];
102102
/** Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered. */
103103
returnAllReadData?: boolean;
104-
/** Comparator function to sort the list of reacted users
105-
* @deprecated use `reactionDetailsSort` instead
106-
*/
107-
sortReactionDetails?: ReactionDetailsComparator;
108104
/** Comparator function to sort reactions, defaults to chronological order */
109105
sortReactions?: ReactionsComparator;
110106
/** Whether the Message is in a Thread */

src/components/MessageList/MessageList.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ const MessageListWithContext = (props: MessageListWithContextProps) => {
9292
returnAllReadData = false,
9393
reviewProcessedMessage,
9494
showUnreadNotificationAlways,
95-
sortReactionDetails,
9695
sortReactions,
9796
suppressAutoscroll,
9897
threadList = false,
@@ -232,7 +231,6 @@ const MessageListWithContext = (props: MessageListWithContextProps) => {
232231
renderText: props.renderText,
233232
retrySendMessage: props.retrySendMessage,
234233
showAvatar: props.showAvatar,
235-
sortReactionDetails,
236234
sortReactions,
237235
unsafeHTML,
238236
},
@@ -500,7 +498,6 @@ type PropsDrilledToMessage =
500498
| 'retrySendMessage'
501499
| 'showAvatar'
502500
| 'sortReactions'
503-
| 'sortReactionDetails'
504501
| 'unsafeHTML';
505502

506503
export type MessageListProps = Partial<Pick<MessageProps, PropsDrilledToMessage>> & {

src/components/MessageList/VirtualizedMessageList.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ type PropsDrilledToMessage =
8686
| 'reactionDetailsSort'
8787
| 'renderText'
8888
| 'showAvatar'
89-
| 'sortReactions'
90-
| 'sortReactionDetails';
89+
| 'sortReactions';
9190

9291
type VirtualizedMessageListPropsForContext =
9392
| PropsDrilledToMessage
@@ -230,7 +229,6 @@ const VirtualizedMessageListWithContext = (
230229
shouldGroupByUser = false,
231230
showAvatar,
232231
showUnreadNotificationAlways,
233-
sortReactionDetails,
234232
sortReactions,
235233
stickToBottomScrollBehavior = 'smooth',
236234
suppressAutoscroll,
@@ -558,7 +556,6 @@ const VirtualizedMessageListWithContext = (
558556
returnAllReadData,
559557
shouldGroupByUser,
560558
showAvatar,
561-
sortReactionDetails,
562559
sortReactions,
563560
threadList,
564561
unreadMessageCount: channelUnreadUiState?.unread_messages,

src/components/MessageList/VirtualizedMessageListComponents.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ export const messageRenderer = (
130130
renderText,
131131
returnAllReadData,
132132
showAvatar,
133-
sortReactionDetails,
134133
sortReactions,
135134
threadList,
136135
unreadMessageCount = 0,
@@ -193,7 +192,6 @@ export const messageRenderer = (
193192
renderText={renderText}
194193
returnAllReadData={returnAllReadData}
195194
showAvatar={showAvatar}
196-
sortReactionDetails={sortReactionDetails}
197195
sortReactions={sortReactions}
198196
threadList={threadList}
199197
/>

src/components/Reactions/MessageReactions.tsx

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,48 +19,31 @@ import {
1919
import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks';
2020

2121
import type { ReactionGroupResponse, ReactionResponse } from 'stream-chat';
22-
import type { ReactionOptions } from './reactionOptions';
23-
import type {
24-
ReactionDetailsComparator,
25-
ReactionsComparator,
26-
ReactionType,
27-
} from './types';
22+
import type { ReactionsComparator, ReactionType } from './types';
2823
import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
2924

3025
export type MessageReactionsProps = Partial<
3126
Pick<MessageContextValue, 'handleFetchReactions' | 'reactionDetailsSort'>
3227
> & {
3328
/** An array of the own reaction objects to distinguish own reactions visually */
3429
own_reactions?: ReactionResponse[];
35-
/**
36-
* An object that keeps track of the count of each type of reaction on a message
37-
* @deprecated This override value is no longer taken into account. Use `reaction_groups` to override reaction counts instead.
38-
* */
39-
reaction_counts?: Record<string, number>;
4030
/** An object containing summary for each reaction type on a message */
4131
reaction_groups?: Record<string, ReactionGroupResponse>;
42-
/**
43-
* @deprecated
44-
* A list of the currently supported reactions on a message
45-
* */
46-
reactionOptions?: ReactionOptions;
4732
/** An array of the reaction objects to display in the list */
4833
reactions?: ReactionResponse[];
4934
/** Display the reactions in the list in reverse order, defaults to false */
5035
reverse?: boolean;
51-
/** Comparator function to sort the list of reacted users
52-
* @deprecated use `reactionDetailsSort` instead
53-
*/
54-
sortReactionDetails?: ReactionDetailsComparator;
5536
/** Comparator function to sort reactions, defaults to chronological order */
5637
sortReactions?: ReactionsComparator;
57-
5838
/**
5939
* Positioning of the reactions list relative to the message. Position is flipped by default for the messages of other users.
6040
*/
6141
flipHorizontalPosition?: boolean;
6242
verticalPosition?: 'top' | 'bottom' | null;
6343
visualStyle?: 'clustered' | 'segmented' | null;
44+
capLimit?: {
45+
[key in Extract<MessageReactionsProps['visualStyle'], string>]?: number;
46+
};
6447
};
6548

6649
/**
@@ -80,18 +63,23 @@ const FragmentOrButton = ({
8063

8164
const UnMemoizedMessageReactions = (props: MessageReactionsProps) => {
8265
const {
66+
capLimit: { clustered: capLimitClustered = 5, segmented: capLimitSegmented = 4 } = {},
8367
flipHorizontalPosition = false,
8468
handleFetchReactions,
8569
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8670
reactionDetailsSort,
87-
sortReactionDetails,
8871
verticalPosition = 'top',
8972
visualStyle = 'clustered',
9073
...rest
9174
} = props;
9275

93-
const { existingReactions, hasReactions, totalReactionCount, uniqueReactionTypeCount } =
94-
useProcessReactions(rest);
76+
const {
77+
existingReactions,
78+
hasReactions,
79+
reactionGroups,
80+
totalReactionCount,
81+
uniqueReactionTypeCount,
82+
} = useProcessReactions(rest);
9583
const [selectedReactionType, setSelectedReactionType] = useState<ReactionType | null>(
9684
null,
9785
);
@@ -118,20 +106,36 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => {
118106
/**
119107
* In segmented style with top position we show max 4 reactions and a
120108
* count of the rest, so we need to cap the existing reactions to display
121-
* at 4 and calculate the count of the rest.
109+
* at 4 and calculate the count of the rest. For clustered(top/bottom) we cap
110+
* the existing reactions to 5 but we don't calculate the count of the rest
111+
* because we show the total count instead. For segmented style with bottom
112+
* position we don't cap the existing reactions and we show all of them.
122113
*/
123114
const cappedExistingReactions = useMemo(() => {
124-
if (visualStyle !== 'segmented' || verticalPosition !== 'top') return null;
115+
if (visualStyle === 'segmented' && verticalPosition !== 'top') return null;
116+
117+
const capLimit = visualStyle === 'segmented' ? capLimitSegmented : capLimitClustered;
118+
119+
const sliced = existingReactions.slice(0, capLimit);
125120

126-
const sliced = existingReactions.slice(0, 4);
127121
return {
122+
/**
123+
* Accumulated reaction count of capped reaction types, first four in case of
124+
* segmented(top) and first five in case of clustered(top/bottom) variations.
125+
*/
128126
reactionCountToDisplay: sliced.reduce(
129127
(accumulatedCount, { reactionCount }) => accumulatedCount + reactionCount,
130128
0,
131129
),
132130
reactionsToDisplay: sliced,
133-
};
134-
}, [existingReactions, verticalPosition, visualStyle]);
131+
} as const;
132+
}, [
133+
capLimitClustered,
134+
capLimitSegmented,
135+
existingReactions,
136+
verticalPosition,
137+
visualStyle,
138+
]);
135139

136140
if (!hasReactions) return null;
137141

@@ -154,9 +158,7 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => {
154158
aria-pressed={isDialogOpen}
155159
buttonIf={visualStyle === 'clustered'}
156160
className='str-chat__message-reactions__list-button'
157-
onClick={() =>
158-
handleReactionButtonClick(existingReactions[0]?.reactionType ?? null)
159-
}
161+
onClick={() => handleReactionButtonClick(null)}
160162
>
161163
<ul className='str-chat__message-reactions__list'>
162164
{(cappedExistingReactions?.reactionsToDisplay ?? existingReactions).map(
@@ -186,18 +188,22 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => {
186188
</li>
187189
),
188190
)}
189-
{uniqueReactionTypeCount > 4 && cappedExistingReactions && (
190-
<li className='str-chat__message-reactions__list-item str-chat__message-reactions__list-item--more'>
191-
<button
192-
className='str-chat__message-reactions__list-item-button'
193-
onClick={() => handleReactionButtonClick(null)}
194-
>
195-
<span className='str-chat__message-reactions__overflow-count'>
196-
+{totalReactionCount - cappedExistingReactions.reactionCountToDisplay}
197-
</span>
198-
</button>
199-
</li>
200-
)}
191+
{uniqueReactionTypeCount > 4 &&
192+
cappedExistingReactions &&
193+
visualStyle === 'segmented' && (
194+
<li className='str-chat__message-reactions__list-item str-chat__message-reactions__list-item--more'>
195+
<button
196+
className='str-chat__message-reactions__list-item-button'
197+
onClick={() => handleReactionButtonClick(null)}
198+
>
199+
<span className='str-chat__message-reactions__overflow-count'>
200+
+
201+
{totalReactionCount -
202+
cappedExistingReactions.reactionCountToDisplay}
203+
</span>
204+
</button>
205+
</li>
206+
)}
201207
</ul>
202208
{visualStyle === 'clustered' && (
203209
<span className='str-chat__message-reactions__total-count'>
@@ -219,9 +225,9 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => {
219225
<MessageReactionsDetail
220226
handleFetchReactions={handleFetchReactions}
221227
onSelectedReactionTypeChange={setSelectedReactionType}
228+
reactionGroups={reactionGroups}
222229
reactions={existingReactions}
223230
selectedReactionType={selectedReactionType}
224-
sortReactionDetails={sortReactionDetails}
225231
totalReactionCount={totalReactionCount}
226232
/>
227233
</DialogAnchor>

src/components/Reactions/MessageReactionsDetail.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useMemo } from 'react';
22

3-
import type { ReactionDetailsComparator, ReactionSummary, ReactionType } from './types';
3+
import type { ReactionSummary, ReactionType } from './types';
44

55
import { useFetchReactions } from './hooks/useFetchReactions';
66
import { Avatar as DefaultAvatar } from '../Avatar';
@@ -13,6 +13,7 @@ import {
1313
} from '../../context';
1414
import type { ReactionSort } from 'stream-chat';
1515
import { defaultReactionOptions } from './reactionOptions';
16+
import type { useProcessReactions } from './hooks/useProcessReactions';
1617

1718
export type MessageReactionsDetailProps = Partial<
1819
Pick<MessageContextValue, 'handleFetchReactions' | 'reactionDetailsSort'>
@@ -21,9 +22,8 @@ export type MessageReactionsDetailProps = Partial<
2122
selectedReactionType: ReactionType | null;
2223
onSelectedReactionTypeChange?: (reactionType: ReactionType | null) => void;
2324
sort?: ReactionSort;
24-
/** @deprecated use `sort` instead */
25-
sortReactionDetails?: ReactionDetailsComparator;
2625
totalReactionCount?: number;
26+
reactionGroups?: ReturnType<typeof useProcessReactions>['reactionGroups'];
2727
};
2828

2929
const defaultReactionDetailsSort = { created_at: -1 } as const;
@@ -47,9 +47,9 @@ export function MessageReactionsDetail({
4747
handleFetchReactions,
4848
onSelectedReactionTypeChange,
4949
reactionDetailsSort: propReactionDetailsSort,
50+
reactionGroups,
5051
reactions,
5152
selectedReactionType,
52-
sortReactionDetails: propSortReactionDetails,
5353
totalReactionCount,
5454
}: MessageReactionsDetailProps) {
5555
const { client } = useChatContext();
@@ -63,11 +63,8 @@ export function MessageReactionsDetail({
6363
const {
6464
handleReaction: contextHandleReaction,
6565
reactionDetailsSort: contextReactionDetailsSort,
66-
sortReactionDetails: contextSortReactionDetails,
6766
} = useMessageContext(MessageReactionsDetail.name);
6867

69-
const legacySortReactionDetails = propSortReactionDetails ?? contextSortReactionDetails;
70-
7168
const reactionDetailsSort =
7269
propReactionDetailsSort ?? contextReactionDetailsSort ?? defaultReactionDetailsSort;
7370

@@ -82,14 +79,6 @@ export function MessageReactionsDetail({
8279
sort: reactionDetailsSort,
8380
});
8481

85-
const reactionDetailsWithLegacyFallback = useMemo(
86-
() =>
87-
legacySortReactionDetails
88-
? [...reactionDetails].sort(legacySortReactionDetails)
89-
: reactionDetails,
90-
[legacySortReactionDetails, reactionDetails],
91-
);
92-
9382
return (
9483
<div
9584
className='str-chat__message-reactions-detail'
@@ -142,7 +131,7 @@ export function MessageReactionsDetail({
142131
{areReactionsLoading && <LoadingIndicator />}
143132
{!areReactionsLoading && (
144133
<>
145-
{reactionDetailsWithLegacyFallback.map(({ type, user }) => {
134+
{reactionDetails.map(({ type, user }) => {
146135
const belongsToCurrentUser = client.user?.id === user?.id;
147136
const EmojiComponent = Array.isArray(reactionOptions)
148137
? undefined
@@ -173,8 +162,17 @@ export function MessageReactionsDetail({
173162
className='str-chat__message-reactions-detail__user-list-item-button'
174163
data-testid='remove-reaction-button'
175164
onClick={async (e) => {
165+
const reactionCountBeforeRemoval =
166+
reactionGroups?.[type]?.count ?? 0;
167+
176168
await contextHandleReaction(type, e);
177-
refetch();
169+
170+
// was 1, should be 0 after removal, display all reactions
171+
if (reactionCountBeforeRemoval <= 1) {
172+
onSelectedReactionTypeChange?.(null);
173+
} else {
174+
refetch();
175+
}
178176
}}
179177
>
180178
{t('Tap to remove')}

0 commit comments

Comments
 (0)