Skip to content

Commit eb6ab99

Browse files
committed
Merge branch 'release-v11' into feat/dialogs-manager
2 parents 9751e15 + 153bd75 commit eb6ab99

File tree

6 files changed

+109
-67
lines changed

6 files changed

+109
-67
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [11.23.9](https://github.com/GetStream/stream-chat-react/compare/v11.23.8...v11.23.9) (2024-09-04)
2+
3+
4+
### Bug Fixes
5+
6+
* MessageActions adjustments ([#2472](https://github.com/GetStream/stream-chat-react/issues/2472)) ([fbd1b6f](https://github.com/GetStream/stream-chat-react/commit/fbd1b6fd0843d94f250de4158b144ee65eb9bdaf))
7+
18
## [11.23.8](https://github.com/GetStream/stream-chat-react/compare/v11.23.7...v11.23.8) (2024-08-28)
29

310

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

+24-9
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { MessageActionsBox } from './MessageActionsBox';
55

66
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
77
import { ActionsIcon as DefaultActionsIcon } from '../Message/icons';
8-
import { isUserMuted } from '../Message/utils';
8+
import { isUserMuted, shouldRenderMessageActions } from '../Message/utils';
9+
910
import { useChatContext } from '../../context/ChatContext';
1011
import { MessageContextValue, useMessageContext } from '../../context/MessageContext';
11-
import { useTranslationContext } from '../../context';
12+
import { useComponentContext, useTranslationContext } from '../../context';
1213

1314
import type { DefaultStreamChatGenerics, IconProps } from '../../types/types';
1415

@@ -57,6 +58,7 @@ export const MessageActions = <
5758
} = props;
5859

5960
const { mutes } = useChatContext<StreamChatGenerics>('MessageActions');
61+
6062
const {
6163
customMessageActions,
6264
getMessageActions: contextGetMessageActions,
@@ -68,8 +70,11 @@ export const MessageActions = <
6870
isMyMessage,
6971
message: contextMessage,
7072
setEditingState,
73+
threadList,
7174
} = useMessageContext<StreamChatGenerics>('MessageActions');
7275

76+
const { CustomMessageActionsList } = useComponentContext<StreamChatGenerics>('MessageActions');
77+
7378
const { t } = useTranslationContext('MessageActions');
7479

7580
const getMessageActions = propGetMessageActions || contextGetMessageActions;
@@ -87,6 +92,17 @@ export const MessageActions = <
8792
const dialog = useDialog({ id: dialogId });
8893
const dialogIsOpen = useDialogIsOpen(dialogId);
8994

95+
const messageActions = getMessageActions();
96+
97+
const renderMessageActions = shouldRenderMessageActions<StreamChatGenerics>({
98+
customMessageActions,
99+
CustomMessageActionsList,
100+
inThread: threadList,
101+
messageActions,
102+
});
103+
104+
const messageDeletedAt = !!message?.deleted_at;
105+
90106
const hideOptions = useCallback(
91107
(event: MouseEvent | KeyboardEvent) => {
92108
if (event instanceof KeyboardEvent && event.key !== 'Escape') {
@@ -96,8 +112,6 @@ export const MessageActions = <
96112
},
97113
[dialog],
98114
);
99-
const messageActions = getMessageActions();
100-
const messageDeletedAt = !!message?.deleted_at;
101115

102116
useEffect(() => {
103117
if (messageWrapperRef?.current) {
@@ -123,7 +137,7 @@ export const MessageActions = <
123137

124138
const actionsBoxButtonRef = useRef<ElementRef<'button'>>(null);
125139

126-
if (!messageActions.length && !customMessageActions) return null;
140+
if (!renderMessageActions) return null;
127141

128142
return (
129143
<MessageActionsWrapper
@@ -172,10 +186,11 @@ export type MessageActionsWrapperProps = {
172186
const MessageActionsWrapper = (props: PropsWithChildren<MessageActionsWrapperProps>) => {
173187
const { children, customWrapperClass, inline, toggleOpen } = props;
174188

175-
const defaultWrapperClass = `
176-
str-chat__message-simple__actions__action
177-
str-chat__message-simple__actions__action--options
178-
str-chat__message-actions-container`;
189+
const defaultWrapperClass = clsx(
190+
'str-chat__message-simple__actions__action',
191+
'str-chat__message-simple__actions__action--options',
192+
'str-chat__message-actions-container',
193+
);
179194

180195
const wrapperProps = {
181196
className: customWrapperClass || defaultWrapperClass,

0 commit comments

Comments
 (0)