diff --git a/docusaurus/docs/React/components/contexts/component-context.mdx b/docusaurus/docs/React/components/contexts/component-context.mdx index e96b9c94f..5a4de7065 100644 --- a/docusaurus/docs/React/components/contexts/component-context.mdx +++ b/docusaurus/docs/React/components/contexts/component-context.mdx @@ -71,18 +71,18 @@ Custom UI component to display a user's avatar. ### BaseImage -Custom UI component to display image resp. a fallback in case of load error, in `` element. The default resp. custom (from `ComponentContext`) `BaseImage` component is rendered by: +Custom UI component to display image resp. a fallback in case of load error, in `` element. The default resp. custom (from `ComponentContext`) `BaseImage` component is rendered by: -- - single image attachment in message list -- - group of image attachments in message list -- - image uploads preview in message input (composer) +- - single image attachment in message list +- - group of image attachments in message list +- - image uploads preview in message input (composer) The `BaseImage` component accepts the same props as `` element. 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 `` element as a CSS mask targeting attached `str-chat__base-image--load-failed` class. -| Type | Default | -|-----------|-----------------------------------------------------------------------| +| Type | Default | +| --------- | ----------------------------------------------------------------- | | component | | ### CooldownTimer @@ -93,6 +93,14 @@ Custom UI component to display the slow mode cooldown timer. | --------- | ------------------------------------------------------------------------------ | | component | | +### CustomMessageActionsList + +Custom UI component to render set of buttons to be displayed in the . + +| Type | Default | +| --------- | ------------------------------------------------------------------------------------------------------- | +| component | | + ### DateSeparator Custom UI component for date separators. diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index a91c6a0f3..4a5eecb3e 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -112,6 +112,8 @@ type ChannelPropsForwardedToComponentContext< BaseImage?: ComponentContextValue['BaseImage']; /** 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) */ CooldownTimer?: ComponentContextValue['CooldownTimer']; + /** 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) */ + CustomMessageActionsList?: ComponentContextValue['CustomMessageActionsList']; /** 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) */ DateSeparator?: ComponentContextValue['DateSeparator']; /** 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 = < Avatar: props.Avatar, BaseImage: props.BaseImage, CooldownTimer: props.CooldownTimer, + CustomMessageActionsList: props.CustomMessageActionsList, DateSeparator: props.DateSeparator, EditMessageInput: props.EditMessageInput, EmojiPicker: props.EmojiPicker, diff --git a/src/components/Channel/__tests__/Channel.test.js b/src/components/Channel/__tests__/Channel.test.js index c2613c516..f067d03ff 100644 --- a/src/components/Channel/__tests__/Channel.test.js +++ b/src/components/Channel/__tests__/Channel.test.js @@ -26,6 +26,8 @@ import { } from '../../../mock-builders'; import { MessageList } from '../../MessageList'; import { Thread } from '../../Thread'; +import { MessageProvider } from '../../../context'; +import { MessageActionsBox } from '../../MessageActions'; jest.mock('../../Loading', () => ({ LoadingErrorIndicator: jest.fn(() =>
), @@ -1530,4 +1532,33 @@ describe('Channel', () => { }); }); }); + + describe('Custom Components', () => { + it('should render CustomMessageActionsList if provided', async () => { + const { channel, chatClient } = await initClient(); + const CustomMessageActionsList = jest + .fn() + .mockImplementation(() => 'CustomMessageActionsList'); + + const messageContextValue = { + message: generateMessage(), + messageListRect: {}, + }; + + renderComponent({ + channel, + chatClient, + children: ( + + [])} /> + + ), + CustomMessageActionsList, + }); + + await waitFor(() => { + expect(CustomMessageActionsList).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/src/components/MessageActions/CustomMessageActionsList.tsx b/src/components/MessageActions/CustomMessageActionsList.tsx new file mode 100644 index 000000000..ca455fa0f --- /dev/null +++ b/src/components/MessageActions/CustomMessageActionsList.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import { CustomMessageActions } from '../../context/MessageContext'; + +import type { StreamMessage } from '../../context/ChannelStateContext'; +import type { DefaultStreamChatGenerics } from '../../types/types'; + +export type CustomMessageActionsListProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +> = { + message: StreamMessage; + customMessageActions?: CustomMessageActions; +}; + +/** + * @deprecated alias for `CustomMessageActionsListProps`, will be removed in the next major release + */ +export type CustomMessageActionsType< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +> = CustomMessageActionsListProps; + +export const CustomMessageActionsList = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>( + props: CustomMessageActionsListProps, +) => { + const { customMessageActions, message } = props; + + if (!customMessageActions) return null; + + const customActionsArray = Object.keys(customMessageActions); + + return ( + <> + {customActionsArray.map((customAction) => { + const customHandler = customMessageActions[customAction]; + + return ( + + ); + })} + + ); +}; diff --git a/src/components/MessageActions/MessageActionsBox.tsx b/src/components/MessageActions/MessageActionsBox.tsx index 0f52a4035..6cb3aee17 100644 --- a/src/components/MessageActions/MessageActionsBox.tsx +++ b/src/components/MessageActions/MessageActionsBox.tsx @@ -3,53 +3,17 @@ import clsx from 'clsx'; import { MESSAGE_ACTIONS } from '../Message/utils'; -import { useChannelActionContext } from '../../context/ChannelActionContext'; import { - CustomMessageActions, MessageContextValue, + useChannelActionContext, + useComponentContext, useMessageContext, -} from '../../context/MessageContext'; -import { useTranslationContext } from '../../context/TranslationContext'; - -import type { StreamMessage } from '../../context/ChannelStateContext'; + useTranslationContext, +} from '../../context'; import type { DefaultStreamChatGenerics } from '../../types/types'; -export type CustomMessageActionsType< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics -> = { - customMessageActions: CustomMessageActions; - message: StreamMessage; -}; - -const CustomMessageActionsList = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics ->( - props: CustomMessageActionsType, -) => { - const { customMessageActions, message } = props; - const customActionsArray = Object.keys(customMessageActions); - - return ( - <> - {customActionsArray.map((customAction) => { - const customHandler = customMessageActions[customAction]; - - return ( - - ); - })} - - ); -}; +import { CustomMessageActionsList as DefaultCustomMessageActionsList } from './CustomMessageActionsList'; type PropsDrilledToMessageActionsBox = | 'getMessageActions' @@ -84,6 +48,9 @@ const UnMemoizedMessageActionsBox = < open = false, } = props; + const { + CustomMessageActionsList = DefaultCustomMessageActionsList, + } = useComponentContext('MessageActionsBox'); const { setQuotedMessage } = useChannelActionContext('MessageActionsBox'); const { customMessageActions, message, messageListRect } = useMessageContext( 'MessageActionsBox', @@ -139,9 +106,7 @@ const UnMemoizedMessageActionsBox = < return (
- {customMessageActions && ( - - )} + {messageActions.indexOf(MESSAGE_ACTIONS.quote) > -1 && ( , + , + ] + `); + }); + + it('should allow clicking custom action', () => { + const message = { id: 'mId' }; + + const actions = { + key0: jest.fn(), + }; + + const { getByText } = render( + , + ); + + const button = getByText('key0'); + + const event = new Event('click', { bubbles: true }); + + act(() => { + fireEvent(button, event); + }); + + expect(actions.key0).toHaveBeenCalledWith(message, expect.any(Object)); // replacing SyntheticEvent with any(Object) + }); +}); diff --git a/src/components/MessageActions/index.ts b/src/components/MessageActions/index.ts index 7b3d7609d..1790160d9 100644 --- a/src/components/MessageActions/index.ts +++ b/src/components/MessageActions/index.ts @@ -1,2 +1,3 @@ export * from './MessageActions'; export * from './MessageActionsBox'; +export * from './CustomMessageActionsList'; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 6633476b2..a062c15e5 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -32,7 +32,11 @@ import type { ThreadHeaderProps } from '../components/Thread/ThreadHeader'; import type { TypingIndicatorProps } from '../components/TypingIndicator/TypingIndicator'; import type { CustomTrigger, DefaultStreamChatGenerics, UnknownType } from '../types/types'; -import type { BaseImageProps, CooldownTimerProps } from '../components'; +import type { + BaseImageProps, + CooldownTimerProps, + CustomMessageActionsListProps, +} from '../components'; import type { LinkPreviewListProps } from '../components/MessageInput/LinkPreviewList'; import type { ReactionOptions } from '../components/Reactions/reactionOptions'; @@ -50,6 +54,7 @@ export type ComponentContextValue< Avatar?: React.ComponentType>; BaseImage?: React.ComponentType; CooldownTimer?: React.ComponentType; + CustomMessageActionsList?: React.ComponentType>; DateSeparator?: React.ComponentType; EditMessageInput?: React.ComponentType>; EmojiPicker?: React.ComponentType;