Skip to content

Commit fbd1b6f

Browse files
fix: MessageActions adjustments (#2472)
### 🎯 Goal This change allows rendering `CustomMessageActionsList` component without having to provide empty `customMessageActions` object.
1 parent eb0d6d4 commit fbd1b6f

File tree

5 files changed

+100
-66
lines changed

5 files changed

+100
-66
lines changed

src/components/Message/MessageOptions.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ReactionIcon as DefaultReactionIcon,
66
ThreadIcon as DefaultThreadIcon,
77
} from './icons';
8-
import { MESSAGE_ACTIONS, showMessageActionsBox } from './utils';
8+
import { MESSAGE_ACTIONS } from './utils';
99

1010
import { MessageActions } from '../MessageActions';
1111

@@ -47,7 +47,6 @@ const UnMemoizedMessageOptions = <
4747
} = props;
4848

4949
const {
50-
customMessageActions,
5150
getMessageActions,
5251
handleOpenThread: contextHandleOpenThread,
5352
initialMessage,
@@ -62,8 +61,6 @@ const UnMemoizedMessageOptions = <
6261
const handleOpenThread = propHandleOpenThread || contextHandleOpenThread;
6362

6463
const messageActions = getMessageActions();
65-
const showActionsBox =
66-
showMessageActionsBox(messageActions, threadList) || !!customMessageActions;
6764

6865
const shouldShowReactions = messageActions.indexOf(MESSAGE_ACTIONS.react) > -1;
6966
const shouldShowReplies =
@@ -85,9 +82,7 @@ const UnMemoizedMessageOptions = <
8582

8683
return (
8784
<div className={rootClassName} data-testid='message-options'>
88-
{showActionsBox && (
89-
<MessageActions ActionsIcon={ActionsIcon} messageWrapperRef={messageWrapperRef} />
90-
)}
85+
<MessageActions ActionsIcon={ActionsIcon} messageWrapperRef={messageWrapperRef} />
9186
{shouldShowReplies && (
9287
<button
9388
aria-label={t('aria/Open Thread')}

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

+32-35
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { MessageSimple } from '../MessageSimple';
99
import { ACTIONS_NOT_WORKING_IN_THREAD, MESSAGE_ACTIONS } from '../utils';
1010

1111
import { Attachment } from '../../Attachment';
12-
import { MessageActions as MessageActionsMock } from '../../MessageActions';
1312

1413
import { ChannelActionProvider } from '../../../context/ChannelActionContext';
1514
import { ChannelStateProvider } from '../../../context/ChannelStateContext';
@@ -23,9 +22,7 @@ import {
2322
getTestClientWithUser,
2423
} from '../../../mock-builders';
2524

26-
jest.mock('../../MessageActions', () => ({
27-
MessageActions: jest.fn(() => <div />),
28-
}));
25+
const MESSAGE_ACTIONS_TEST_ID = 'message-actions';
2926

3027
const minimumCapabilitiesToRenderMessageActions = { 'delete-any-message': true };
3128
const alice = generateUser({ name: 'alice' });
@@ -185,122 +182,122 @@ describe('<MessageOptions />', () => {
185182
});
186183

187184
it('should render message actions', async () => {
188-
await renderMessageOptions({
185+
const { queryByTestId } = await renderMessageOptions({
189186
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
190187
});
191-
// eslint-disable-next-line jest/prefer-called-with
192-
expect(MessageActionsMock).toHaveBeenCalled();
188+
189+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).toBeInTheDocument();
193190
});
194191

195192
it('should not show message actions button if actions are disabled', async () => {
196-
await renderMessageOptions({
193+
const { queryByTestId } = await renderMessageOptions({
197194
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
198195
customMessageProps: { messageActions: [] },
199196
});
200-
expect(MessageActionsMock).not.toHaveBeenCalled();
197+
198+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).not.toBeInTheDocument();
201199
});
202200

203201
it('should not show actions box for message in thread if only non-thread actions are available', async () => {
204-
await renderMessageOptions({
202+
const { queryByTestId } = await renderMessageOptions({
205203
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
206204
customMessageProps: { messageActions: ACTIONS_NOT_WORKING_IN_THREAD, threadList: true },
207205
});
208-
expect(MessageActionsMock).not.toHaveBeenCalled();
206+
207+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).not.toBeInTheDocument();
209208
});
210209

211210
it('should show actions box for message in thread if not only non-thread actions are available', async () => {
212-
await renderMessageOptions({
211+
const { queryByTestId } = await renderMessageOptions({
213212
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
214213
customMessageProps: {
215214
messageActions: [...ACTIONS_NOT_WORKING_IN_THREAD, MESSAGE_ACTIONS.delete],
216215
threadList: true,
217216
},
218217
});
219-
// eslint-disable-next-line jest/prefer-called-with
220-
expect(MessageActionsMock).toHaveBeenCalled();
218+
219+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).toBeInTheDocument();
221220
});
222221

223222
it('should show actions box for a message in thread if custom actions provided are non-thread', async () => {
224-
await renderMessageOptions({
223+
const { queryByTestId } = await renderMessageOptions({
225224
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
226225
customMessageProps: {
227226
customMessageActions: ACTIONS_NOT_WORKING_IN_THREAD,
228227
messageActions: ACTIONS_NOT_WORKING_IN_THREAD,
229228
threadList: true,
230229
},
231230
});
232-
// eslint-disable-next-line jest/prefer-called-with
233-
expect(MessageActionsMock).toHaveBeenCalled();
231+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).toBeInTheDocument();
234232
});
235233

236234
it('should not show actions box for message outside thread with single action "react"', async () => {
237-
await renderMessageOptions({
235+
const { queryByTestId } = await renderMessageOptions({
238236
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
239237
customMessageProps: {
240238
messageActions: [MESSAGE_ACTIONS.react],
241239
},
242240
});
243-
// eslint-disable-next-line jest/prefer-called-with
244-
expect(MessageActionsMock).not.toHaveBeenCalled();
241+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).not.toBeInTheDocument();
245242
});
246243

247244
it('should show actions box for message outside thread with single action "react" if custom actions available', async () => {
248-
await renderMessageOptions({
245+
const { queryByTestId } = await renderMessageOptions({
249246
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
250247
customMessageProps: {
251248
customMessageActions: [MESSAGE_ACTIONS.react],
252249
messageActions: [MESSAGE_ACTIONS.react],
253250
},
254251
});
255-
// eslint-disable-next-line jest/prefer-called-with
256-
expect(MessageActionsMock).toHaveBeenCalled();
252+
253+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).toBeInTheDocument();
257254
});
258255

259256
it('should not show actions box for message outside thread with single action "reply"', async () => {
260-
await renderMessageOptions({
257+
const { queryByTestId } = await renderMessageOptions({
261258
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
262259
customMessageProps: {
263260
messageActions: [MESSAGE_ACTIONS.reply],
264261
},
265262
});
266-
// eslint-disable-next-line jest/prefer-called-with
267-
expect(MessageActionsMock).not.toHaveBeenCalled();
263+
264+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).not.toBeInTheDocument();
268265
});
269266

270267
it('should show actions box for message outside thread with single action "reply" if custom actions available', async () => {
271-
await renderMessageOptions({
268+
const { queryByTestId } = await renderMessageOptions({
272269
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
273270
customMessageProps: {
274271
customMessageActions: [MESSAGE_ACTIONS.reply],
275272
messageActions: [MESSAGE_ACTIONS.reply],
276273
},
277274
});
278-
// eslint-disable-next-line jest/prefer-called-with
279-
expect(MessageActionsMock).toHaveBeenCalled();
275+
276+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).toBeInTheDocument();
280277
});
281278

282279
it('should not show actions box for message outside thread with two actions "react" & "reply"', async () => {
283280
const actions = [MESSAGE_ACTIONS.react, MESSAGE_ACTIONS.reply];
284-
await renderMessageOptions({
281+
const { queryByTestId } = await renderMessageOptions({
285282
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
286283
customMessageProps: {
287284
messageActions: actions,
288285
},
289286
});
290-
// eslint-disable-next-line jest/prefer-called-with
291-
expect(MessageActionsMock).not.toHaveBeenCalled();
287+
288+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).not.toBeInTheDocument();
292289
});
293290

294291
it('should show actions box for message outside thread with single actions "react" & "reply" if custom actions available', async () => {
295292
const actions = [MESSAGE_ACTIONS.react, MESSAGE_ACTIONS.reply];
296-
await renderMessageOptions({
293+
const { queryByTestId } = await renderMessageOptions({
297294
channelStateOpts: { channelCapabilities: minimumCapabilitiesToRenderMessageActions },
298295
customMessageProps: {
299296
customMessageActions: actions,
300297
messageActions: actions,
301298
},
302299
});
303-
// eslint-disable-next-line jest/prefer-called-with
304-
expect(MessageActionsMock).toHaveBeenCalled();
300+
301+
expect(queryByTestId(MESSAGE_ACTIONS_TEST_ID)).toBeInTheDocument();
305302
});
306303
});

src/components/Message/utils.tsx

+42-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import type { TFunction } from 'i18next';
55
import type { MessageResponse, Mute, StreamChat, UserResponse } from 'stream-chat';
66
import type { PinPermissions } from './hooks';
77
import type { MessageProps } from './types';
8-
import type { MessageContextValue, StreamMessage } from '../../context';
8+
import type {
9+
ComponentContextValue,
10+
CustomMessageActions,
11+
MessageContextValue,
12+
StreamMessage,
13+
} from '../../context';
914
import type { DefaultStreamChatGenerics } from '../../types/types';
1015

1116
/**
@@ -206,26 +211,55 @@ export const ACTIONS_NOT_WORKING_IN_THREAD = [
206211
MESSAGE_ACTIONS.markUnread,
207212
];
208213

214+
/**
215+
* @deprecated use `shouldRenderMessageActions` instead
216+
*/
209217
export const showMessageActionsBox = (
210218
actions: MessageActionsArray,
211219
inThread?: boolean | undefined,
212-
) => {
213-
if (actions.length === 0) {
214-
return false;
215-
}
220+
) => shouldRenderMessageActions({ inThread, messageActions: actions });
221+
222+
export const shouldRenderMessageActions = <
223+
SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
224+
>({
225+
customMessageActions,
226+
CustomMessageActionsList,
227+
inThread,
228+
messageActions,
229+
}: {
230+
messageActions: MessageActionsArray;
231+
customMessageActions?: CustomMessageActions<SCG>;
232+
CustomMessageActionsList?: ComponentContextValue<SCG>['CustomMessageActionsList'];
233+
inThread?: boolean;
234+
}) => {
235+
if (
236+
typeof CustomMessageActionsList !== 'undefined' ||
237+
typeof customMessageActions !== 'undefined'
238+
)
239+
return true;
240+
241+
if (!messageActions.length) return false;
216242

217243
if (
218244
inThread &&
219-
actions.filter((action) => !ACTIONS_NOT_WORKING_IN_THREAD.includes(action)).length === 0
245+
messageActions.filter((action) => !ACTIONS_NOT_WORKING_IN_THREAD.includes(action)).length === 0
220246
) {
221247
return false;
222248
}
223249

224-
if (actions.length === 1 && (actions.includes('react') || actions.includes('reply'))) {
250+
if (
251+
messageActions.length === 1 &&
252+
(messageActions.includes(MESSAGE_ACTIONS.react) ||
253+
messageActions.includes(MESSAGE_ACTIONS.reply))
254+
) {
225255
return false;
226256
}
227257

228-
if (actions.length === 2 && actions.includes('react') && actions.includes('reply')) {
258+
if (
259+
messageActions.length === 2 &&
260+
messageActions.includes(MESSAGE_ACTIONS.react) &&
261+
messageActions.includes(MESSAGE_ACTIONS.reply)
262+
) {
229263
return false;
230264
}
231265

src/components/MessageActions/MessageActions.tsx

+22-8
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ import React, {
66
useRef,
77
useState,
88
} from 'react';
9+
import clsx from 'clsx';
910

1011
import { MessageActionsBox } from './MessageActionsBox';
1112

1213
import { ActionsIcon as DefaultActionsIcon } from '../Message/icons';
13-
import { isUserMuted } from '../Message/utils';
14+
import { isUserMuted, shouldRenderMessageActions } from '../Message/utils';
1415

1516
import { useChatContext } from '../../context/ChatContext';
1617
import { MessageContextValue, useMessageContext } from '../../context/MessageContext';
1718

1819
import type { DefaultStreamChatGenerics, IconProps } from '../../types/types';
1920
import { useMessageActionsBoxPopper } from './hooks';
20-
import { useTranslationContext } from '../../context';
21+
import { useComponentContext, useTranslationContext } from '../../context';
2122

2223
type MessageContextPropsToPick =
2324
| 'getMessageActions'
@@ -64,6 +65,7 @@ export const MessageActions = <
6465
} = props;
6566

6667
const { mutes } = useChatContext<StreamChatGenerics>('MessageActions');
68+
6769
const {
6870
customMessageActions,
6971
getMessageActions: contextGetMessageActions,
@@ -75,8 +77,11 @@ export const MessageActions = <
7577
isMyMessage,
7678
message: contextMessage,
7779
setEditingState,
80+
threadList,
7881
} = useMessageContext<StreamChatGenerics>('MessageActions');
7982

83+
const { CustomMessageActionsList } = useComponentContext<StreamChatGenerics>('MessageActions');
84+
8085
const { t } = useTranslationContext('MessageActions');
8186

8287
const getMessageActions = propGetMessageActions || contextGetMessageActions;
@@ -92,13 +97,21 @@ export const MessageActions = <
9297

9398
const isMuted = useCallback(() => isUserMuted(message, mutes), [message, mutes]);
9499

100+
const messageActions = getMessageActions();
101+
102+
const renderMessageActions = shouldRenderMessageActions<StreamChatGenerics>({
103+
customMessageActions,
104+
CustomMessageActionsList,
105+
inThread: threadList,
106+
messageActions,
107+
});
108+
95109
const hideOptions = useCallback((event: MouseEvent | KeyboardEvent) => {
96110
if (event instanceof KeyboardEvent && event.key !== 'Escape') {
97111
return;
98112
}
99113
setActionsBoxOpen(false);
100114
}, []);
101-
const messageActions = getMessageActions();
102115
const messageDeletedAt = !!message?.deleted_at;
103116

104117
useEffect(() => {
@@ -133,7 +146,7 @@ export const MessageActions = <
133146
referenceElement: actionsBoxButtonRef.current,
134147
});
135148

136-
if (!messageActions.length && !customMessageActions) return null;
149+
if (!renderMessageActions) return null;
137150

138151
return (
139152
<MessageActionsWrapper
@@ -178,10 +191,11 @@ export type MessageActionsWrapperProps = {
178191
const MessageActionsWrapper = (props: PropsWithChildren<MessageActionsWrapperProps>) => {
179192
const { children, customWrapperClass, inline, setActionsBoxOpen } = props;
180193

181-
const defaultWrapperClass = `
182-
str-chat__message-simple__actions__action
183-
str-chat__message-simple__actions__action--options
184-
str-chat__message-actions-container`;
194+
const defaultWrapperClass = clsx(
195+
'str-chat__message-simple__actions__action',
196+
'str-chat__message-simple__actions__action--options',
197+
'str-chat__message-actions-container',
198+
);
185199

186200
const wrapperClass = customWrapperClass || defaultWrapperClass;
187201

0 commit comments

Comments
 (0)