Skip to content

Commit af6b94b

Browse files
committed
feat: control ReactionsSelector dialog display
1 parent d9f3709 commit af6b94b

12 files changed

+366
-542
lines changed

src/components/Message/Message.tsx

+2-19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo, useRef } from 'react';
1+
import React, { useCallback, useMemo } from 'react';
22

33
import {
44
useActionHandler,
@@ -10,7 +10,6 @@ import {
1010
useMuteHandler,
1111
useOpenThreadHandler,
1212
usePinHandler,
13-
useReactionClick,
1413
useReactionHandler,
1514
useReactionsFetcher,
1615
useRetryHandler,
@@ -44,14 +43,10 @@ type MessageContextPropsToPick =
4443
| 'handleReaction'
4544
| 'handleFetchReactions'
4645
| 'handleRetry'
47-
| 'isReactionEnabled'
4846
| 'mutes'
4947
| 'onMentionsClickMessage'
5048
| 'onMentionsHoverMessage'
51-
| 'onReactionListClick'
52-
| 'reactionSelectorRef'
5349
| 'reactionDetailsSort'
54-
| 'showDetailedReactions'
5550
| 'sortReactions'
5651
| 'sortReactionDetails';
5752

@@ -218,8 +213,6 @@ export const Message = <
218213
const { addNotification } = useChannelActionContext<StreamChatGenerics>('Message');
219214
const { highlightedMessageId, mutes } = useChannelStateContext<StreamChatGenerics>('Message');
220215

221-
const reactionSelectorRef = useRef<HTMLDivElement | null>(null);
222-
223216
const handleAction = useActionHandler(message);
224217
const handleOpenThread = useOpenThreadHandler(message, propOpenThread);
225218
const handleReaction = useReactionHandler(message);
@@ -264,20 +257,14 @@ export const Message = <
264257
notify: addNotification,
265258
});
266259

267-
const { isReactionEnabled, onReactionListClick, showDetailedReactions } = useReactionClick(
268-
message,
269-
reactionSelectorRef,
270-
undefined,
271-
closeReactionSelectorOnClick,
272-
);
273-
274260
const highlighted = highlightedMessageId === message.id;
275261

276262
return (
277263
<MemoizedMessage
278264
additionalMessageInputProps={props.additionalMessageInputProps}
279265
autoscrollToBottom={props.autoscrollToBottom}
280266
canPin={canPin}
267+
closeReactionSelectorOnClick={closeReactionSelectorOnClick}
281268
customMessageActions={props.customMessageActions}
282269
disableQuotedMessages={props.disableQuotedMessages}
283270
endOfGroup={props.endOfGroup}
@@ -297,7 +284,6 @@ export const Message = <
297284
handleRetry={handleRetry}
298285
highlighted={highlighted}
299286
initialMessage={props.initialMessage}
300-
isReactionEnabled={isReactionEnabled}
301287
lastReceivedId={props.lastReceivedId}
302288
message={message}
303289
Message={props.Message}
@@ -306,15 +292,12 @@ export const Message = <
306292
mutes={mutes}
307293
onMentionsClickMessage={onMentionsClick}
308294
onMentionsHoverMessage={onMentionsHover}
309-
onReactionListClick={onReactionListClick}
310295
onUserClick={props.onUserClick}
311296
onUserHover={props.onUserHover}
312297
pinPermissions={props.pinPermissions}
313298
reactionDetailsSort={reactionDetailsSort}
314-
reactionSelectorRef={reactionSelectorRef}
315299
readBy={props.readBy}
316300
renderText={props.renderText}
317-
showDetailedReactions={showDetailedReactions}
318301
sortReactionDetails={sortReactionDetails}
319302
sortReactions={sortReactions}
320303
threadList={props.threadList}

src/components/Message/MessageOptions.tsx

+15-21
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import {
66
ThreadIcon as DefaultThreadIcon,
77
} from './icons';
88
import { MESSAGE_ACTIONS } from './utils';
9-
109
import { MessageActions } from '../MessageActions';
1110

11+
import { useTranslationContext } from '../../context';
1212
import { MessageContextValue, useMessageContext } from '../../context/MessageContext';
1313

1414
import type { DefaultStreamChatGenerics, IconProps } from '../../types/types';
15-
import { useTranslationContext } from '../../context';
15+
import { ReactionSelectorWithButton } from '../Reactions/ReactionSelectorWithButton';
16+
import { useDialogIsOpen } from '../Dialog';
17+
import clsx from 'clsx';
1618

1719
export type MessageOptionsProps<
1820
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
@@ -21,8 +23,6 @@ export type MessageOptionsProps<
2123
ActionsIcon?: React.ComponentType<IconProps>;
2224
/* If true, show the `ThreadIcon` and enable navigation into a `Thread` component. */
2325
displayReplies?: boolean;
24-
/* React mutable ref that can be placed on the message root `div` of MessageActions component */
25-
messageWrapperRef?: React.RefObject<HTMLDivElement>;
2626
/* Custom component rendering the icon used in a button invoking reactions selector for a given message. */
2727
ReactionIcon?: React.ComponentType<IconProps>;
2828
/* Theme string to be added to CSS class names. */
@@ -40,7 +40,6 @@ const UnMemoizedMessageOptions = <
4040
ActionsIcon = DefaultActionsIcon,
4141
displayReplies = true,
4242
handleOpenThread: propHandleOpenThread,
43-
messageWrapperRef,
4443
ReactionIcon = DefaultReactionIcon,
4544
theme = 'simple',
4645
ThreadIcon = DefaultThreadIcon,
@@ -51,13 +50,12 @@ const UnMemoizedMessageOptions = <
5150
handleOpenThread: contextHandleOpenThread,
5251
initialMessage,
5352
message,
54-
onReactionListClick,
55-
showDetailedReactions,
5653
threadList,
5754
} = useMessageContext<StreamChatGenerics>('MessageOptions');
5855

5956
const { t } = useTranslationContext('MessageOptions');
60-
57+
const messageActionsDialogIsOpen = useDialogIsOpen(`message-actions--${message.id}`);
58+
const reactionSelectorDialogIsOpen = useDialogIsOpen(`reaction-selector--${message.id}`);
6159
const handleOpenThread = propHandleOpenThread || contextHandleOpenThread;
6260

6361
const messageActions = getMessageActions();
@@ -78,11 +76,15 @@ const UnMemoizedMessageOptions = <
7876
return null;
7977
}
8078

81-
const rootClassName = `str-chat__message-${theme}__actions str-chat__message-options`;
82-
8379
return (
84-
<div className={rootClassName} data-testid='message-options'>
85-
<MessageActions ActionsIcon={ActionsIcon} messageWrapperRef={messageWrapperRef} />
80+
<div
81+
className={clsx(`str-chat__message-${theme}__actions str-chat__message-options`, {
82+
'str-chat__message-options--active':
83+
messageActionsDialogIsOpen || reactionSelectorDialogIsOpen,
84+
})}
85+
data-testid='message-options'
86+
>
87+
<MessageActions ActionsIcon={ActionsIcon} />
8688
{shouldShowReplies && (
8789
<button
8890
aria-label={t('aria/Open Thread')}
@@ -94,15 +96,7 @@ const UnMemoizedMessageOptions = <
9496
</button>
9597
)}
9698
{shouldShowReactions && (
97-
<button
98-
aria-expanded={showDetailedReactions}
99-
aria-label={t('aria/Open Reaction Selector')}
100-
className={`str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--reactions str-chat__message-reactions-button`}
101-
data-testid='message-reaction-action'
102-
onClick={onReactionListClick}
103-
>
104-
<ReactionIcon className='str-chat__message-action-icon' />
105-
</button>
99+
<ReactionSelectorWithButton ReactionIcon={ReactionIcon} theme={theme} />
106100
)}
107101
</div>
108102
);

src/components/Message/MessageSimple.tsx

+4-19
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
2222
import { EditMessageForm as DefaultEditMessageForm, MessageInput } from '../MessageInput';
2323
import { MML } from '../MML';
2424
import { Modal } from '../Modal';
25-
import {
26-
ReactionsList as DefaultReactionList,
27-
ReactionSelector as DefaultReactionSelector,
28-
} from '../Reactions';
25+
import { ReactionsList as DefaultReactionList } from '../Reactions';
2926
import { MessageBounceModal } from '../MessageBounce/MessageBounceModal';
3027

3128
import { useChatContext } from '../../context/ChatContext';
@@ -59,13 +56,10 @@ const MessageSimpleWithContext = <
5956
handleRetry,
6057
highlighted,
6158
isMyMessage,
62-
isReactionEnabled,
6359
message,
6460
onUserClick,
6561
onUserHover,
66-
reactionSelectorRef,
6762
renderText,
68-
showDetailedReactions,
6963
threadList,
7064
} = props;
7165

@@ -83,7 +77,7 @@ const MessageSimpleWithContext = <
8377
MessageRepliesCountButton = DefaultMessageRepliesCountButton,
8478
MessageStatus = DefaultMessageStatus,
8579
MessageTimestamp = DefaultMessageTimestamp,
86-
ReactionSelector = DefaultReactionSelector,
80+
8781
ReactionsList = DefaultReactionList,
8882
PinIndicator,
8983
} = useComponentContext<StreamChatGenerics>('MessageSimple');
@@ -100,14 +94,6 @@ const MessageSimpleWithContext = <
10094
return <MessageDeleted message={message} />;
10195
}
10296

103-
/** FIXME: isReactionEnabled should be removed with next major version and a proper centralized permissions logic should be put in place
104-
* With the current permissions implementation it would be sth like:
105-
* const messageActions = getMessageActions();
106-
* const canReact = messageActions.includes(MESSAGE_ACTIONS.react);
107-
*/
108-
const canReact = isReactionEnabled;
109-
const canShowReactions = hasReactions;
110-
11197
const showMetadata = !groupedByUser || endOfGroup;
11298
const showReplyCountButton = !threadList && !!message.reply_count;
11399
const allowRetry = message.status === 'failed' && message.errorStatusCode !== 403;
@@ -136,7 +122,7 @@ const MessageSimpleWithContext = <
136122
'str-chat__message--has-attachment': hasAttachment,
137123
'str-chat__message--highlighted': highlighted,
138124
'str-chat__message--pinned pinned-message': message.pinned,
139-
'str-chat__message--with-reactions': canShowReactions,
125+
'str-chat__message--with-reactions': hasReactions,
140126
'str-chat__message-send-can-be-retried':
141127
message?.status === 'failed' && message?.errorStatusCode !== 403,
142128
'str-chat__message-with-thread-link': showReplyCountButton,
@@ -190,8 +176,7 @@ const MessageSimpleWithContext = <
190176
>
191177
<MessageOptions />
192178
<div className='str-chat__message-reactions-host'>
193-
{canShowReactions && <ReactionsList reverse />}
194-
{showDetailedReactions && canReact && <ReactionSelector ref={reactionSelectorRef} />}
179+
{hasReactions && <ReactionsList reverse />}
195180
</div>
196181
<div className='str-chat__message-bubble'>
197182
{message.attachments?.length && !message.quoted_message ? (

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

+82-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable jest-dom/prefer-to-have-class */
22
import React from 'react';
3-
import { fireEvent, render } from '@testing-library/react';
3+
import { act, fireEvent, render, screen } from '@testing-library/react';
44
import '@testing-library/jest-dom';
55

66
import { Message } from '../Message';
@@ -22,6 +22,7 @@ import {
2222
getTestClientWithUser,
2323
} from '../../../mock-builders';
2424
import { DialogsManagerProvider } from '../../../context';
25+
import { defaultReactionOptions } from '../../Reactions';
2526

2627
const MESSAGE_ACTIONS_TEST_ID = 'message-actions';
2728

@@ -73,6 +74,7 @@ async function renderMessageOptions({
7374
onReactionListClick={customMessageProps?.onReactionListClick}
7475
/>
7576
),
77+
reactionOptions: defaultReactionOptions,
7678
}}
7779
>
7880
<Message {...defaultMessageProps} {...customMessageProps}>
@@ -182,6 +184,85 @@ describe('<MessageOptions />', () => {
182184
expect(queryByTestId(reactionActionTestId)).not.toBeInTheDocument();
183185
});
184186

187+
it('should not render ReactionsSelector until open', async () => {
188+
const { queryByTestId } = await renderMessageOptions({
189+
channelStateOpts: {
190+
channelCapabilities: { 'send-reaction': true },
191+
},
192+
});
193+
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
194+
await act(async () => {
195+
await fireEvent.click(queryByTestId(reactionActionTestId));
196+
});
197+
expect(screen.getByTestId('reaction-selector')).toBeInTheDocument();
198+
});
199+
200+
it('should unmount ReactionsSelector when closed by click on dialog overlay', async () => {
201+
const { queryByTestId } = await renderMessageOptions({
202+
channelStateOpts: {
203+
channelCapabilities: { 'send-reaction': true },
204+
},
205+
});
206+
await act(async () => {
207+
await fireEvent.click(queryByTestId(reactionActionTestId));
208+
});
209+
await act(async () => {
210+
await fireEvent.click(screen.getByTestId('str-chat__dialog-overlay'));
211+
});
212+
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
213+
});
214+
215+
it('should unmount ReactionsSelector when closed pressed Esc button', async () => {
216+
const { queryByTestId } = await renderMessageOptions({
217+
channelStateOpts: {
218+
channelCapabilities: { 'send-reaction': true },
219+
},
220+
});
221+
await act(async () => {
222+
await fireEvent.click(queryByTestId(reactionActionTestId));
223+
});
224+
await act(async () => {
225+
await fireEvent.keyUp(document, { charCode: 27, code: 'Escape', key: 'Escape' });
226+
});
227+
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
228+
});
229+
230+
it('should unmount ReactionsSelector when closed on reaction selection and closeReactionSelectorOnClick enabled', async () => {
231+
const { queryByTestId } = await renderMessageOptions({
232+
channelStateOpts: {
233+
channelCapabilities: { 'send-reaction': true },
234+
},
235+
customMessageProps: {
236+
closeReactionSelectorOnClick: true,
237+
},
238+
});
239+
await act(async () => {
240+
await fireEvent.click(queryByTestId(reactionActionTestId));
241+
});
242+
await act(async () => {
243+
await fireEvent.click(screen.queryAllByTestId('select-reaction-button')[0]);
244+
});
245+
expect(screen.queryByTestId('reaction-selector')).not.toBeInTheDocument();
246+
});
247+
248+
it('should not unmount ReactionsSelector when closed on reaction selection and closeReactionSelectorOnClick enabled', async () => {
249+
const { queryByTestId } = await renderMessageOptions({
250+
channelStateOpts: {
251+
channelCapabilities: { 'send-reaction': true },
252+
},
253+
customMessageProps: {
254+
closeReactionSelectorOnClick: false,
255+
},
256+
});
257+
await act(async () => {
258+
await fireEvent.click(queryByTestId(reactionActionTestId));
259+
});
260+
await act(async () => {
261+
await fireEvent.click(screen.queryAllByTestId('select-reaction-button')[0]);
262+
});
263+
expect(screen.queryByTestId('reaction-selector')).toBeInTheDocument();
264+
});
265+
185266
it('should render message actions', async () => {
186267
const { queryByTestId } = await renderMessageOptions({
187268
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ChannelStateProvider,
1010
ChatProvider,
1111
ComponentProvider,
12+
DialogsManagerProvider,
1213
TranslationProvider,
1314
} from '../../../context';
1415
import {
@@ -65,9 +66,11 @@ async function renderQuotedMessage(customProps) {
6566
Message: () => <MessageSimple channelConfig={channelConfig} />,
6667
}}
6768
>
68-
<Message {...customProps}>
69-
<QuotedMessage {...customProps} />
70-
</Message>
69+
<DialogsManagerProvider id='quoted-message-dialogs-manager-provider'>
70+
<Message {...customProps}>
71+
<QuotedMessage {...customProps} />
72+
</Message>
73+
</DialogsManagerProvider>
7174
</ComponentProvider>
7275
</TranslationProvider>
7376
</ChannelActionProvider>

0 commit comments

Comments
 (0)