Skip to content

Commit 2c6e56c

Browse files
feat: add CustomMessageActionsList to ComponentContext (#2226)
### 🎯 Goal Adds CustomMessageActionsList to ComponentContext for easier message actions adjustment.
1 parent 61213d7 commit 2c6e56c

File tree

8 files changed

+179
-51
lines changed

8 files changed

+179
-51
lines changed

β€Ždocusaurus/docs/React/components/contexts/component-context.mdxβ€Ž

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,18 @@ Custom UI component to display a user's avatar.
7171

7272
### BaseImage
7373

74-
Custom UI component to display image resp. a fallback in case of load error, in `<img/>` element. The default resp. custom (from `ComponentContext`) `BaseImage` component is rendered by:
74+
Custom UI component to display image resp. a fallback in case of load error, in `<img/>` element. The default resp. custom (from `ComponentContext`) `BaseImage` component is rendered by:
7575

76-
- <GHComponentLink text='Image' path='/Gallery/Image.tsx'/> - single image attachment in message list
77-
- <GHComponentLink text='Gallery' path='/Gallery/Gallery.tsx'/> - group of image attachments in message list
78-
- <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx'/> - image uploads preview in message input (composer)
76+
- <GHComponentLink text='Image' path='/Gallery/Image.tsx' /> - single image attachment in message list
77+
- <GHComponentLink text='Gallery' path='/Gallery/Gallery.tsx' /> - group of image attachments in message list
78+
- <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx' /> - image uploads preview in message input (composer)
7979

8080
The `BaseImage` component accepts the same props as `<img/>` element.
8181

8282
The [default `BaseImage` component](../../utility-components/base-image) tries to load and display an image and if the load fails, then an SVG image fallback is applied to the `<img/>` element as a CSS mask targeting attached `str-chat__base-image--load-failed` class.
8383

84-
| Type | Default |
85-
|-----------|-----------------------------------------------------------------------|
84+
| Type | Default |
85+
| --------- | ----------------------------------------------------------------- |
8686
| component | <GHComponentLink text='BaseImage' path='/Gallery/BaseImage.tsx'/> |
8787

8888
### CooldownTimer
@@ -93,6 +93,14 @@ Custom UI component to display the slow mode cooldown timer.
9393
| --------- | ------------------------------------------------------------------------------ |
9494
| component | <GHComponentLink text='CooldownTimer' path='/MessageInput/CooldownTimer.tsx'/> |
9595

96+
### CustomMessageActionsList
97+
98+
Custom UI component to render set of buttons to be displayed in the <GHComponentLink text='MessageActionsBox' path='/MessageActions/MessageActionsBox.tsx' />.
99+
100+
| Type | Default |
101+
| --------- | ------------------------------------------------------------------------------------------------------- |
102+
| component | <GHComponentLink text='CustomMessageActionsList' path='/MessageActions/CustomMessageActionsList.tsx' /> |
103+
96104
### DateSeparator
97105

98106
Custom UI component for date separators.

β€Žsrc/components/Channel/Channel.tsxβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ type ChannelPropsForwardedToComponentContext<
112112
BaseImage?: ComponentContextValue<StreamChatGenerics>['BaseImage'];
113113
/** Custom UI component to display the slow mode cooldown timer, defaults to and accepts same props as: [CooldownTimer](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/CooldownTimer.tsx) */
114114
CooldownTimer?: ComponentContextValue<StreamChatGenerics>['CooldownTimer'];
115+
/** Custom UI component to render set of buttons to be displayed in the MessageActionsBox, defaults to and accepts same props as: [CustomMessageActionsList](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageActions/CustomMessageActionsList.tsx) */
116+
CustomMessageActionsList?: ComponentContextValue<StreamChatGenerics>['CustomMessageActionsList'];
115117
/** Custom UI component for date separators, defaults to and accepts same props as: [DateSeparator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/DateSeparator.tsx) */
116118
DateSeparator?: ComponentContextValue<StreamChatGenerics>['DateSeparator'];
117119
/** Custom UI component to override default edit message input, defaults to and accepts same props as: [EditMessageForm](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/EditMessageForm.tsx) */
@@ -1008,6 +1010,7 @@ const ChannelInner = <
10081010
Avatar: props.Avatar,
10091011
BaseImage: props.BaseImage,
10101012
CooldownTimer: props.CooldownTimer,
1013+
CustomMessageActionsList: props.CustomMessageActionsList,
10111014
DateSeparator: props.DateSeparator,
10121015
EditMessageInput: props.EditMessageInput,
10131016
EmojiPicker: props.EmojiPicker,

β€Žsrc/components/Channel/__tests__/Channel.test.jsβ€Ž

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
} from '../../../mock-builders';
2727
import { MessageList } from '../../MessageList';
2828
import { Thread } from '../../Thread';
29+
import { MessageProvider } from '../../../context';
30+
import { MessageActionsBox } from '../../MessageActions';
2931

3032
jest.mock('../../Loading', () => ({
3133
LoadingErrorIndicator: jest.fn(() => <div />),
@@ -1530,4 +1532,33 @@ describe('Channel', () => {
15301532
});
15311533
});
15321534
});
1535+
1536+
describe('Custom Components', () => {
1537+
it('should render CustomMessageActionsList if provided', async () => {
1538+
const { channel, chatClient } = await initClient();
1539+
const CustomMessageActionsList = jest
1540+
.fn()
1541+
.mockImplementation(() => 'CustomMessageActionsList');
1542+
1543+
const messageContextValue = {
1544+
message: generateMessage(),
1545+
messageListRect: {},
1546+
};
1547+
1548+
renderComponent({
1549+
channel,
1550+
chatClient,
1551+
children: (
1552+
<MessageProvider value={{ ...messageContextValue }}>
1553+
<MessageActionsBox getMessageActions={jest.fn(() => [])} />
1554+
</MessageProvider>
1555+
),
1556+
CustomMessageActionsList,
1557+
});
1558+
1559+
await waitFor(() => {
1560+
expect(CustomMessageActionsList).toHaveBeenCalledTimes(1);
1561+
});
1562+
});
1563+
});
15331564
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
3+
import { CustomMessageActions } from '../../context/MessageContext';
4+
5+
import type { StreamMessage } from '../../context/ChannelStateContext';
6+
import type { DefaultStreamChatGenerics } from '../../types/types';
7+
8+
export type CustomMessageActionsListProps<
9+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
10+
> = {
11+
message: StreamMessage<StreamChatGenerics>;
12+
customMessageActions?: CustomMessageActions<StreamChatGenerics>;
13+
};
14+
15+
/**
16+
* @deprecated alias for `CustomMessageActionsListProps`, will be removed in the next major release
17+
*/
18+
export type CustomMessageActionsType<
19+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
20+
> = CustomMessageActionsListProps<StreamChatGenerics>;
21+
22+
export const CustomMessageActionsList = <
23+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
24+
>(
25+
props: CustomMessageActionsListProps<StreamChatGenerics>,
26+
) => {
27+
const { customMessageActions, message } = props;
28+
29+
if (!customMessageActions) return null;
30+
31+
const customActionsArray = Object.keys(customMessageActions);
32+
33+
return (
34+
<>
35+
{customActionsArray.map((customAction) => {
36+
const customHandler = customMessageActions[customAction];
37+
38+
return (
39+
<button
40+
aria-selected='false'
41+
className='str-chat__message-actions-list-item str-chat__message-actions-list-item-button'
42+
key={customAction}
43+
onClick={(event) => customHandler(message, event)}
44+
role='option'
45+
>
46+
{customAction}
47+
</button>
48+
);
49+
})}
50+
</>
51+
);
52+
};

β€Žsrc/components/MessageActions/MessageActionsBox.tsxβ€Ž

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,17 @@ import clsx from 'clsx';
33

44
import { MESSAGE_ACTIONS } from '../Message/utils';
55

6-
import { useChannelActionContext } from '../../context/ChannelActionContext';
76
import {
8-
CustomMessageActions,
97
MessageContextValue,
8+
useChannelActionContext,
9+
useComponentContext,
1010
useMessageContext,
11-
} from '../../context/MessageContext';
12-
import { useTranslationContext } from '../../context/TranslationContext';
13-
14-
import type { StreamMessage } from '../../context/ChannelStateContext';
11+
useTranslationContext,
12+
} from '../../context';
1513

1614
import type { DefaultStreamChatGenerics } from '../../types/types';
1715

18-
export type CustomMessageActionsType<
19-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
20-
> = {
21-
customMessageActions: CustomMessageActions<StreamChatGenerics>;
22-
message: StreamMessage<StreamChatGenerics>;
23-
};
24-
25-
const CustomMessageActionsList = <
26-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
27-
>(
28-
props: CustomMessageActionsType<StreamChatGenerics>,
29-
) => {
30-
const { customMessageActions, message } = props;
31-
const customActionsArray = Object.keys(customMessageActions);
32-
33-
return (
34-
<>
35-
{customActionsArray.map((customAction) => {
36-
const customHandler = customMessageActions[customAction];
37-
38-
return (
39-
<button
40-
aria-selected='false'
41-
className='str-chat__message-actions-list-item str-chat__message-actions-list-item-button'
42-
key={customAction}
43-
onClick={(event) => customHandler(message, event)}
44-
role='option'
45-
>
46-
{customAction}
47-
</button>
48-
);
49-
})}
50-
</>
51-
);
52-
};
16+
import { CustomMessageActionsList as DefaultCustomMessageActionsList } from './CustomMessageActionsList';
5317

5418
type PropsDrilledToMessageActionsBox =
5519
| 'getMessageActions'
@@ -84,6 +48,9 @@ const UnMemoizedMessageActionsBox = <
8448
open = false,
8549
} = props;
8650

51+
const {
52+
CustomMessageActionsList = DefaultCustomMessageActionsList,
53+
} = useComponentContext<StreamChatGenerics>('MessageActionsBox');
8754
const { setQuotedMessage } = useChannelActionContext<StreamChatGenerics>('MessageActionsBox');
8855
const { customMessageActions, message, messageListRect } = useMessageContext<StreamChatGenerics>(
8956
'MessageActionsBox',
@@ -139,9 +106,7 @@ const UnMemoizedMessageActionsBox = <
139106
return (
140107
<div className={rootClassName} data-testid='message-actions-box' ref={checkIfReverse}>
141108
<div aria-label='Message Options' className='str-chat__message-actions-list' role='listbox'>
142-
{customMessageActions && (
143-
<CustomMessageActionsList customMessageActions={customMessageActions} message={message} />
144-
)}
109+
<CustomMessageActionsList customMessageActions={customMessageActions} message={message} />
145110
{messageActions.indexOf(MESSAGE_ACTIONS.quote) > -1 && (
146111
<button
147112
aria-selected='false'
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import { fireEvent, render } from '@testing-library/react';
3+
import renderer from 'react-test-renderer';
4+
import { CustomMessageActionsList } from '../CustomMessageActionsList';
5+
import { act } from 'react-dom/test-utils';
6+
7+
describe('CustomMessageActionsList', () => {
8+
it('should render custom list of actions', () => {
9+
const message = { id: 'mId' };
10+
11+
const actions = {
12+
key0: () => {},
13+
key1: () => {},
14+
};
15+
16+
const tree = renderer.create(
17+
<CustomMessageActionsList customMessageActions={actions} message={message} />,
18+
);
19+
20+
expect(tree.toJSON()).toMatchInlineSnapshot(`
21+
Array [
22+
<button
23+
aria-selected="false"
24+
className="str-chat__message-actions-list-item str-chat__message-actions-list-item-button"
25+
onClick={[Function]}
26+
role="option"
27+
>
28+
key0
29+
</button>,
30+
<button
31+
aria-selected="false"
32+
className="str-chat__message-actions-list-item str-chat__message-actions-list-item-button"
33+
onClick={[Function]}
34+
role="option"
35+
>
36+
key1
37+
</button>,
38+
]
39+
`);
40+
});
41+
42+
it('should allow clicking custom action', () => {
43+
const message = { id: 'mId' };
44+
45+
const actions = {
46+
key0: jest.fn(),
47+
};
48+
49+
const { getByText } = render(
50+
<CustomMessageActionsList customMessageActions={actions} message={message} />,
51+
);
52+
53+
const button = getByText('key0');
54+
55+
const event = new Event('click', { bubbles: true });
56+
57+
act(() => {
58+
fireEvent(button, event);
59+
});
60+
61+
expect(actions.key0).toHaveBeenCalledWith(message, expect.any(Object)); // replacing SyntheticEvent with any(Object)
62+
});
63+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './MessageActions';
22
export * from './MessageActionsBox';
3+
export * from './CustomMessageActionsList';

β€Žsrc/context/ComponentContext.tsxβ€Ž

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ import type { ThreadHeaderProps } from '../components/Thread/ThreadHeader';
3232
import type { TypingIndicatorProps } from '../components/TypingIndicator/TypingIndicator';
3333

3434
import type { CustomTrigger, DefaultStreamChatGenerics, UnknownType } from '../types/types';
35-
import type { BaseImageProps, CooldownTimerProps } from '../components';
35+
import type {
36+
BaseImageProps,
37+
CooldownTimerProps,
38+
CustomMessageActionsListProps,
39+
} from '../components';
3640
import type { LinkPreviewListProps } from '../components/MessageInput/LinkPreviewList';
3741
import type { ReactionOptions } from '../components/Reactions/reactionOptions';
3842

@@ -50,6 +54,7 @@ export type ComponentContextValue<
5054
Avatar?: React.ComponentType<AvatarProps<StreamChatGenerics>>;
5155
BaseImage?: React.ComponentType<BaseImageProps>;
5256
CooldownTimer?: React.ComponentType<CooldownTimerProps>;
57+
CustomMessageActionsList?: React.ComponentType<CustomMessageActionsListProps<StreamChatGenerics>>;
5358
DateSeparator?: React.ComponentType<DateSeparatorProps>;
5459
EditMessageInput?: React.ComponentType<MessageInputProps<StreamChatGenerics>>;
5560
EmojiPicker?: React.ComponentType;

0 commit comments

Comments
Β (0)