From c15d4647a1eee7e6b2f78526795c1e6ec9b9522b Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 16 Nov 2023 13:27:31 +0100 Subject: [PATCH] feat: allow to configure channel query options --- .../components/core-components/channel.mdx | 33 ++++ src/components/Channel/Channel.tsx | 181 ++++++++++-------- .../Channel/__tests__/Channel.test.js | 23 ++- src/utils/getChannel.ts | 12 +- 4 files changed, 166 insertions(+), 83 deletions(-) diff --git a/docusaurus/docs/React/components/core-components/channel.mdx b/docusaurus/docs/React/components/core-components/channel.mdx index 4ff40420c..a858655bb 100644 --- a/docusaurus/docs/React/components/core-components/channel.mdx +++ b/docusaurus/docs/React/components/core-components/channel.mdx @@ -176,6 +176,39 @@ Custom UI component to display a user's avatar. | --------- | ---------------------------------------------------------- | | component | | +### channelQueryOptions + +Optional configuration parameters used for the initial channel query. Applied only if the value of `channel.initialized` is false. If the channel instance has already been initialized (channel has been queried), then the channel query will be skipped and channelQueryOptions will not be applied. + +In the example below, we specify, that the first page of messages when a channel is queried should have 20 messages (the default is 100). Note that the `channel` prop has to be passed along `channelQueryOptions`. + +```tsx +import {ChannelQueryOptions} from "stream-chat"; +import {Channel, useChatContext} from "stream-chat-react"; + +const channelQueryOptions: ChannelQueryOptions = { + messages: { limit: 20 }, +}; + +type ChannelRendererProps = { + id: string; + type: string; +}; + +const ChannelRenderer = ({id, type}: ChannelRendererProps) => { + const { client } = useChatContext(); + return ( + + {/* Channel children */} + + ); +} +``` + +| Type | +|-----------------------| +| `ChannelQueryOptions` | + ### CooldownTimer Custom UI component to display the slow mode cooldown timer. diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 3a0e7915f..81f9d0aa0 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -14,6 +14,7 @@ import throttle from 'lodash.throttle'; import { ChannelAPIResponse, ChannelMemberResponse, + ChannelQueryOptions, ChannelState, Event, logChatPromiseExecution, @@ -93,14 +94,9 @@ import { } from '../Attachment/attachment-sizing'; import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews'; -export type ChannelProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, - V extends CustomTrigger = CustomTrigger +type ChannelPropsForwardedToComponentContext< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics > = { - /** List of accepted file types */ - acceptedFiles?: string[]; - /** Custom handler function that runs when the active channel has unread messages (i.e., when chat is running on a separate browser tab) */ - activeUnreadHandler?: (unread: number, documentTitle: string) => void; /** Custom UI component to display a message attachment, defaults to and accepts same props as: [Attachment](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Attachment/Attachment.tsx) */ Attachment?: ComponentContextValue['Attachment']; /** Custom UI component to display a attachment previews in MessageInput, defaults to and accepts same props as: [Attachment](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/AttachmentPreviewList.tsx) */ @@ -113,74 +109,22 @@ export type ChannelProps< AutocompleteSuggestionList?: ComponentContextValue['AutocompleteSuggestionList']; /** UI component to display a user's avatar, defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) */ Avatar?: ComponentContextValue['Avatar']; - /** The connected and active channel */ - channel?: StreamChannel; /** 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/hooks/useCooldownTimer.tsx) */ CooldownTimer?: ComponentContextValue['CooldownTimer']; /** 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 action handler to override the default `client.deleteMessage(message.id)` function */ - doDeleteMessageRequest?: ( - message: StreamMessage, - ) => Promise>; - /** Custom action handler to override the default `channel.markRead` request function (advanced usage only) */ - doMarkReadRequest?: ( - channel: StreamChannel, - ) => Promise> | void; - /** Custom action handler to override the default `channel.sendMessage` request function (advanced usage only) */ - doSendMessageRequest?: ( - channelId: string, - message: Message, - options?: SendMessageOptions, - ) => ReturnType['sendMessage']> | void; - /** Custom action handler to override the default `client.updateMessage` request function (advanced usage only) */ - doUpdateMessageRequest?: ( - cid: string, - updatedMessage: UpdatedMessage, - options?: UpdateMessageOptions, - ) => ReturnType['updateMessage']>; - /** If true, chat users will be able to drag and drop file uploads to the entire channel window */ - dragAndDropWindow?: boolean; /** 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) */ EditMessageInput?: ComponentContextValue['EditMessageInput']; - /** Custom UI component to override default `NimbleEmoji` from `emoji-mart` */ - Emoji?: EmojiContextValue['Emoji']; - /** Custom prop to override default `facebook.json` emoji data set from `emoji-mart` */ - emojiData?: EmojiMartData; /** Custom UI component for emoji button in input, defaults to and accepts same props as: [EmojiIconSmall](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/icons.tsx) */ EmojiIcon?: ComponentContextValue['EmojiIcon']; - /** Custom UI component to override default `NimbleEmojiIndex` from `emoji-mart` */ - EmojiIndex?: EmojiContextValue['EmojiIndex']; - /** Custom UI component to override default `NimblePicker` from `emoji-mart` */ - EmojiPicker?: EmojiContextValue['EmojiPicker']; - /** Custom UI component to be shown if no active channel is set, defaults to null and skips rendering the Channel component */ - EmptyPlaceholder?: React.ReactElement; /** Custom UI component to be displayed when the `MessageList` is empty, defaults to and accepts same props as: [EmptyStateIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/EmptyStateIndicator/EmptyStateIndicator.tsx) */ EmptyStateIndicator?: ComponentContextValue['EmptyStateIndicator']; - /** - * A global flag to toggle the URL enrichment and link previews in `MessageInput` components. - * By default, the feature is disabled. Can be overridden on Thread, MessageList level through additionalMessageInputProps - * or directly on MessageInput level through urlEnrichmentConfig. - */ - enrichURLForPreview?: URLEnrichmentConfig['enrichURLForPreview']; - /** Global configuration for link preview generation in all the MessageInput components */ - enrichURLForPreviewConfig?: Omit; /** Custom UI component for file upload icon, defaults to and accepts same props as: [FileUploadIcon](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/icons.tsx) */ FileUploadIcon?: ComponentContextValue['FileUploadIcon']; /** Custom UI component to render a Giphy preview in the `VirtualizedMessageList` */ GiphyPreviewMessage?: ComponentContextValue['GiphyPreviewMessage']; - /** The giphy version to render - check the keys of the [Image Object](https://developers.giphy.com/docs/api/schema#image-object) for possible values. Uses 'fixed_height' by default */ - giphyVersion?: GiphyVersions; /** Custom UI component to render at the top of the `MessageList` */ HeaderComponent?: ComponentContextValue['HeaderComponent']; - /** A custom function to provide size configuration for image attachments */ - imageAttachmentSizeHandler?: ImageAttachmentSizeHandler; - /** - * Allows to prevent triggering the channel.watch() call when mounting the component. - * That means that no channel data from the back-end will be received neither channel WS events will be delivered to the client. - * Preventing to initialize the channel on mount allows us to postpone the channel creation to a later point in time. - */ - initializeOnMount?: boolean; /** Custom UI component handling how the message input is rendered, defaults to and accepts the same props as [MessageInputFlat](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/MessageInputFlat.tsx) */ Input?: ComponentContextValue['Input']; /** Custom component to render link previews in message input **/ @@ -189,8 +133,6 @@ export type ChannelProps< LoadingErrorIndicator?: React.ComponentType; /** Custom UI component to render while the `MessageList` is loading new messages, defaults to and accepts same props as: [LoadingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/LoadingIndicator.tsx) */ LoadingIndicator?: ComponentContextValue['LoadingIndicator']; - /** Maximum number of attachments allowed per message */ - maxNumberOfFiles?: number; /** Custom UI component to display a message in the standard `MessageList`, defaults to and accepts the same props as: [MessageSimple](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageSimple.tsx) */ Message?: ComponentContextValue['Message']; /** Custom UI component for a deleted message, defaults to and accepts same props as: [MessageDeleted](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageDeleted.tsx) */ @@ -211,14 +153,6 @@ export type ChannelProps< MessageTimestamp?: ComponentContextValue['MessageTimestamp']; /** Custom UI component for viewing message's image attachments, defaults to and accepts the same props as [ModalGallery](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/ModalGallery.tsx) */ ModalGallery?: ComponentContextValue['ModalGallery']; - /** Whether to allow multiple attachment uploads */ - multipleUploads?: boolean; - /** Custom action handler function to run on click of an @mention in a message */ - onMentionsClick?: OnMentionAction; - /** Custom action handler function to run on hover of an @mention in a message */ - onMentionsHover?: OnMentionAction; - /** If `dragAndDropWindow` prop is true, the props to pass to the MessageInput component (overrides props placed directly on MessageInput) */ - optionalMessageInputProps?: MessageInputProps; /** Custom UI component to override default pinned message indicator, defaults to and accepts same props as: [PinIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/icons.tsx) */ PinIndicator?: ComponentContextValue['PinIndicator']; /** Custom UI component to override quoted message UI on a sent message, defaults to and accepts same props as: [QuotedMessage](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/QuotedMessage.tsx) */ @@ -231,10 +165,6 @@ export type ChannelProps< ReactionsList?: ComponentContextValue['ReactionsList']; /** Custom UI component for send button, defaults to and accepts same props as: [SendButton](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/icons.tsx) */ SendButton?: ComponentContextValue['SendButton']; - /** You can turn on/off thumbnail generation for video attachments */ - shouldGenerateVideoThumbnail?: boolean; - /** If true, skips the message data string comparison used to memoize the current channel messages (helpful for channels with 1000s of messages) */ - skipMessageDataMemoization?: boolean; /** Custom UI component that displays thread's parent or other message at the top of the `MessageList`, defaults to and accepts same props as [MessageSimple](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageSimple.tsx) */ ThreadHead?: React.ComponentType>; /** Custom UI component to display the header of a `Thread`, defaults to and accepts same props as: [DefaultThreadHeader](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Thread/Thread.tsx) */ @@ -245,12 +175,99 @@ export type ChannelProps< TriggerProvider?: ComponentContextValue['TriggerProvider']; /** Custom UI component for the typing indicator, defaults to and accepts same props as: [TypingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/TypingIndicator/TypingIndicator.tsx) */ TypingIndicator?: ComponentContextValue['TypingIndicator']; - /** A custom function to provide size configuration for video attachments */ - videoAttachmentSizeHandler?: VideoAttachmentSizeHandler; /** Custom UI component to display a message in the `VirtualizedMessageList`, does not have a default implementation */ VirtualMessage?: ComponentContextValue['VirtualMessage']; }; +type ChannelPropsForwardedToEmojiContext = { + /** Custom UI component to override default `NimbleEmoji` from `emoji-mart` */ + Emoji?: EmojiContextValue['Emoji']; + /** Custom prop to override default `facebook.json` emoji data set from `emoji-mart` */ + emojiData?: EmojiMartData; + /** Custom UI component to override default `NimbleEmojiIndex` from `emoji-mart` */ + EmojiIndex?: EmojiContextValue['EmojiIndex']; + /** Custom UI component to override default `NimblePicker` from `emoji-mart` */ + EmojiPicker?: EmojiContextValue['EmojiPicker']; +}; + +export type ChannelProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, + V extends CustomTrigger = CustomTrigger +> = ChannelPropsForwardedToComponentContext & + ChannelPropsForwardedToEmojiContext & { + /** List of accepted file types */ + acceptedFiles?: string[]; + /** Custom handler function that runs when the active channel has unread messages (i.e., when chat is running on a separate browser tab) */ + activeUnreadHandler?: (unread: number, documentTitle: string) => void; + /** The connected and active channel */ + channel?: StreamChannel; + /** + * Optional configuration parameters used for the initial channel query. + * Applied only if the value of channel.initialized is false. + * If the channel instance has already been initialized (channel has been queried), + * then the channel query will be skipped and channelQueryOptions will not be applied. + */ + channelQueryOptions?: ChannelQueryOptions; + /** Custom action handler to override the default `client.deleteMessage(message.id)` function */ + doDeleteMessageRequest?: ( + message: StreamMessage, + ) => Promise>; + /** Custom action handler to override the default `channel.markRead` request function (advanced usage only) */ + doMarkReadRequest?: ( + channel: StreamChannel, + ) => Promise> | void; + /** Custom action handler to override the default `channel.sendMessage` request function (advanced usage only) */ + doSendMessageRequest?: ( + channelId: string, + message: Message, + options?: SendMessageOptions, + ) => ReturnType['sendMessage']> | void; + /** Custom action handler to override the default `client.updateMessage` request function (advanced usage only) */ + doUpdateMessageRequest?: ( + cid: string, + updatedMessage: UpdatedMessage, + options?: UpdateMessageOptions, + ) => ReturnType['updateMessage']>; + /** If true, chat users will be able to drag and drop file uploads to the entire channel window */ + dragAndDropWindow?: boolean; + /** Custom UI component to be shown if no active channel is set, defaults to null and skips rendering the Channel component */ + EmptyPlaceholder?: React.ReactElement; + /** + * A global flag to toggle the URL enrichment and link previews in `MessageInput` components. + * By default, the feature is disabled. Can be overridden on Thread, MessageList level through additionalMessageInputProps + * or directly on MessageInput level through urlEnrichmentConfig. + */ + enrichURLForPreview?: URLEnrichmentConfig['enrichURLForPreview']; + /** Global configuration for link preview generation in all the MessageInput components */ + enrichURLForPreviewConfig?: Omit; + /** The giphy version to render - check the keys of the [Image Object](https://developers.giphy.com/docs/api/schema#image-object) for possible values. Uses 'fixed_height' by default */ + giphyVersion?: GiphyVersions; + /** A custom function to provide size configuration for image attachments */ + imageAttachmentSizeHandler?: ImageAttachmentSizeHandler; + /** + * Allows to prevent triggering the channel.watch() call when mounting the component. + * That means that no channel data from the back-end will be received neither channel WS events will be delivered to the client. + * Preventing to initialize the channel on mount allows us to postpone the channel creation to a later point in time. + */ + initializeOnMount?: boolean; + /** Maximum number of attachments allowed per message */ + maxNumberOfFiles?: number; + /** Whether to allow multiple attachment uploads */ + multipleUploads?: boolean; + /** Custom action handler function to run on click of an @mention in a message */ + onMentionsClick?: OnMentionAction; + /** Custom action handler function to run on hover of an @mention in a message */ + onMentionsHover?: OnMentionAction; + /** If `dragAndDropWindow` prop is true, the props to pass to the MessageInput component (overrides props placed directly on MessageInput) */ + optionalMessageInputProps?: MessageInputProps; + /** You can turn on/off thumbnail generation for video attachments */ + shouldGenerateVideoThumbnail?: boolean; + /** If true, skips the message data string comparison used to memoize the current channel messages (helpful for channels with 1000s of messages) */ + skipMessageDataMemoization?: boolean; + /** A custom function to provide size configuration for video attachments */ + videoAttachmentSizeHandler?: VideoAttachmentSizeHandler; + }; + const UnMemoizedChannel = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, V extends CustomTrigger = CustomTrigger @@ -298,7 +315,6 @@ const UnMemoizedChannel = < return
{EmptyPlaceholder}
; } - // @ts-ignore return ; }; @@ -317,6 +333,7 @@ const ChannelInner = < acceptedFiles, activeUnreadHandler, channel, + channelQueryOptions, children, doDeleteMessageRequest, doMarkReadRequest, @@ -359,7 +376,7 @@ const ChannelInner = < const [state, dispatch] = useReducer>( channelReducer, - // channel.initialized === false if client.channels() was not called, e.g. ChannelList is not used + // channel.initialized === false if client.channel().query() was not called, e.g. ChannelList is not used // => Channel will call channel.watch() in useLayoutEffect => state.loading is used to signal the watch() call state { ...initialState, loading: !channel.initialized }, ); @@ -512,7 +529,7 @@ const ChannelInner = < } } } - await getChannel({ channel, client, members }); + await getChannel({ channel, client, members, options: channelQueryOptions }); const config = channel.getConfig(); setChannelConfig(config); } catch (e) { @@ -547,7 +564,13 @@ const ChannelInner = < client.off('user.deleted', handleEvent); notificationTimeouts.forEach(clearTimeout); }; - }, [channel.cid, doMarkReadRequest, channelConfig?.read_events, initializeOnMount]); + }, [ + channel.cid, + channelQueryOptions, + doMarkReadRequest, + channelConfig?.read_events, + initializeOnMount, + ]); useEffect(() => { if (!state.thread) return; diff --git a/src/components/Channel/__tests__/Channel.test.js b/src/components/Channel/__tests__/Channel.test.js index 38219cb87..7b201b63a 100644 --- a/src/components/Channel/__tests__/Channel.test.js +++ b/src/components/Channel/__tests__/Channel.test.js @@ -272,7 +272,26 @@ describe('Channel', () => { renderComponent({ channel, chatClient }); }); - await waitFor(() => expect(watchSpy).toHaveBeenCalledTimes(1)); + await waitFor(() => { + expect(watchSpy).toHaveBeenCalledTimes(1); + expect(watchSpy).toHaveBeenCalledWith(undefined); + }); + }); + + it('should apply channelQueryOptions to channel watch call', async () => { + const { channel, chatClient } = await initClient(); + const watchSpy = jest.spyOn(channel, 'watch'); + const channelQueryOptions = { + messages: { limit: 20 }, + }; + await act(() => { + renderComponent({ channel, channelQueryOptions, chatClient }); + }); + + await waitFor(() => { + expect(watchSpy).toHaveBeenCalledTimes(1); + expect(watchSpy).toHaveBeenCalledWith(channelQueryOptions); + }); }); it('should not call watch the current channel on mount if channel is initialized', async () => { @@ -375,7 +394,7 @@ describe('Channel', () => { // first, wait for the effect in which the channel is watched, // so we know the event listener is added to the document. - await waitFor(() => expect(watchSpy).toHaveBeenCalledWith()); + await waitFor(() => expect(watchSpy).toHaveBeenCalledWith(undefined)); setTimeout(() => fireEvent(document, new Event('visibilitychange')), 0); await waitFor(() => expect(markReadSpy).toHaveBeenCalledWith()); diff --git a/src/utils/getChannel.ts b/src/utils/getChannel.ts index 0a388f6bb..00962c571 100644 --- a/src/utils/getChannel.ts +++ b/src/utils/getChannel.ts @@ -1,4 +1,9 @@ -import type { Channel, QueryChannelAPIResponse, StreamChat } from 'stream-chat'; +import type { + Channel, + ChannelQueryOptions, + QueryChannelAPIResponse, + StreamChat, +} from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../types/types'; /** @@ -17,12 +22,14 @@ type GetChannelParams< channel?: Channel; id?: string; members?: string[]; + options?: ChannelQueryOptions; type?: string; }; /** * Calls channel.watch() if it was not already recently called. Waits for watch promise to resolve even if it was invoked previously. * @param client * @param members + * @param options * @param type * @param id * @param channel @@ -34,6 +41,7 @@ export const getChannel = async < client, id, members, + options, type, }: GetChannelParams) => { if (!channel && !type) { @@ -60,7 +68,7 @@ export const getChannel = async < if (queryPromise) { await queryPromise; } else { - WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid] = theChannel.watch(); + WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid] = theChannel.watch(options); await WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; delete WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; }