diff --git a/docusaurus/docs/React/assets/_link-preview-message-input.png b/docusaurus/docs/React/assets/_link-preview-message-input.png new file mode 100644 index 000000000..4907f498c Binary files /dev/null and b/docusaurus/docs/React/assets/_link-preview-message-input.png differ diff --git a/docusaurus/docs/React/assets/link-preview-edit-message-form.png b/docusaurus/docs/React/assets/link-preview-edit-message-form.png new file mode 100644 index 000000000..b05f7081d Binary files /dev/null and b/docusaurus/docs/React/assets/link-preview-edit-message-form.png differ diff --git a/docusaurus/docs/React/components/contexts/channel-action-context.mdx b/docusaurus/docs/React/components/contexts/channel-action-context.mdx index 97e2d36d8..a2b50b800 100644 --- a/docusaurus/docs/React/components/contexts/channel-action-context.mdx +++ b/docusaurus/docs/React/components/contexts/channel-action-context.mdx @@ -22,7 +22,7 @@ const { closeThread, loadMoreThread } = useChannelActionContext(); Function to add a temporary notification to `MessageList`, and it will be removed after 5 seconds. | Type | -|----------| +| -------- | | function | | | @@ -31,7 +31,7 @@ Function to add a temporary notification to `MessageList`, and it will be remove The function to close the currently open `Thread`. | Type | -|----------| +| -------- | | function | ### dispatch @@ -47,7 +47,7 @@ The dispatch function for the [`ChannelStateReducer`](https://github.com/GetStre A function that takes a message to be edited, returns a Promise. | Type | -|----------| +| -------- | | function | ### jumpToLatestMessage @@ -61,7 +61,7 @@ Used in conjunction with `jumpToMessage`. Restores the position of the message l ### jumpToMessage When called, `jumpToMessage` causes the current message list to jump to the message with the id specified in the `messageId` parameter. -Here's an example of a button, which, when clicked, searches for a given message and navigates to it: +Here's an example of a button, which, when clicked, searches for a given message and navigates to it: ```tsx const JumpToMessage = () => { @@ -90,17 +90,18 @@ const JumpToMessage = () => { // further down the line, add the JumpToMessage to the component tree as a child of `Channel` // ... - return ( +return ( + - ) + +); ``` - | Type | -| ------------------------------------ | +| -------------------------------------- | | `(messageId: string) => Promise` | ### loadMore @@ -108,16 +109,15 @@ const JumpToMessage = () => { The function to load next page/batch of `messages` (used for pagination). | Type | -|----------| +| -------- | | function | - ### loadMoreNewer The function to load next page/batch of `messages` (used for pagination). -| Type | -|-------------------------------------| +| Type | +| ------------------------------------ | | (limit?: number) => Promise | ### loadMoreThread @@ -125,7 +125,7 @@ The function to load next page/batch of `messages` (used for pagination). The function to load next page/batch of `messages` in a currently active/open `Thread` (used for pagination). | Type | -|----------| +| -------- | | function | ### onMentionsClick @@ -133,7 +133,7 @@ The function to load next page/batch of `messages` in a currently active/open `T Custom action handler function to execute when @mention is clicked, takes a DOM click event object and an array of mentioned users. | Type | -|----------| +| -------- | | function | ### onMentionsHover @@ -141,7 +141,7 @@ Custom action handler function to execute when @mention is clicked, takes a DOM The function to execute when @mention is hovered in a `message`, takes a DOM click event object and an array of mentioned users. | Type | -|----------| +| -------- | | function | ### openThread @@ -149,7 +149,7 @@ The function to execute when @mention is hovered in a `message`, takes a DOM cli The function to execute when replies count button is clicked, takes the parent message of the `Thread` to be opened and optionally a DOM click event. | Type | -|----------| +| -------- | | function | ### removeMessage @@ -157,7 +157,7 @@ The function to execute when replies count button is clicked, takes the parent m The function to remove a `message` from `MessageList`, handled by the `Channel` component. Takes a `message` object. | Type | -|----------| +| -------- | | function | ### retrySendMessage @@ -165,7 +165,7 @@ The function to remove a `message` from `MessageList`, handled by the `Channel` The function to resend a `message`, handled by the `Channel` component. | Type | -|----------| +| -------- | | function | ### sendMessage @@ -173,7 +173,7 @@ The function to resend a `message`, handled by the `Channel` component. The function to send a `message` on `Channel`. Takes a `message` object with the basic message information as the first argument, and custom data as the second argument. | Type | -|----------| +| -------- | | function | ### setQuotedMessage @@ -181,7 +181,7 @@ The function to send a `message` on `Channel`. Takes a `message` object with the The function to send a `QuotedMessage` on a `Channel`, take a `message` object. | Type | -|----------| +| -------- | | function | ### updateMessage @@ -189,5 +189,5 @@ The function to send a `QuotedMessage` on a `Channel`, take a `message` object. The function to update a `message` on `Channel`, takes a `message` object. | Type | -|----------| +| -------- | | function | diff --git a/docusaurus/docs/React/components/contexts/channel-state-context.mdx b/docusaurus/docs/React/components/contexts/channel-state-context.mdx index 44d09c975..68aeb06ea 100644 --- a/docusaurus/docs/React/components/contexts/channel-state-context.mdx +++ b/docusaurus/docs/React/components/contexts/channel-state-context.mdx @@ -57,6 +57,23 @@ If true, chat users will be able to drag and drop file uploads to the entire cha | ------- | ------- | | boolean | false | +### debounceURLEnrichmentMs: + +Number of milliseconds to debounce firing the URL enrichment queries when typing. The default value is 1500(ms). See the guide [Link Previews in Message Input](../../../guides/customization/link-previews) for more. + +| Type | Default | +| ------ | ------- | +| number | 1500 | + +### enrichURLForPreview + +A global flag to toggle the URL enrichment and link previews in `MessageInput`. By default, the feature is disabled. It can be overridden on Thread and MessageList level through `additionalMessageInputProps` +or directly on `MessageInput` level through `urlEnrichmentConfig` prop. + +| Type | Default | +| ------- | ------- | +| boolean | false | + ### error Error object (if any) in loading the `channel`, otherwise null. @@ -65,6 +82,14 @@ Error object (if any) in loading the `channel`, otherwise null. | ------ | | object | +### findURLFn + +Custom function to identify URLs in a string for later generation of link previews. See the guide [Link Previews in Message Input](../../../guides/customization/link-previews) for more. + +| Type | +| ---------------------------- | +| `(text: string) => string[]` | + ### giphyVersion The giphy version to use when displaying giphies. @@ -89,7 +114,6 @@ If the channel has more, older, messages to paginate through. | ------- | | boolean | - ### hasMoreNewer If the channel has more, newer, messages to paginate through. @@ -103,7 +127,7 @@ If the channel has more, newer, messages to paginate through. Value is used internally for jump-to-message logic. Once the user "jumped" to the message, the message with the given ID is highlighted by manipulating its styles attribute. | Type | -|--------| +| ------ | | string | ### loading @@ -127,7 +151,7 @@ Boolean for the `channel` loading more messages. Flag signalling whether newer messages are being loaded as the user scrolls down in the message list. Used internally by `VirtualizedMessageList`. | Type | -|---------| +| ------- | | boolean | ### maxNumberOfFiles @@ -178,6 +202,14 @@ Temporary notifications added to the `MessageList` on specific user/message acti | -------------------------------------------------------- | | {id: string, text: string, type: 'success' \| 'error'}[] | +### onLinkPreviewDismissed + +Custom function to react to link preview dismissal. See the guide [Link Previews in Message Input](../../../guides/customization/link-previews) for more. + +| Type | +| ------------------------------------ | +| `(linkPreview: LinkPreview) => void` | + ### pinnedMessages The messages that are pinned in the `channel`. @@ -202,14 +234,14 @@ The read state for each `channel` member. | ------ | | object | - ### suppressAutoscroll Flag signalling whether the scroll to the bottom is prevented. Used internally by `MessageList` and `VirtualizedMessageList` components. | Type | -|---------| +| ------- | | boolean | + ### shouldGenerateVideoThumbnail You can turn on/off thumbnail generation for video attachments @@ -255,8 +287,9 @@ Array of messages within a `thread`. Flag signalling whether the scroll to the bottom is prevented in thread. Used internally by `MessageList` and `VirtualizedMessageList` components. | Type | -|---------| +| ------- | | boolean | + ### videoAttachmentSizeHandler A custom function to provide size configuration for video attachments diff --git a/docusaurus/docs/React/components/contexts/chat-context.mdx b/docusaurus/docs/React/components/contexts/chat-context.mdx index 6de883b46..498edef25 100644 --- a/docusaurus/docs/React/components/contexts/chat-context.mdx +++ b/docusaurus/docs/React/components/contexts/chat-context.mdx @@ -38,7 +38,7 @@ The currently active channel, which populates the [`Channel`](../core-components The function to close mobile navigation. | Type | -|----------| +| -------- | | function | ### customClasses @@ -55,7 +55,7 @@ for implementation assistance. The callback function used to get available client-side app settings, includes image and file upload config. | Type | -|----------| +| -------- | | function | ### latestMessageDatesByChannel @@ -79,7 +79,7 @@ An array of users that have been muted by the connected user. When the screen width is at a mobile breakpoint, whether the mobile navigation menu is open. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### openMobileNav @@ -87,7 +87,7 @@ When the screen width is at a mobile breakpoint, whether the mobile navigation m The function to open mobile navigation. | Type | -|----------| +| -------- | | function | ### setActiveChannel @@ -96,7 +96,7 @@ A function to set the currently active channel. This is used in the `ChannelList You can override the default behavior by pulling it from context and then utilizing the function. | Type | -|----------| +| -------- | | function | ### theme @@ -111,9 +111,9 @@ Deprecated and to be removed in a future major release. Use the `customStyles` p Stream chat theme version 2 has been introduced with the release of stream-chat-react v10.0.0. This flag is used internally by some UI components of the SDK and the integrators shouldn't need to use it. The value is extracted from a CSS variable `--str-chat__theme-version`. You can set it to values `'1'` or `'2'` in your stylesheets and import the corresponding v2 stylesheet from `stream-chat-react/dist`. Find out more about benefits that the theme version 2 brings to the integrators with [the theming guide](../../theming/introduction.mdx). -| Type | Default | -| -------------- | --------- | -| `'1'` \| `'2'` | `'1'` | +| Type | Default | +| -------------- | ------- | +| `'1'` \| `'2'` | `'1'` | ### useImageFlagEmojisOnWindow diff --git a/docusaurus/docs/React/components/contexts/component-context.mdx b/docusaurus/docs/React/components/contexts/component-context.mdx index 077c949d8..6c2004498 100644 --- a/docusaurus/docs/React/components/contexts/component-context.mdx +++ b/docusaurus/docs/React/components/contexts/component-context.mdx @@ -26,15 +26,23 @@ const { Attachment, Avatar, Message } = useComponentContext(); Custom UI component to display attachment in an individual message. | Type | Default | -|-----------|------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------- | | component | | +### AttachmentPreviewList + +Custom UI component to display a attachment previews in `MessageInput`. + +| Type | Default | +| --------- | ----------------------------------------------------------------------------------- | +| component | | + ### AutocompleteSuggestionHeader Custom UI component to override the default suggestion header component. | Type | Default | -|-----------|--------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------ | | component | | ### AutocompleteSuggestionItem @@ -42,7 +50,7 @@ Custom UI component to override the default suggestion header component. Custom UI component to override the default suggestion Item component. | Type | Default | -|-----------|---------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------- | | component | | ### AutocompleteSuggestionList @@ -50,7 +58,7 @@ Custom UI component to override the default suggestion Item component. Custom UI component to override the default List component that displays suggestions. | Type | Default | -|-----------|---------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------- | | component | | ### Avatar @@ -58,7 +66,7 @@ Custom UI component to override the default List component that displays suggest Custom UI component to display a user's avatar. | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### CooldownTimer @@ -66,7 +74,7 @@ Custom UI component to display a user's avatar. Custom UI component to display the slow mode cooldown timer. | Type | Default | -|-----------|--------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------ | | component | | ### DateSeparator @@ -74,7 +82,7 @@ Custom UI component to display the slow mode cooldown timer. Custom UI component for date separators. | Type | Default | -|-----------|---------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------- | | component | | ### EditMessageInput @@ -82,7 +90,7 @@ Custom UI component for date separators. Custom UI component to override default edit message input. | Type | Default | -|-----------|------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------- | | component | | ### EmojiIcon @@ -90,7 +98,7 @@ Custom UI component to override default edit message input. Custom UI component for emoji button in input. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### EmptyStateIndicator @@ -98,7 +106,7 @@ Custom UI component for emoji button in input. Custom UI component to be displayed when the `MessageList` is empty. | Type | Default | -|-----------|---------------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------------- | | component | | ### FileUploadIcon @@ -106,7 +114,7 @@ Custom UI component to be displayed when the `MessageList` is empty. Custom UI component for file upload icon. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### GiphyPreviewMessage @@ -114,7 +122,7 @@ Custom UI component for file upload icon. Custom UI component to render a Giphy preview in the `VirtualizedMessageList`. | Type | Default | -|-----------|-------------------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------------------- | | component | | ### HeaderComponent @@ -130,15 +138,23 @@ Custom UI component to render at the top of the `MessageList`. Custom UI component handling how the message input is rendered. | Type | Default | -|-----------|--------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------ | | component | | +### LinkPreviewList + +Custom component to render link previews in `MessageInput`. + +| Type | Default | +| --------- | ----------------------------------------------------------------------------------- | +| component | | + ### LoadingErrorIndicator Custom UI component to be shown if the channel query fails. | Type | Default | -|-----------|-------------------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------------------- | | component | | ### LoadingIndicator @@ -146,7 +162,7 @@ Custom UI component to be shown if the channel query fails. Custom UI component to render while the `MessageList` is loading new messages. | Type | Default | -|-----------|---------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------- | | component | | ### Message @@ -154,7 +170,7 @@ Custom UI component to render while the `MessageList` is loading new messages. Custom UI component to display a message in the standard `MessageList`. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### MessageDeleted @@ -162,7 +178,7 @@ Custom UI component to display a message in the standard `MessageList`. Custom UI component for a deleted message. | Type | Default | -|-----------|-----------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------- | | component | | ### MessageListNotifications @@ -170,7 +186,7 @@ Custom UI component for a deleted message. Custom UI component that displays message and connection status notifications in the `MessageList`. | Type | Default | -|-----------|------------------------------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------------------------------- | | component | | ### MessageNotification @@ -178,7 +194,7 @@ Custom UI component that displays message and connection status notifications in Custom UI component to display a notification when scrolled up the list and new messages arrive. | Type | Default | -|-----------|-------------------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------------------- | | component | | ### MessageOptions @@ -186,7 +202,7 @@ Custom UI component to display a notification when scrolled up the list and new Custom UI component for message options popup. | Type | Default | -|-----------|-----------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------- | | component | | ### MessageRepliesCountButton @@ -194,7 +210,7 @@ Custom UI component for message options popup. Custom UI component to display message replies. | Type | Default | -|-----------|---------------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------------- | | component | | ### MessageStatus @@ -202,7 +218,7 @@ Custom UI component to display message replies. Custom UI component to display message delivery status. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### MessageSystem @@ -210,7 +226,7 @@ Custom UI component to display message delivery status. Custom UI component to display system messages. | Type | Default | -|-----------|------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------- | | component | | ### MessageTimestamp @@ -218,7 +234,7 @@ Custom UI component to display system messages. Custom UI component to display a timestamp on a message. | Type | Default | -|-----------|---------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------- | | component | | ### ModalGallery @@ -226,7 +242,7 @@ Custom UI component to display a timestamp on a message. Custom UI component for viewing message's image attachments. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### PinIndicator @@ -234,7 +250,7 @@ Custom UI component for viewing message's image attachments. Custom UI component to override default pinned message indicator. | Type | Default | -|-----------|------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------- | | component | | ### QuotedMessage @@ -242,7 +258,7 @@ Custom UI component to override default pinned message indicator. Custom UI component to override quoted message UI on a sent message. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### QuotedMessagePreview @@ -250,7 +266,7 @@ Custom UI component to override quoted message UI on a sent message. Custom UI component to override the message input's quoted message preview. | Type | Default | -|-----------|----------------------------------------------------------------------------------------------| +| --------- | -------------------------------------------------------------------------------------------- | | component | | ### ReactionSelector @@ -258,7 +274,7 @@ Custom UI component to override the message input's quoted message preview. Custom UI component to display the reaction selector. | Type | Default | -|-----------|-----------------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------------- | | component | | ### ReactionsList @@ -266,7 +282,7 @@ Custom UI component to display the reaction selector. Custom UI component to display the list of reactions on a message. | Type | Default | -|-----------|-----------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------- | | component | | ### SendButton @@ -274,7 +290,7 @@ Custom UI component to display the list of reactions on a message. Custom UI component for send button. | Type | Default | -|-----------|---------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------- | | component | | ### ThreadHead @@ -282,7 +298,7 @@ Custom UI component for send button. Custom UI component to be displayed at the beginning of a thread. By default, it is the thread parent message. It is composed of [Message](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/Message.tsx) context provider component and [ThreadStart](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Thread/ThreadStart.tsx) component. The latter can be customized by passing custom component to `Channel` props. The `ThreadHead` component defaults to and accepts the same props as [MessageSimple](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageSimple.tsx). | Type | Default | -|-----------|--------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------ | | component | | ### ThreadHeader @@ -290,23 +306,23 @@ Custom UI component to be displayed at the beginning of a thread. By default, it Custom UI component to display the header of a `Thread`. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### ThreadInput -Custom UI component to replace the `MessageInput` of a `Thread`. For the applications using [theme version 1](../../guides/theming/css-and-theming.mdx), the default is `MessageInputSmall`. Applications using [theme version 2](../../theming/introduction.mdx) will use `MessageInputFlat` by default. +Custom UI component to replace the `MessageInput` of a `Thread`. For the applications using [theme version 1](../../guides/theming/css-and-theming.mdx), the default is `MessageInputSmall`. Applications using [theme version 2](../../theming/introduction.mdx) will use `MessageInputFlat` by default. -| Type | Default | -| --------- | ------------------------------------------------------------------------------------------------------------------ | -| component | / | +| Type | Default | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| component | / | ### ThreadStart Custom UI component to display the start of a threaded `MessageList`. | Type | Default | -|-----------|------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------- | | component | | ### TriggerProvider @@ -314,7 +330,7 @@ Custom UI component to display the start of a threaded `MessageList`. Optional context provider that lets you override the default autocomplete triggers. | Type | Default | -|-----------|--------------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------------ | | component | | ### TypingIndicator @@ -322,7 +338,7 @@ Optional context provider that lets you override the default autocomplete trigge Custom UI component for the typing indicator. | Type | Default | -|-----------|---------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------- | | component | | ### VirtualMessage @@ -330,5 +346,5 @@ Custom UI component for the typing indicator. Custom UI component to display a message in the `VirtualizedMessageList`. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | diff --git a/docusaurus/docs/React/components/contexts/message-context.mdx b/docusaurus/docs/React/components/contexts/message-context.mdx index 26aa35e9e..8f88085ee 100644 --- a/docusaurus/docs/React/components/contexts/message-context.mdx +++ b/docusaurus/docs/React/components/contexts/message-context.mdx @@ -50,7 +50,7 @@ Call this function to keep message list scrolled to the bottom when the message When called, this function will exit the editing state on the message. | Type | -|--------------------------------------------| +| ------------------------------------------ | | (event?: React.BaseSyntheticEvent) => void | ### customMessageActions @@ -60,7 +60,7 @@ An object containing custom message actions (key) and function handlers (value). ```jsx const customActions = { 'Copy text': (message) => { - navigator.clipboard.writeText(message.text || '') + navigator.clipboard.writeText(message.text || ''); }, }; @@ -68,7 +68,12 @@ const customActions = { ``` Custom action item "Copy text" in the message actions box: -Image of a custom action item "Copy text" in the message actions box + +Image of a custom action item "Copy text" in the message actions box | Type | | ------ | @@ -202,13 +207,12 @@ Function that retries sending a message after a failed request (overrides the fu | ------------------------------------------ | | (message: StreamMessage) => Promise | - ### highlighted Whether to highlight and focus the message on load. | Type | -|---------| +| ------- | | boolean | ### initialMessage @@ -263,8 +267,8 @@ DOMRect object linked to the parent `MessageList` component. An array of users that have been muted by the connected user. -| Type | Default | -| ------ | --------------------------------------------------------------------- | +| Type | Default | +| ------ | ----------------------------------------------------------- | | Mute[] | [ChannelStateContext['mutes']](./channel-state-context.mdx) | ### onMentionsClickMessage @@ -312,7 +316,7 @@ Function that runs on hover of a user avatar. The user roles allowed to pin messages in various channel types (deprecated in favor of `channelCapabilities`). | Type | Default | -|--------|---------------------------------------------------------------------------| +| ------ | ------------------------------------------------------------------------- | | object | | ### reactionSelectorRef diff --git a/docusaurus/docs/React/components/contexts/message-input-context.mdx b/docusaurus/docs/React/components/contexts/message-input-context.mdx index b51cbaa1f..cc9327c44 100644 --- a/docusaurus/docs/React/components/contexts/message-input-context.mdx +++ b/docusaurus/docs/React/components/contexts/message-input-context.mdx @@ -46,6 +46,14 @@ A mapping of the current triggers permitted in the currently active channel. | | `@` - mentions | | | `:` - emojis | +### cancelURLEnrichment + +Function cancels all the scheduled or in-progress URL enrichment queries and resets the state. + +| Type | +| ---------- | +| () => void | + ### clearEditingState Function to clear the editing state while editing a message. @@ -70,13 +78,12 @@ Function to close the `EmojiPicker` component. | ------------------------------------------- | | React.MouseEventHandler | - ### closeEmojiPickerOnClick If true, picking an emoji from the `EmojiPicker` component will close the picker. | Type | -|---------| +| ------- | | boolean | ### closeMentionsList @@ -119,6 +126,14 @@ If true, the suggestion list will not display and autocomplete @mentions. | ------- | ------- | | boolean | false | +### dismissLinkPreview + +Function called when a single link preview is dismissed. + +| Type | +| ---------------------------------- | +| (linkPreview: LinkPreview) => void | + ### doFileUploadRequest Function to override the default file upload request. @@ -183,6 +198,14 @@ A mapping of the file attachments added to the current message. | ---------------------------- | | { [id: string]: FileUpload } | +### findAndEnqueueURLsToEnrich + +A function responsible for initiating URL discovery and their subsequent enrichment. It is available only if link preview rendering is enabled. Link previews are disabled by default. + +| Type | +| ------------------------------------------------- | +| (text: string, mode?: SetLinkPreviewMode) => void | + ### focus If true, focuses the text input on component mount. @@ -255,6 +278,14 @@ If true, file uploads are enabled in the currently active channel. | ------- | ------- | | boolean | true | +### linkPreviews + +A Map of `LinkPreview` objects (a union type of `LinkPreviewState` and `OGAttachment`) indexed by string representing link URL. The link URL value is provided by `OGAttachment.og_scrape_url`. + +| Type | +| -------------------------- | +| Map | + ### shouldSubmit Currently, `Enter` is the default submission key and `Shift`+`Enter` is the default combination for the new line. @@ -480,7 +511,6 @@ Function to upload an image. | -------------------- | | (id: string) => void | - ### uploadNewFiles Function to upload an array of files to the `fileUploads` and `imageUploads` mappings. diff --git a/docusaurus/docs/React/components/contexts/translation-context.mdx b/docusaurus/docs/React/components/contexts/translation-context.mdx index b4d82ad90..255933cfc 100644 --- a/docusaurus/docs/React/components/contexts/translation-context.mdx +++ b/docusaurus/docs/React/components/contexts/translation-context.mdx @@ -26,7 +26,7 @@ const { t } = useTranslationContext(); Function that translates text into the connected user's set language. | Type | -|----------| +| -------- | | function | ### tDateTimeParser @@ -34,7 +34,7 @@ Function that translates text into the connected user's set language. Function that parses date times. | Type | Default | -|----------|---------| +| -------- | ------- | | function | Day.js | ### userLanguage @@ -42,5 +42,5 @@ Function that parses date times. Value to set the connected user's language (ex: 'en', 'fr', 'ru', etc), which auto translates text fields in the library. | Type | Default | -|--------|---------| +| ------ | ------- | | string | 'en' | diff --git a/docusaurus/docs/React/components/core-components/channel-list.mdx b/docusaurus/docs/React/components/core-components/channel-list.mdx index 32c4b3688..b418b3a66 100644 --- a/docusaurus/docs/React/components/core-components/channel-list.mdx +++ b/docusaurus/docs/React/components/core-components/channel-list.mdx @@ -3,11 +3,11 @@ id: channel_list sidebar_position: 3 title: ChannelList --- + import CodeBlock from '@theme/CodeBlock'; import GHComponentLink from '../../_docusaurus-components/GHComponentLink'; - The `ChannelList` component queries an array of `channel` objects from the Stream Chat API and displays as a customizable list in the UI. It accepts props for [`filters`](#filters), [`sort`](#sort) and [`options`](#options), which allows you to tailor your request to the [Query Channels](https://getstream.io/chat/docs/javascript/query_channels/?language=javascript) API. The @@ -30,6 +30,7 @@ By default when channels query does not have any filter it will match all the ch **At a minimum, the filter should include `{members: { $in: [userID] }}` .** ::: + ```jsx const filters = { members: { $in: [ 'jimmy', 'buffet' ] } } const sort = { last_message_at: -1 }; @@ -171,7 +172,7 @@ Similarly, events other than `notification.message_new` can be handled as per ap Additional props to be passed to the underlying [`ChannelSearch`](../utility-components/channel-search.mdx) component. | Type | -|--------| +| ------ | | object | ### allowNewMessagesFromUnfilteredChannels @@ -182,7 +183,7 @@ and push it to the top of the list. You can disable this behavior by setting thi list from incrementing the list. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### Avatar @@ -190,7 +191,7 @@ list from incrementing the list. Custom UI component to display the user's avatar. | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### channelRenderFilterFn @@ -199,7 +200,7 @@ Optional function to filter channels prior to loading in the DOM. Do not use any loading of the `ChannelList`. We recommend using a pure function with array methods like filter/sort/reduce. | Type | -|------------------------------------| +| ---------------------------------- | | (channels: Channel[]) => Channel[] | ### ChannelSearch @@ -207,7 +208,7 @@ loading of the `ChannelList`. We recommend using a pure function with array meth Custom UI component to display search results. | Type | Default | -|-----------|---------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------- | | component | | ### customActiveChannel @@ -215,7 +216,7 @@ Custom UI component to display search results. Set a channel (with this ID) to active and force it to move to the top of the list. | Type | -|--------| +| ------ | | string | ### EmptyStateIndicator @@ -223,7 +224,7 @@ Set a channel (with this ID) to active and force it to move to the top of the li Custom UI component for rendering an empty list. | Type | Default | -|-----------|---------------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------------- | | component | | ### filters @@ -232,7 +233,7 @@ An object containing channel query filters, check our [query parameters docs](ht for more information. | Type | -|--------| +| ------ | | object | ### List @@ -240,7 +241,7 @@ for more information. Custom UI component to display the container for the queried channels. | Type | Default | -|-----------|---------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------- | | component | | ### LoadingErrorIndicator @@ -248,7 +249,7 @@ Custom UI component to display the container for the queried channels. Custom UI component to display the loading error indicator. | Type | Default | -|-----------|------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------- | | component | | ### LoadingIndicator @@ -256,7 +257,7 @@ Custom UI component to display the loading error indicator. Custom UI component to display the loading state. | Type | Default | -|-----------|-------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------- | | component | | ### lockChannelOrder @@ -264,7 +265,7 @@ Custom UI component to display the loading state. When true, channels won't dynamically sort by most recent message. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### onAddedToChannel @@ -272,7 +273,7 @@ When true, channels won't dynamically sort by most recent message. Function to override the default behavior when a user is added to a channel. | Type | -|----------| +| -------- | | function | ### onChannelDeleted @@ -280,7 +281,7 @@ Function to override the default behavior when a user is added to a channel. Function to override the default behavior when a channel is deleted. | Type | -|----------| +| -------- | | function | ### onChannelHidden @@ -288,7 +289,7 @@ Function to override the default behavior when a channel is deleted. Function to override the default behavior when a channel is hidden. | Type | -|----------| +| -------- | | function | ### onChannelTruncated @@ -296,7 +297,7 @@ Function to override the default behavior when a channel is hidden. Function to override the default behavior when a channel is truncated. | Type | -|----------| +| -------- | | function | ### onChannelUpdated @@ -304,7 +305,7 @@ Function to override the default behavior when a channel is truncated. Function to override the default behavior when a channel is updated. | Type | -|----------| +| -------- | | function | ### onChannelVisible @@ -312,7 +313,7 @@ Function to override the default behavior when a channel is updated. Function to override the default channel visible behavior. | Type | -|----------| +| -------- | | function | ### onMessageNew @@ -320,7 +321,7 @@ Function to override the default channel visible behavior. Function to override the default behavior when a message is received on a channel not being watched. | Type | -|----------| +| -------- | | function | ### onRemovedFromChannel @@ -328,7 +329,7 @@ Function to override the default behavior when a message is received on a channe Function to override the default behavior when a user gets removed from a channel. | Type | -|----------| +| -------- | | function | ### options @@ -337,7 +338,7 @@ An object containing channel query options, check our [query parameters docs](ht for more information. | Type | -|--------| +| ------ | | object | ### Paginator @@ -345,7 +346,7 @@ for more information. Custom UI component to handle channel pagination logic. | Type | Default | -|-----------|------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------- | | component | | ### Preview @@ -353,7 +354,7 @@ Custom UI component to handle channel pagination logic. Custom UI component to display the channel preview in the list. | Type | Default | -|-----------|------------------------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------------------------- | | component | | ### renderChannels @@ -361,7 +362,7 @@ Custom UI component to display the channel preview in the list. Function to override the default behavior when rendering channels, so this function is called instead of rendering the Preview directly. | Type | -|----------| +| -------- | | function | ### sendChannelsToList @@ -369,7 +370,7 @@ Function to override the default behavior when rendering channels, so this funct If true, sends the list's currently loaded channels to the `List` component as the `loadedChannels` prop. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### setActiveChannelOnMount @@ -377,7 +378,7 @@ If true, sends the list's currently loaded channels to the `List` component as t If true, sets the most recent channel received from the query as active on component mount. If set to `false` no channel is set as active on mount. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### showChannelSearch @@ -385,7 +386,7 @@ If true, sets the most recent channel received from the query as active on compo If true, renders the [`ChannelSearch`](./#channelsearch) component above the [`List`](./#list) component. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### sort @@ -394,7 +395,7 @@ An object containing channel query sort parameters. Check our [query parameters for more information. | Type | -|--------| +| ------ | | object | ### watchers @@ -402,5 +403,5 @@ for more information. An object containing query parameters for fetching channel watchers. | Type | -|---------------------------------------| +| ------------------------------------- | | `{ limit?: number; offset?: number }` | diff --git a/docusaurus/docs/React/components/core-components/channel.mdx b/docusaurus/docs/React/components/core-components/channel.mdx index def2eae7a..2d7ab0523 100644 --- a/docusaurus/docs/React/components/core-components/channel.mdx +++ b/docusaurus/docs/React/components/core-components/channel.mdx @@ -3,6 +3,7 @@ id: channel sidebar_position: 4 title: Channel --- + import GHComponentLink from '../../_docusaurus-components/GHComponentLink'; The `Channel` component is a React Context provider that wraps all the logic, functionality, and UI for an individual chat channel. @@ -66,31 +67,31 @@ In case you would like to customize parts of your chat application, you can do t **Example of registering custom Avatar component** ```jsx -import {Channel, ChannelList, Chat, MessageInput, MessageList} from 'stream-chat-react'; +import { Channel, ChannelList, Chat, MessageInput, MessageList } from 'stream-chat-react'; import { CustomTooltip } from '../Tooltip/CustomTooltip'; -const Avatar = ({image, title}) => { - +const Avatar = ({ image, title }) => { return ( <> - {title} -
{title}/
+ {title} +
+ {title} +
); -} +}; export const App = ( - - - - - - -) + + + + + + +); ``` - ## Props ### channel @@ -108,23 +109,23 @@ Do not provide this prop if you are using the `ChannelList` component, as it han ::: | Type | -|--------| +| ------ | | object | ### acceptedFiles A list of accepted file upload types. -| Type | -|------------| +| Type | +| -------- | | string[] | ### activeUnreadHandler Custom handler function that runs when the active channel has unread messages (i.e., when chat is running on a separate browser tab). -| Type | -|---------------------------------------------------| +| Type | +| ----------------------------------------------- | | (unread: number, documentTitle: string) => void | ### Attachment @@ -132,15 +133,23 @@ Custom handler function that runs when the active channel has unread messages (i Custom UI component to display a message attachment. | Type | Default | -|-----------|------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------- | | component | | +### AttachmentPreviewList + +Custom UI component to display a attachment previews in `MessageInput`. + +| Type | Default | +| --------- | ----------------------------------------------------------------------------------- | +| component | | + ### AutocompleteSuggestionHeader Custom UI component to override the default suggestion header component. | Type | Default | -|-----------|--------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------ | | component | | ### AutocompleteSuggestionItem @@ -148,7 +157,7 @@ Custom UI component to override the default suggestion header component. Custom UI component to override the default suggestion Item component. | Type | Default | -|-----------|---------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------- | | component | | ### AutocompleteSuggestionList @@ -156,7 +165,7 @@ Custom UI component to override the default suggestion Item component. Custom UI component to override the default List component that displays suggestions. | Type | Default | -|-----------|---------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------- | | component | | ### Avatar @@ -164,7 +173,7 @@ Custom UI component to override the default List component that displays suggest Custom UI component to display a user's avatar. | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### CooldownTimer @@ -172,7 +181,7 @@ Custom UI component to display a user's avatar. Custom UI component to display the slow mode cooldown timer. | Type | Default | -|-----------|--------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------ | | component | | ### DateSeparator @@ -180,7 +189,7 @@ Custom UI component to display the slow mode cooldown timer. Custom UI component for date separators. | Type | Default | -|-----------|---------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------- | | component | | ### doMarkReadRequest @@ -188,7 +197,7 @@ Custom UI component for date separators. Custom action handler to override the default `channel.markRead` request function (advanced usage only). | Type | -|----------| +| -------- | | function | ### doSendMessageRequest @@ -196,7 +205,7 @@ Custom action handler to override the default `channel.markRead` request functio Custom action handler to override the default `channel.sendMessage` request function (advanced usage only). | Type | -|----------| +| -------- | | function | ### doUpdateMessageRequest @@ -204,7 +213,7 @@ Custom action handler to override the default `channel.sendMessage` request func Custom action handler to override the default `client.updateMessage` request function (advanced usage only). | Type | -|----------| +| -------- | | function | ### dragAndDropWindow @@ -212,7 +221,7 @@ Custom action handler to override the default `client.updateMessage` request fun If true, chat users will be able to drag and drop file uploads to the entire channel window. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### EditMessageInput @@ -220,7 +229,7 @@ If true, chat users will be able to drag and drop file uploads to the entire cha Custom UI component to override default edit message input. | Type | Default | -|-----------|------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------- | | component | | ### Emoji @@ -228,7 +237,7 @@ Custom UI component to override default edit message input. Custom UI component to override default `NimbleEmoji` from `emoji-mart`. | Type | -|-----------| +| --------- | | component | ### emojiData @@ -236,7 +245,7 @@ Custom UI component to override default `NimbleEmoji` from `emoji-mart`. Custom prop to override default `facebook.json` emoji data set from `emoji-mart`. | Type | -|--------| +| ------ | | object | ### EmojiIcon @@ -244,7 +253,7 @@ Custom prop to override default `facebook.json` emoji data set from `emoji-mart` Custom UI component for emoji button in input. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### EmojiIndex @@ -252,7 +261,7 @@ Custom UI component for emoji button in input. Custom UI component to override default `NimbleEmojiIndex` from `emoji-mart`. | Type | -|-----------| +| --------- | | component | ### EmojiPicker @@ -260,7 +269,7 @@ Custom UI component to override default `NimbleEmojiIndex` from `emoji-mart`. Custom UI component to override default `NimblePicker` from `emoji-mart`. | Type | -|-----------| +| --------- | | component | ### EmptyPlaceholder @@ -268,7 +277,7 @@ Custom UI component to override default `NimblePicker` from `emoji-mart`. Custom UI component to be shown if no active `channel` is set, defaults to `null` and skips rendering the `Channel` component. | Type | Default | -|-----------|---------| +| --------- | ------- | | component | null | ### EmptyStateIndicator @@ -276,15 +285,32 @@ Custom UI component to be shown if no active `channel` is set, defaults to `null Custom UI component to be displayed when the `MessageList` or `VirtualizedMessageList` is empty. | Type | Default | -|-----------|---------------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------------- | | component | | +### enrichURLForPreview + +A global flag to toggle the URL enrichment and link previews in `MessageInput`. The feature is disabled by default. It can be overridden on `Thread` and `MessageList` level through `additionalMessageInputProps` +or directly on `MessageInput` level through `urlEnrichmentConfig` prop. See the guide [Link Previews in Message Input](../../../guides/customization/link-previews) for more. + +| Type | Default | +| ------- | ------- | +| boolean | false | + +### enrichURLForPreviewConfig: + +Global configuration for link preview generation in all the MessageInput components. See the guide [Link Previews in Message Input](../../../guides/customization/link-previews) for more. + +| Type | +| ------------------------------------------------- | +| Omit | + ### FileUploadIcon Custom UI component for file upload icon. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### GiphyPreviewMessage @@ -292,7 +318,7 @@ Custom UI component for file upload icon. Custom UI component to render a Giphy preview in the `VirtualizedMessageList`. | Type | Default | -|-----------|-------------------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------------------- | | component | | ### giphyVersion @@ -316,7 +342,7 @@ A custom function to provide size configuration for image attachments Custom UI component to render at the top of the `MessageList`. | Type | Default | -|-----------|---------| +| --------- | ------- | | component | none | ### Input @@ -324,15 +350,23 @@ Custom UI component to render at the top of the `MessageList`. Custom UI component handling how the message input is rendered. | Type | Default | -|-----------|--------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------ | | component | | +### LinkPreviewList + +Custom component to render link previews in `MessageInput`. + +| Type | Default | +| --------- | ----------------------------------------------------------------------------------- | +| component | | + ### LoadingErrorIndicator Custom UI component to be shown if the channel query fails. | Type | Default | -|-----------|-------------------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------------------- | | component | | ### LoadingIndicator @@ -340,7 +374,7 @@ Custom UI component to be shown if the channel query fails. Custom UI component to render while the `MessageList` is loading new messages. | Type | Default | -|-----------|---------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------- | | component | | ### maxNumberOfFiles @@ -348,7 +382,7 @@ Custom UI component to render while the `MessageList` is loading new messages. The maximum number of attachments allowed per message, defaults to the Stream Chat API maximum. | Type | Default | -|--------|---------| +| ------ | ------- | | number | 10 | ### Message @@ -356,7 +390,7 @@ The maximum number of attachments allowed per message, defaults to the Stream Ch Custom UI component to display a message in the standard `MessageList`. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### MessageDeleted @@ -364,7 +398,7 @@ Custom UI component to display a message in the standard `MessageList`. Custom UI component for a deleted message. | Type | Default | -|-----------|-----------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------- | | component | | ### MessageListNotifications @@ -372,7 +406,7 @@ Custom UI component for a deleted message. Custom UI component that displays message and connection status notifications in the `MessageList`. | Type | Default | -|-----------|------------------------------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------------------------------- | | component | | ### MessageNotification @@ -380,7 +414,7 @@ Custom UI component that displays message and connection status notifications in Custom UI component to display a notification when scrolled up the list and new messages arrive. | Type | Default | -|-----------|-------------------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------------------- | | component | | ### MessageOptions @@ -388,7 +422,7 @@ Custom UI component to display a notification when scrolled up the list and new Custom UI component for message options popup. | Type | Default | -|-----------|-----------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------- | | component | | ### MessageRepliesCountButton @@ -396,7 +430,7 @@ Custom UI component for message options popup. Custom UI component to display message replies. | Type | Default | -|-----------|---------------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------------- | | component | | ### MessageStatus @@ -404,7 +438,7 @@ Custom UI component to display message replies. Custom UI component to display message delivery status. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### MessageSystem @@ -412,7 +446,7 @@ Custom UI component to display message delivery status. Custom UI component to display system messages. | Type | Default | -|-----------|------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------- | | component | | ### MessageTimestamp @@ -420,7 +454,7 @@ Custom UI component to display system messages. Custom UI component to display a timestamp on a message. | Type | Default | -|-----------|---------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------- | | component | | ### ModalGallery @@ -428,7 +462,7 @@ Custom UI component to display a timestamp on a message. Custom UI component for viewing message's image attachments. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### multipleUploads @@ -436,7 +470,7 @@ Custom UI component for viewing message's image attachments. Whether to allow multiple attachment uploads on a message. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### onMentionsClick @@ -444,7 +478,7 @@ Whether to allow multiple attachment uploads on a message. Custom action handler function to run on click of an @mention in a message. | Type | -|----------| +| -------- | | function | ### onMentionsHover @@ -452,7 +486,7 @@ Custom action handler function to run on click of an @mention in a message. Custom action handler function to run on hover of an @mention in a message. | Type | -|----------| +| -------- | | function | ### optionalMessageInputProps @@ -468,7 +502,7 @@ If `dragAndDropWindow` prop is true, the props to pass to the MessageInput compo Custom UI component to override default pinned message indicator. | Type | Default | -|-----------|------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------- | | component | | ### QuotedMessage @@ -476,7 +510,7 @@ Custom UI component to override default pinned message indicator. Custom UI component to override quoted message UI on a sent message. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### QuotedMessagePreview @@ -484,7 +518,7 @@ Custom UI component to override quoted message UI on a sent message. Custom UI component to override the message input's quoted message preview. | Type | Default | -|-----------|----------------------------------------------------------------------------------------------| +| --------- | -------------------------------------------------------------------------------------------- | | component | | ### ReactionSelector @@ -492,7 +526,7 @@ Custom UI component to override the message input's quoted message preview. Custom UI component to display the reaction selector. | Type | Default | -|-----------|-----------------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------------- | | component | | ### ReactionsList @@ -500,7 +534,7 @@ Custom UI component to display the reaction selector. Custom UI component to display the list of reactions on a message. | Type | Default | -|-----------|-----------------------------------------------------------------------------| +| --------- | --------------------------------------------------------------------------- | | component | | ### SendButton @@ -508,15 +542,15 @@ Custom UI component to display the list of reactions on a message. Custom UI component for send button. | Type | Default | -|-----------|---------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------- | | component | | ### shouldGenerateVideoThumbnail You can turn on/off thumbnail generation for video attachments -| Type | -| -------- | +| Type | +| --------- | | `boolean` | ### skipMessageDataMemoization @@ -524,7 +558,7 @@ You can turn on/off thumbnail generation for video attachments If true, skips the message data string comparison used to memoize the current channel messages (helpful for channels with 1000s of messages). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### ThreadHead @@ -532,32 +566,31 @@ If true, skips the message data string comparison used to memoize the current ch Custom UI component to be displayed at the beginning of a thread. By default, it is the thread parent message. It is composed of [Message](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/Message.tsx) context provider component and [ThreadStart](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Thread/ThreadStart.tsx) component. The latter can be customized by passing custom component to `Channel` props. The `ThreadHead` component defaults to and accepts the same props as [MessageSimple](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageSimple.tsx). | Type | Default | -|-----------|--------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------ | | component | | - ### ThreadHeader Custom UI component to display the header of a `Thread`. | Type | Default | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | component | | ### ThreadInput -Custom UI component to replace the `MessageInput` of a `Thread`. For the applications using [theme version 1](../../guides/theming/css-and-theming.mdx), the default is `MessageInputSmall`. Applications using [theme version 2](../../theming/introduction.mdx) will use `MessageInputFlat` by default. +Custom UI component to replace the `MessageInput` of a `Thread`. For the applications using [theme version 1](../../guides/theming/css-and-theming.mdx), the default is `MessageInputSmall`. Applications using [theme version 2](../../theming/introduction.mdx) will use `MessageInputFlat` by default. -| Type | Default | -|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| component | / | +| Type | Default | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| component | / | ### ThreadStart Custom UI component to display the start of a threaded `MessageList`. | Type | Default | -|-----------|------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------- | | component | | ### TriggerProvider @@ -565,7 +598,7 @@ Custom UI component to display the start of a threaded `MessageList`. Optional context provider that lets you override the default autocomplete triggers. | Type | Default | -|-----------|--------------------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------------------ | | component | | ### TypingIndicator @@ -573,7 +606,7 @@ Optional context provider that lets you override the default autocomplete trigge Custom UI component for the typing indicator. | Type | Default | -|-----------|---------------------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------------------- | | component | | ### videoAttachmentSizeHandler @@ -589,5 +622,5 @@ A custom function to provide size configuration for video attachments Custom UI component to display a message in the `VirtualizedMessageList`. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | diff --git a/docusaurus/docs/React/components/core-components/chat.mdx b/docusaurus/docs/React/components/core-components/chat.mdx index fb83b1be8..3d05ad135 100644 --- a/docusaurus/docs/React/components/core-components/chat.mdx +++ b/docusaurus/docs/React/components/core-components/chat.mdx @@ -68,7 +68,7 @@ for implementation assistance. If true, toggles the CSS variables to the default dark mode color palette. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### defaultLanguage @@ -76,7 +76,7 @@ If true, toggles the CSS variables to the default dark mode color palette. Sets the default fallback language for UI component translation, defaults to 'en' for English. | Type | Default | -|--------|---------| +| ------ | ------- | | string | 'en' | ### i18nInstance diff --git a/docusaurus/docs/React/components/core-components/thread.mdx b/docusaurus/docs/React/components/core-components/thread.mdx index e3ead4506..81e29875c 100644 --- a/docusaurus/docs/React/components/core-components/thread.mdx +++ b/docusaurus/docs/React/components/core-components/thread.mdx @@ -126,7 +126,7 @@ const CustomThreadHead = (props) => { Additional props to be passed to the underlying [`MessageInput`](../message-input-components/message-input.mdx) component. | Type | -|--------| +| ------ | | object | ### additionalMessageListProps @@ -134,7 +134,7 @@ Additional props to be passed to the underlying [`MessageInput`](../message-inpu Additional props to be passed to the underlying [`MessageList`](./message-list.mdx) component. | Type | -|--------| +| ------ | | object | ### additionalParentMessageProps @@ -143,7 +143,7 @@ Additional props to be passed to the underlying [`Message`](../message-component thread's parent message. | Type | -|--------| +| ------ | | object | ### additionalVirtualizedMessageListProps @@ -151,7 +151,7 @@ thread's parent message. Additional [props for `VirtualizedMessageList`](../virtualized_list/#props) component. | Type | -|--------| +| ------ | | object | ### autoFocus @@ -159,7 +159,7 @@ Additional [props for `VirtualizedMessageList`](../virtualized_list/#props) comp If true, focuses the `MessageInput` component on opening a thread. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### enableDateSeparator @@ -167,7 +167,7 @@ If true, focuses the `MessageInput` component on opening a thread. Controls injection of UI component into underlying `MessageList` or `VirtualizedMessageList`. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### fullWidth @@ -175,23 +175,23 @@ Controls injection of / | +| Type | Default | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| component | / | ### Message Custom thread message UI component used to override the default `Message` value stored in `ComponentContext`. | Type | Default | -|-----------|--------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------ | | component | [ComponentContext['Message']](../contexts/component-context.mdx#message) | ### messageActions @@ -199,7 +199,7 @@ Custom thread message UI component used to override the default `Message` value Array of allowed message actions (ex: 'edit', 'delete', 'reply'). To disable all actions, provide an empty array. | Type | Default | -|-------|----------------------------------------------------------------------| +| ----- | -------------------------------------------------------------------- | | array | ['edit', 'delete', 'flag', 'mute', 'pin', 'quote', 'react', 'reply'] | ### virtualized @@ -207,5 +207,5 @@ Array of allowed message actions (ex: 'edit', 'delete', 'reply'). To disable all If true, render the `VirtualizedMessageList` instead of the standard `MessageList` component. | Type | -|---------| +| ------- | | boolean | diff --git a/docusaurus/docs/React/components/core-components/virtualized-list.mdx b/docusaurus/docs/React/components/core-components/virtualized-list.mdx index be36abb9e..262dbb1fc 100644 --- a/docusaurus/docs/React/components/core-components/virtualized-list.mdx +++ b/docusaurus/docs/React/components/core-components/virtualized-list.mdx @@ -78,12 +78,20 @@ The `VirtualizedMessageList` internally creates a mapping of message id to a sty ## Props +### additionalMessageInputProps + +Additional props to be passed to the `MessageInput` component, [available props](../message-input-components/message-input.mdx/#props). It is rendered when editing a message. + +| Type | +| ------ | +| object | + ### additionalVirtuosoProps Additional props to be passed the underlying [`react-virtuoso` virtualized list dependency](https://virtuoso.dev/virtuoso-api-reference/). | Type | -|--------| +| ------ | | object | ### closeReactionSelectorOnClick @@ -91,7 +99,7 @@ Additional props to be passed the underlying [`react-virtuoso` virtualized list If true, picking a reaction from the `ReactionSelector` component will close the selector. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### customMessageRenderer @@ -99,7 +107,7 @@ If true, picking a reaction from the `ReactionSelector` component will close the Custom message render function, overrides the default `messageRenderer` function defined in the component. | Type | -|--------------------------------------------------------------------| +| ------------------------------------------------------------------ | | ( messages: StreamMessage[], index: number ) => React.ReactElement | ### defaultItemHeight @@ -107,7 +115,7 @@ Custom message render function, overrides the default `messageRenderer` function If set, the default item height is used for the calculation of the total list height. Use if you expect messages with a lot of height variance. | Type | -|--------| +| ------ | | number | ### disableDateSeparator @@ -115,7 +123,7 @@ If set, the default item height is used for the calculation of the total list he If true, disables the injection of date separator UI components. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### groupStyles @@ -123,16 +131,15 @@ If true, disables the injection of date separator UI components. Callback function to set group styles for each message. | Type | -|----------------------------------------------------------------------------------------------------------------------------| +| -------------------------------------------------------------------------------------------------------------------------- | | (message: StreamMessage, previousMessage: StreamMessage, nextMessage: StreamMessage, noGroupByUser: boolean) => GroupStyle | - ### hasMore Whether the list has more items to load. | Type | Default | -|---------|--------------------------------------------------------------------------------------| +| ------- | ------------------------------------------------------------------------------------ | | boolean | [ChannelStateContextValue['hasMore']](../contexts/channel-state-context.mdx#hasmore) | ### hideDeletedMessages @@ -156,7 +163,7 @@ If true, hides the `DateSeparator` component that renders when new messages are Whether the list is currently loading more items. | Type | Default | -|---------|----------------------------------------------------------------------------------------------| +| ------- | -------------------------------------------------------------------------------------------- | | boolean | [ChannelStateContextValue['loadingMore']](../contexts/channel-state-context.mdx#loadingmore) | ### loadMore @@ -164,7 +171,7 @@ Whether the list is currently loading more items. Function called when more messages are to be loaded, provide your own function to override the handler stored in context. | Type | Default | -|----------|------------------------------------------------------------------------------------------| +| -------- | ---------------------------------------------------------------------------------------- | | function | [ChannelActionContextValue['loadMore']](../contexts/channel-action-context.mdx#loadmore) | ### Message @@ -172,7 +179,7 @@ Function called when more messages are to be loaded, provide your own function t Custom UI component to display an individual message. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### messageLimit @@ -180,7 +187,7 @@ Custom UI component to display an individual message. The limit to use when paginating messages (the page size). | Type | Default | -|--------|---------| +| ------ | ------- | | number | 100 | ### messages @@ -188,7 +195,7 @@ The limit to use when paginating messages (the page size). The messages to render in the list, provide your own array to override the data stored in context. | Type | Default | -|-------|----------------------------------------------------------------------------------------| +| ----- | -------------------------------------------------------------------------------------- | | array | [ChannelStateContextValue['messages']](../contexts/channel-state-context.mdx#messages) | ### overscan @@ -196,7 +203,7 @@ The messages to render in the list, provide your own array to override the data The amount of extra content the list should render in addition to what's necessary to fill in the viewport. | Type | Default | -|--------|---------| +| ------ | ------- | | number | 0 | ### returnAllReadData @@ -204,7 +211,7 @@ The amount of extra content the list should render in addition to what's necessa Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### scrollSeekPlaceHolder @@ -212,7 +219,7 @@ Keep track of read receipts for each message sent by the user. When disabled, on Custom data passed to the list that determines when message placeholders should be shown during fast scrolling. | Type | -|--------| +| ------ | | object | ### scrollToLatestMessageOnFocus @@ -220,7 +227,7 @@ Custom data passed to the list that determines when message placeholders should If true, the list will scroll to the latest message when the window regains focus. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### shouldGroupByUser @@ -228,7 +235,7 @@ If true, the list will scroll to the latest message when the window regains focu If true, group messages belonging to the same user, otherwise show each message individually. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### separateGiphyPreview @@ -236,7 +243,7 @@ If true, group messages belonging to the same user, otherwise show each message If true, the Giphy preview will render as a separate component above the `MessageInput`, rather than inline with the other messages in the list. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### stickToBottomScrollBehavior @@ -253,5 +260,5 @@ The scroll-to behavior when new messages appear. Use `'smooth'` for regular chat If true, indicates that the current `VirtualizedMessageList` component is part of a `Thread`. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | diff --git a/docusaurus/docs/React/components/message-components/message-ui.mdx b/docusaurus/docs/React/components/message-components/message-ui.mdx index 69527561a..95fcd0e42 100644 --- a/docusaurus/docs/React/components/message-components/message-ui.mdx +++ b/docusaurus/docs/React/components/message-components/message-ui.mdx @@ -104,15 +104,15 @@ The `themeVersion` `'2'` markup is a bit different: // highlight-next-line - + -// highlight-start + // highlight-start
-// highlight-end + // highlight-end ``` @@ -137,7 +137,6 @@ In thread button opening thread is omitted. The drop-down menu contains a default list of actions that are enabled for a message. These are determined by the permissions the user has. On the other hand, it is also possible to specify [own custom actions](./#custommessageactions). This will lead to adding more items into the drop-down menu. - ## Props :::note @@ -152,7 +151,7 @@ you wish to override a single prop, so all options are detailed below. If true, actions such as edit, delete, flag, etc. are enabled on the message (overrides the value stored in `MessageContext`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### additionalMessageInputProps @@ -161,7 +160,7 @@ Additional props to be passed to the underlying [`MessageInput`](../message-inpu while editing (overrides the value stored in `MessageContext`). | Type | -|--------| +| ------ | | object | ### autoscrollToBottom @@ -172,21 +171,22 @@ You can even use the function to keep the container scrolled to the bottom while ```tsx const Image = (props: ImageProps) => { - ... - const { autoscrollToBottom } = useMessageContext(); - ... - - return ( - -} + ... + const { autoscrollToBottom } = useMessageContext(); + ... + + return ( + + ); +}; ``` | Type | -|------------| +| ---------- | | () => void | ### clearEditingState @@ -194,7 +194,7 @@ const Image = (props: ImageProps) => { When called, this function will exit the editing state on the message (overrides the function stored in `MessageContext`). | Type | -|--------------------------------------------| +| ------------------------------------------ | | (event?: React.BaseSyntheticEvent) => void | ### customMessageActions @@ -204,7 +204,7 @@ An object containing custom message actions (key) and function handlers (value) ```jsx const customActions = { 'Copy text': (message) => { - navigator.clipboard.writeText(message.text || '') + navigator.clipboard.writeText(message.text || ''); }, }; @@ -212,10 +212,15 @@ const customActions = { ``` Custom action item "Copy text" in the message actions box: -Image of a custom action item "Copy text" in the message actions box + +Image of a custom action item "Copy text" in the message actions box | Type | -|--------| +| ------ | | object | ### editing @@ -223,7 +228,7 @@ Custom action item "Copy text" in the message actions box: If true, the message toggles to an editing state (overrides the value stored in `MessageContext`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### formatDate @@ -231,7 +236,7 @@ If true, the message toggles to an editing state (overrides the value stored in Overrides the default date formatting logic, has access to the original date object (overrides the function stored in `MessageContext`). | Type | -|------------------------| +| ---------------------- | | (date: Date) => string | ### getMessageActions @@ -239,7 +244,7 @@ Overrides the default date formatting logic, has access to the original date obj Function that returns an array of the allowed actions on a message by the currently connected user (overrides the function stored in `MessageContext`). | Type | -|---------------------------| +| ------------------------- | | () => MessageActionsArray | ### groupByUser @@ -247,7 +252,7 @@ Function that returns an array of the allowed actions on a message by the curren If true, group messages sent by each user (only used in the `VirtualizedMessageList`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### groupStyles @@ -335,7 +340,7 @@ Function that retries sending a message after a failed request (overrides the fu When true, signifies the message is the parent message in a thread list (overrides the value stored in `MessageContext`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### isMyMessage @@ -343,7 +348,7 @@ When true, signifies the message is the parent message in a thread list (overrid Function that returns whether a message belongs to the current user (overrides the function stored in `MessageContext`). | Type | -|---------------| +| ------------- | | () => boolean | ### isReactionEnabled @@ -351,7 +356,7 @@ Function that returns whether a message belongs to the current user (overrides t If true, reactions are enabled in the currently active channel (overrides the value stored in `MessageContext`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### lastReceivedId @@ -359,7 +364,7 @@ If true, reactions are enabled in the currently active channel (overrides the va The latest message ID in the current channel (overrides the value stored in `MessageContext`). | Type | -|--------| +| ------ | | string | ### message @@ -367,7 +372,7 @@ The latest message ID in the current channel (overrides the value stored in `Mes The `StreamChat` message object, which provides necessary data to the underlying UI components (overrides the value stored in `MessageContext`). | Type | -|--------| +| ------ | | object | ### messageListRect @@ -375,7 +380,7 @@ The `StreamChat` message object, which provides necessary data to the underlying DOMRect object linked to the parent `MessageList` component (overrides the value stored in `MessageContext`). | Type | -|---------| +| ------- | | DOMRect | ### mutes @@ -383,7 +388,7 @@ DOMRect object linked to the parent `MessageList` component (overrides the value An array of users that have been muted by the connected user (overrides the value stored in `MessageContext`). | Type | Default | -|--------|-----------------------------------------------------------------------| +| ------ | --------------------------------------------------------------------- | | Mute[] | [ChannelStateContext['mutes']](../contexts/channel-state-context.mdx) | ### onMentionsClickMessage @@ -431,7 +436,7 @@ Function that runs on hover of a user avatar (overrides the function stored in ` The user roles allowed to pin messages in various channel types (deprecated in favor of `channelCapabilities`). | Type | Default | -|--------|----------------------------------------------------------------------------------------------------------------------| +| ------ | -------------------------------------------------------------------------------------------------------------------- | | object | [defaultPinPermissions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/utils.tsx) | ### reactionSelectorRef @@ -439,7 +444,7 @@ The user roles allowed to pin messages in various channel types (deprecated in f Ref to be placed on the reaction selector component (overrides the ref stored in `MessageContext`). | Type | -|-----------------------------------------| +| --------------------------------------- | | React.MutableRefObject | ### readBy @@ -447,7 +452,7 @@ Ref to be placed on the reaction selector component (overrides the ref stored in An array of users that have read the current message (overrides the value stored in `MessageContext`). | Type | -|-------| +| ----- | | array | ### renderText @@ -455,7 +460,7 @@ An array of users that have read the current message (overrides the value stored Custom function to render message text content (overrides the function stored in `MessageContext`). | Type | Default | -|----------|----------------------------------------------------------------------------------------| +| -------- | -------------------------------------------------------------------------------------- | | function | [renderText](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) | ### setEditingState @@ -471,7 +476,7 @@ Function to toggle the editing state on a message (overrides the function stored When true, show the reactions list component (overrides the value stored in `MessageContext`). | Type | -|---------| +| ------- | | boolean | ### threadList @@ -479,7 +484,7 @@ When true, show the reactions list component (overrides the value stored in `Mes If true, indicates that the current `MessageList` component is part of a `Thread` (overrides the value stored in `MessageContext`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### unsafeHTML @@ -487,5 +492,5 @@ If true, indicates that the current `MessageList` component is part of a `Thread If true, renders HTML instead of markdown. Posting HTML is only supported server-side (overrides the value stored in `MessageContext`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | diff --git a/docusaurus/docs/React/components/message-components/message.mdx b/docusaurus/docs/React/components/message-components/message.mdx index c41c7d188..8a0c75b9c 100644 --- a/docusaurus/docs/React/components/message-components/message.mdx +++ b/docusaurus/docs/React/components/message-components/message.mdx @@ -35,7 +35,7 @@ component. The Message UI component is passed as the `Message` prop into either The `StreamChat` message object, which provides necessary data to the underlying UI components. | Type | -|--------| +| ------ | | object | ### additionalMessageInputProps @@ -43,7 +43,7 @@ The `StreamChat` message object, which provides necessary data to the underlying Additional props to be passed to the underlying `MessageInput` component, [available props](../message-input-components/message-input.mdx/#props). It is rendered when editing a message. | Type | -|--------| +| ------ | | object | ### autoscrollToBottom @@ -51,7 +51,7 @@ Additional props to be passed to the underlying `MessageInput` component, [avail Call this function to keep message list scrolled to the bottom when the message list container scroll height increases (only available in the `VirtualizedMessageList`). An example use case is that upon user's interaction with the application, a new element appears below the last message. In order to keep the newly rendered content visible, the `autoscrollToBottom` function can be called. The container, however, is not scrolled to the bottom, if already scrolled up more than 4px from the bottom. The function is provided by the SDK and is not inteded for customization. | Type | -|------------| +| ---------- | | () => void | ### closeReactionSelectorOnClick @@ -59,7 +59,7 @@ Call this function to keep message list scrolled to the bottom when the message If true, picking a reaction from the `ReactionSelector` component will close the selector. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### customMessageActions @@ -69,7 +69,7 @@ An object containing custom message actions (key) and function handlers (value). ```jsx const customActions = { 'Copy text': (message) => { - navigator.clipboard.writeText(message.text || '') + navigator.clipboard.writeText(message.text || ''); }, }; @@ -77,12 +77,17 @@ const customActions = { ``` Custom action item "Copy text" in the message actions box: -Image of a custom action item "Copy text" in the message actions box + +Image of a custom action item "Copy text" in the message actions box ```jsx const customActions = { 'Copy text': (message) => { - navigator.clipboard.writeText(message.text || '') + navigator.clipboard.writeText(message.text || ''); }, }; @@ -90,11 +95,15 @@ const customActions = { ``` Custom action item "Copy text" in the message actions box: -Image of a custom action item "Copy text" in the message actions box +Image of a custom action item "Copy text" in the message actions box | Type | -|--------| +| ------ | | object | ### disableQuotedMessages @@ -102,7 +111,7 @@ Custom action item "Copy text" in the message actions box: If true, disables the ability for users to quote messages. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### endOfGroup @@ -110,7 +119,7 @@ If true, disables the ability for users to quote messages. When true, the message is the last one in a group sent by a specific user (only used in the `VirtualizedMessageList`). | Type | -|---------| +| ------- | | boolean | ### firstOfGroup @@ -118,7 +127,7 @@ When true, the message is the last one in a group sent by a specific user (only When true, the message is the first one in a group sent by a specific user (only used in the `VirtualizedMessageList`). | Type | -|---------| +| ------- | | boolean | ### formatDate @@ -126,7 +135,7 @@ When true, the message is the first one in a group sent by a specific user (only Overrides the default date formatting logic, has access to the original date object. | Type | -|------------------------| +| ---------------------- | | (date: Date) => string | ### getDeleteMessageErrorNotification @@ -135,7 +144,7 @@ Function that returns the notification text to be displayed when a delete messag deleted [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument. | Type | -|------------------------------------| +| ---------------------------------- | | (message: StreamMessage) => string | ### getFlagMessageErrorNotification @@ -144,7 +153,7 @@ Function that returns the notification text to be displayed when a flag message flagged [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument. | Type | -|------------------------------------| +| ---------------------------------- | | (message: StreamMessage) => string | ### getFlagMessageSuccessNotification @@ -153,7 +162,7 @@ Function that returns the notification text to be displayed when a flag message flagged [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument. | Type | -|------------------------------------| +| ---------------------------------- | | (message: StreamMessage) => string | ### getMuteUserErrorNotification @@ -162,7 +171,7 @@ Function that returns the notification text to be displayed when a mute user req muted [user object](https://getstream.io/chat/docs/javascript/update_users/?language=javascript) as its argument. | Type | -|--------------------------------| +| ------------------------------ | | (user: UserResponse) => string | ### getMuteUserSuccessNotification @@ -171,7 +180,7 @@ Function that returns the notification text to be displayed when a mute user req muted [user object](https://getstream.io/chat/docs/javascript/update_users/?language=javascript) as its argument. | Type | -|--------------------------------| +| ------------------------------ | | (user: UserResponse) => string | ### getPinMessageErrorNotification @@ -180,7 +189,7 @@ Function that returns the notification text to be displayed when a pin message r pinned [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument. | Type | -|------------------------------------| +| ---------------------------------- | | (message: StreamMessage) => string | ### groupedByUser @@ -188,7 +197,7 @@ pinned [message object](https://getstream.io/chat/docs/javascript/message_format If true, group messages sent by each user (only used in the `VirtualizedMessageList`). | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### groupStyles @@ -204,7 +213,7 @@ An array of potential styles to apply to a grouped message (ex: top, bottom, sin Whether to highlight and focus the message on load. Used internally in the process of [jumping to a message](../contexts/channel-action-context.mdx/#jumptomessage). | Type | -|---------| +| ------- | | boolean | ### initialMessage @@ -212,7 +221,7 @@ Whether to highlight and focus the message on load. Used internally in the proce When true, signifies the message is the parent message in a thread list. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### lastReceivedId @@ -220,7 +229,7 @@ When true, signifies the message is the parent message in a thread list. The latest message ID in the current channel. | Type | -|--------| +| ------ | | string | ### Message @@ -228,7 +237,7 @@ The latest message ID in the current channel. Custom UI component to display a message. | Type | Default | -|-----------|---------------------------------------------------------------------------| +| --------- | ------------------------------------------------------------------------- | | component | | ### messageActions @@ -244,7 +253,7 @@ Array of allowed message actions (ex: 'edit', 'delete', 'reply'). To disable all DOMRect object linked to the parent wrapper div around the `InfiniteScroll` component. | Type | -|---------| +| ------- | | DOMRect | ### onlySenderCanEdit @@ -252,7 +261,7 @@ DOMRect object linked to the parent wrapper div around the `InfiniteScroll` comp If true, only the sender of the message has editing privileges. If `false` also channel capability `update-any-message` has to be enabled in order a user can edit other users' messages. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### onMentionsClick @@ -260,7 +269,7 @@ If true, only the sender of the message has editing privileges. If `false` also Custom action handler function to run on click of a @mention in a message. | Type | Default | -|----------|--------------------------------------------------------------------------------------------------------| +| -------- | ------------------------------------------------------------------------------------------------------ | | function | [ChannelActionContextValue['onMentionsClick']](../contexts/channel-action-context.mdx#onmentionsclick) | ### onMentionsHover @@ -268,7 +277,7 @@ Custom action handler function to run on click of a @mention in a message. Custom action handler function to run on hover over a @mention in a message. | Type | Default | -|----------|--------------------------------------------------------------------------------------------------------| +| -------- | ------------------------------------------------------------------------------------------------------ | | function | [ChannelActionContextValue['onMentionsHover']](../contexts/channel-action-context.mdx#onmentionshover) | ### onUserClick @@ -276,7 +285,7 @@ Custom action handler function to run on hover over a @mention in a message. Custom action handler function to run on click of user avatar. | Type | -|-------------------------------------------------------| +| ----------------------------------------------------- | | (event: React.BaseSyntheticEvent, user: User) => void | ### onUserHover @@ -284,7 +293,7 @@ Custom action handler function to run on click of user avatar. Custom action handler function to run on hover of user avatar. | Type | -|-------------------------------------------------------| +| ----------------------------------------------------- | | (event: React.BaseSyntheticEvent, user: User) => void | ### openThread @@ -292,7 +301,7 @@ Custom action handler function to run on hover of user avatar. Custom action handler to open a [`Thread`](../core-components/thread.mdx) component. | Type | Default | -|----------|----------------------------------------------------------------------------------------------| +| -------- | -------------------------------------------------------------------------------------------- | | function | [ChannelActionContextValue['openThread']](../contexts/channel-action-context.mdx#openthread) | ### pinPermissions @@ -300,7 +309,7 @@ Custom action handler to open a [`Thread`](../core-components/thread.mdx) compon The user roles allowed to pin messages in various channel types (deprecated in favor of `channelCapabilities`). | Type | Default | -|--------|----------------------------------------------------------------------------------------------------------------------| +| ------ | -------------------------------------------------------------------------------------------------------------------- | | object | [defaultPinPermissions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/utils.tsx) | ### readBy @@ -308,7 +317,7 @@ The user roles allowed to pin messages in various channel types (deprecated in f An array of users that have read the current message. | Type | -|-------| +| ----- | | array | ### renderText @@ -316,7 +325,7 @@ An array of users that have read the current message. Custom function to render message text content. | Type | Default | -|----------|----------------------------------------------------------------------------------------| +| -------- | -------------------------------------------------------------------------------------- | | function | [renderText](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) | ### retrySendMessage @@ -324,7 +333,7 @@ Custom function to render message text content. Custom action handler to retry sending a message after a failed request. | Type | Default | -|----------|----------------------------------------------------------------------------------------------------------| +| -------- | -------------------------------------------------------------------------------------------------------- | | function | [ChannelActionContextValue['retrySendMessage']](../contexts/channel-action-context.mdx#retrysendmessage) | ### threadList @@ -332,7 +341,7 @@ Custom action handler to retry sending a message after a failed request. If true, indicates that the current `MessageList` component is part of a `Thread`. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ### unsafeHTML @@ -340,5 +349,5 @@ If true, indicates that the current `MessageList` component is part of a `Thread If true, renders HTML instead of markdown. Posting HTML is only supported server-side. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | diff --git a/docusaurus/docs/React/components/message-components/reactions.mdx b/docusaurus/docs/React/components/message-components/reactions.mdx index 460c2a13c..e0559e677 100644 --- a/docusaurus/docs/React/components/message-components/reactions.mdx +++ b/docusaurus/docs/React/components/message-components/reactions.mdx @@ -136,7 +136,7 @@ const CustomReactionsList = (props) => { Additional props to be passed to the [NimbleEmoji](https://github.com/missive/emoji-mart/blob/master/src/components/emoji/nimble-emoji.js) component from `emoji-mart`. | Type | -|--------| +| ------ | | object | ### Avatar @@ -144,7 +144,7 @@ Additional props to be passed to the [NimbleEmoji](https://github.com/missive/em Custom UI component to display a user's avatar. | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### detailedView @@ -152,15 +152,15 @@ Custom UI component to display a user's avatar. If true, shows the user's avatar with the reaction. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### handleReaction Function that adds/removes a reaction on a message (overrides the function stored in `MessageContext`). -| Type | Default | -|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| Type | Default | +| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | (reactionType: string, event: React.BaseSyntheticEvent) => Promise | [MessageContextValue['handleReaction']](./components/contexts/message-context.mdx#handlereaction) | ### latest_reactions @@ -168,7 +168,7 @@ Function that adds/removes a reaction on a message (overrides the function store An array of the reaction objects to display in the list (overrides `message.latest_reactions` from `MessageContext`). | Type | -|-------| +| ----- | | array | ### own_reactions @@ -176,7 +176,7 @@ An array of the reaction objects to display in the list (overrides `message.late An array of own reaction objects to display in the list (overrides `message.own_reactions` from `MessageContext`). | Type | -|-------| +| ----- | | array | ### reaction_counts @@ -184,7 +184,7 @@ An array of own reaction objects to display in the list (overrides `message.own_ An object that keeps track of the count of each type of reaction on a message (overrides `message.reaction_counts` from `MessageContext`). | Type | -|---------------------------------| +| ------------------------------- | | { [key: reactionType]: number } | ### reactionOptions @@ -192,7 +192,7 @@ An object that keeps track of the count of each type of reaction on a message (o A list of the currently supported reactions on a message. | Type | Default | -|-------|-----------------------------------------------------------------------------| +| ----- | --------------------------------------------------------------------------- | | array | | ### reverse @@ -200,7 +200,7 @@ A list of the currently supported reactions on a message. If true, adds a CSS class that reverses the horizontal positioning of the selector. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ## ReactionsList Props @@ -210,15 +210,15 @@ If true, adds a CSS class that reverses the horizontal positioning of the select Additional props to be passed to the [NimbleEmoji](https://github.com/missive/emoji-mart/blob/master/src/components/emoji/nimble-emoji.js) component from `emoji-mart`. | Type | -|--------| +| ------ | | object | ### onClick Custom on click handler for an individual reaction in the list (overrides the function stored in `MessageContext`). -| Type | Default | -| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Type | Default | +| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | (event: React.BaseSyntheticEvent) => Promise \| void | [MessageContextValue['onReactionListClick']](./components/contexts/message-context.mdx#onreactionlistclick) | ### own_reactions @@ -234,7 +234,7 @@ An array of the own reaction objects to distinguish own reactions visually (over An object that keeps track of the count of each type of reaction on a message (overrides `message.reaction_counts` from `MessageContext`). | Type | -|---------------------------------| +| ------------------------------- | | { [key: reactionType]: number } | ### reactionOptions @@ -242,7 +242,7 @@ An object that keeps track of the count of each type of reaction on a message (o A list of the currently supported reactions on a message. | Type | Default | -|-------|-----------------------------------------------------------------------------| +| ----- | --------------------------------------------------------------------------- | | array | | ### reactions @@ -250,7 +250,7 @@ A list of the currently supported reactions on a message. An array of the reaction objects to display in the list (overrides `message.latest_reactions` from `MessageContext`). | Type | -|-------| +| ----- | | array | ### reverse @@ -258,7 +258,7 @@ An array of the reaction objects to display in the list (overrides `message.late If true, adds a CSS class that reverses the horizontal positioning of the selector. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | ## SimpleReactionsList Props @@ -268,15 +268,15 @@ If true, adds a CSS class that reverses the horizontal positioning of the select Additional props to be passed to the [NimbleEmoji](https://github.com/missive/emoji-mart/blob/master/src/components/emoji/nimble-emoji.js) component from `emoji-mart`. | Type | -|--------| +| ------ | | object | ### handleReaction Function that adds/removes a reaction on a message (overrides the function stored in `MessageContext`). -| Type | Default | -| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| Type | Default | +| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | (reactionType: string, event: React.BaseSyntheticEvent) => Promise | [MessageContextValue['handleReaction']](./components/contexts/message-context.mdx#handlereaction) | ### own_reactions @@ -284,7 +284,7 @@ Function that adds/removes a reaction on a message (overrides the function store An array of the own reaction objects to distinguish own reactions visually (overrides `message.own_reactions` from `MessageContext`). | Type | -|-------| +| ----- | | array | ### reaction_counts @@ -292,7 +292,7 @@ An array of the own reaction objects to distinguish own reactions visually (over An object that keeps track of the count of each type of reaction on a message (overrides `message.reaction_counts` from `MessageContext`). | Type | -|---------------------------------| +| ------------------------------- | | { [key: reactionType]: number } | ### reactionOptions @@ -300,7 +300,7 @@ An object that keeps track of the count of each type of reaction on a message (o A list of the currently supported reactions on a message. | Type | Default | -|-------|-----------------------------------------------------------------------------| +| ----- | --------------------------------------------------------------------------- | | array | | ### reactions @@ -308,5 +308,5 @@ A list of the currently supported reactions on a message. An array of the reaction objects to display in the list (overrides `message.latest_reactions` from `MessageContext`). | Type | -|-------| +| ----- | | array | diff --git a/docusaurus/docs/React/components/message-components/ui-components.mdx b/docusaurus/docs/React/components/message-components/ui-components.mdx index 4bfeaad1b..d3ea12b45 100644 --- a/docusaurus/docs/React/components/message-components/ui-components.mdx +++ b/docusaurus/docs/React/components/message-components/ui-components.mdx @@ -6,7 +6,6 @@ title: UI Components import GHComponentLink from '../../_docusaurus-components/GHComponentLink'; - As described in the [Message UI](./message-ui.mdx) section, our default component is a combination of various UI subcomponents. We export all the building blocks of `MessageSimple`, so a custom Message UI component can be built in a similar way. Check out the [Message UI Customization](../../guides/theming/message-ui.mdx) section for an example. @@ -47,7 +46,7 @@ Besides the above there are also components that render reaction list and reacti Custom component rendering the icon used in message actions button. This button invokes the message actions menu. | Type | Default | -|-----------------------|-----------------------------------------------------------------| +| --------------------- | --------------------------------------------------------------- | | `React.ComponentType` | | ### customWrapperClass @@ -55,7 +54,7 @@ Custom component rendering the icon used in message actions button. This button Custom CSS class to be added to the `div` wrapping the component. | Type | -|--------| +| ------ | | string | ### getMessageActions @@ -63,7 +62,7 @@ Custom CSS class to be added to the `div` wrapping the component. Function that returns an array of the allowed actions on a message by the currently connected user (overrides the value from `MessageContext`). | Type | -|---------------------------| +| ------------------------- | | () => MessageActionsArray | ### handleDelete @@ -103,7 +102,7 @@ Function that pins a message in the current channel (overrides the value from `M If true, renders the wrapper component as a `span`, not a `div`. | Type | Default | -|--------|---------| +| ------ | ------- | | string | false | ### message @@ -111,7 +110,7 @@ If true, renders the wrapper component as a `span`, not a `div`. The `StreamChat` message object, which provides necessary data to the underlying UI components (overrides the value from `MessageContext`). | Type | -|--------| +| ------ | | object | ### messageWrapperRef @@ -119,7 +118,7 @@ The `StreamChat` message object, which provides necessary data to the underlying React mutable ref placed on the message root `div`. It is forwarded by `MessageOptions` down to `MessageActions` ([see the example](../../guides/theming/message-ui.mdx)). | Type | -|----------------------------------| +| -------------------------------- | | React.RefObject | ### mine @@ -127,7 +126,7 @@ React mutable ref placed on the message root `div`. It is forwarded by `MessageO Function that returns whether the message was sent by the connected user. | Type | -|---------------| +| ------------- | | () => boolean | ## MessageDeleted Props @@ -137,7 +136,7 @@ Function that returns whether the message was sent by the connected user. The `StreamChat` message object, which provides necessary data to the underlying UI components. | Type | -|--------| +| ------ | | object | ## MessageOptions Props @@ -147,7 +146,7 @@ The `StreamChat` message object, which provides necessary data to the underlying Custom component rendering the icon used in message actions button. This button invokes the message actions menu. | Type | Default | -|-----------------------|-----------------------------------------------------------------| +| --------------------- | --------------------------------------------------------------- | | `React.ComponentType` | | ### displayReplies @@ -155,7 +154,7 @@ Custom component rendering the icon used in message actions button. This button If true, show the `ThreadIcon` and enable navigation into a `Thread` component. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | true | ### handleOpenThread @@ -163,7 +162,7 @@ If true, show the `ThreadIcon` and enable navigation into a `Thread` component. Function that opens a [`Thread`](../core-components/thread.mdx) on a message (overrides the value from `MessageContext`). | Type | -|-------------------------------------------------------------| +| ----------------------------------------------------------- | | (event: React.BaseSyntheticEvent) => Promise \| void | ### messageWrapperRef @@ -171,7 +170,7 @@ Function that opens a [`Thread`](../core-components/thread.mdx) on a message (ov React mutable ref that can be placed on the message root `div`. `MessageOptions` component forwards this prop to [`MessageActions`](#messageactions-props) component ([see the example](../../guides/theming/message-ui.mdx)). | Type | -|----------------------------------| +| -------------------------------- | | React.RefObject | ### ReactionIcon @@ -179,7 +178,7 @@ React mutable ref that can be placed on the message root `div`. `MessageOptions` Custom component rendering the icon used in a message options button invoking reactions selector for a given message. | Type | Default | -|-----------------------|------------------------------------------------------------------| +| --------------------- | ---------------------------------------------------------------- | | `React.ComponentType` | | ### theme @@ -191,7 +190,7 @@ Theme string to be added to CSS class names. ``` | Type | Default | -|--------|----------| +| ------ | -------- | | string | 'simple' | ### ThreadIcon @@ -199,7 +198,7 @@ Theme string to be added to CSS class names. Custom component rendering the icon used in a message options button opening thread. | Type | Default | -|-----------------------|----------------------------------------------------------------| +| --------------------- | -------------------------------------------------------------- | | `React.ComponentType` | | ## MessageRepliesCountButton Props @@ -233,7 +232,7 @@ const singleReplyText = `1 ${labelSingle}`; Function to navigate into an existing thread on a message. | Type | -|-------------------------| +| ----------------------- | | React.MouseEventHandler | ### reply_count @@ -241,7 +240,7 @@ Function to navigate into an existing thread on a message. The amount of replies (i.e., threaded messages) on a message. | Type | -|--------| +| ------ | | number | ## MessageStatus Props @@ -251,7 +250,7 @@ The amount of replies (i.e., threaded messages) on a message. Custom UI component to display a user's avatar (overrides the value from `ComponentContext`). | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### messageType @@ -263,16 +262,16 @@ Message type string to be added to CSS class names. ``` | Type | Default | -|--------|----------| +| ------ | -------- | | string | 'simple' | ### tooltipUserNameMapper Allows to customize the username(s) that appear on the message status tooltip. -| Type | Default | -| ------------------------------ | ------------------------------- | -| (user: UserResponse) => string | (user) => user.name || user.id | +| Type | Default | +| ------------------------------ | ------------------- | --- | ------- | +| (user: UserResponse) => string | (user) => user.name | | user.id | This prop's implementation is not provided out of the box by the SDK. See below for a customization example: @@ -308,7 +307,7 @@ const WrappedConnectedUser = ({ token, userId }: Omit string \| string[]) | ### grow @@ -132,7 +132,6 @@ Custom UI component handling how the message input is rendered. | --------- | ------------------------------------------------------------------------------------------------------------------------------- | | component | [MessageInputFlat](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/MessageInputFlat.tsx) | - ### maxRows Max number of rows the underlying `textarea` component is allowed to grow. @@ -230,6 +229,33 @@ shouldSubmit={shouldSubmit} ::: +### urlEnrichmentConfig + +Configuration parameters for link previews to customize: + +- link discovery, +- what actions to execute on link preview card dismissal, +- what is the debounce interval after which the link enrichment queries are run + +It also allows us to disable querying and rendering the link previews with `enrichURLForPreview` parameter. + +| Type | +| ------------------- | +| URLEnrichmentConfig | + +```typescript +export type URLEnrichmentConfig = { + /** Number of milliseconds to debounce firing the URL enrichment queries when typing. The default value is 1500(ms). */ + debounceURLEnrichmentMs?: number; + /** Allows for toggling the URL enrichment and link previews in `MessageInput`. By default, the feature is disabled. */ + enrichURLForPreview?: boolean; + /** Custom function to identify URLs in a string for later generation of link previews */ + findURLFn?: (text: string) => string[]; + /** Custom function to react to link preview dismissal */ + onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void; +}; +``` + ### useMentionsTransliteration If true, will use an optional dependency to support transliteration in the input for mentions. See: https://github.com/sindresorhus/transliterate diff --git a/docusaurus/docs/React/components/message-input-components/ui-components.mdx b/docusaurus/docs/React/components/message-input-components/ui-components.mdx index 553fc49fa..800a35d6d 100644 --- a/docusaurus/docs/React/components/message-input-components/ui-components.mdx +++ b/docusaurus/docs/React/components/message-input-components/ui-components.mdx @@ -19,6 +19,8 @@ The following UI components are available for use: - [`EmojiPicker`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/EmojiPicker.tsx) - picker component for selecting an emoji +- [`LinkPreviewList`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/LinkPreviewList.tsx) - component rendering scraped link data in a preview cards + - [`QuotedMessagePreview`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/QuotedMessagePreview.tsx) - displays a UI wrapper around the `MessageInput` when an existing message is being quoted @@ -35,7 +37,7 @@ The following UI components are available for use: Function to override the default submit handler on the underlying `textarea` component. | Type | Default | -|-------------------------------------------|------------------------------------------------------------------------------------------------| +| ----------------------------------------- | ---------------------------------------------------------------------------------------------- | | (event: React.BaseSyntheticEvent) => void | [MessageInputContextValue['handleSubmit']](../contexts/message-input-context.mdx#handlesubmit) | ### onBlur @@ -43,7 +45,7 @@ Function to override the default submit handler on the underlying `textarea` com Function to run on blur of the underlying `textarea` component. | Type | -|-----------------------------------------------| +| --------------------------------------------- | | React.FocusEventHandler | ### onChange @@ -51,7 +53,7 @@ Function to run on blur of the underlying `textarea` component. Function to override the default onChange behavior on the underlying `textarea` component. | Type | Default | -|------------------------------------------------|----------------------------------------------------------------------------------------| +| ---------------------------------------------- | -------------------------------------------------------------------------------------- | | React.ChangeEventHandler | [MessageInputContextValue['onChange']](../contexts/message-input-context.mdx#onchange) | ### onFocus @@ -59,7 +61,7 @@ Function to override the default onChange behavior on the underlying `textarea` Function to run on focus of the underlying `textarea` component. | Type | -|-----------------------------------------------| +| --------------------------------------------- | | React.FocusEventHandler | ### onPaste @@ -67,7 +69,7 @@ Function to run on focus of the underlying `textarea` component. Function to override the default onPaste behavior on the underlying `textarea` component. | Type | Default | -|-------------------------------------------------------------|--------------------------------------------------------------------------------------| +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------ | | (event: React.ClipboardEvent) => void | [MessageInputContextValue['onPaste']](../contexts/message-input-context.mdx#onpaste) | ### placeholder @@ -75,7 +77,7 @@ Function to override the default onPaste behavior on the underlying `textarea` c Placeholder for the underlying `textarea` component. | Type | Default | -|--------|---------------------| +| ------ | ------------------- | | string | 'Type your message' | ### rows @@ -83,7 +85,7 @@ Placeholder for the underlying `textarea` component. The initial number of rows for the underlying `textarea` component. | Type | Default | -|--------|---------| +| ------ | ------- | | number | 1 | ### value @@ -91,7 +93,7 @@ The initial number of rows for the underlying `textarea` component. The text value of the underlying `textarea` component. | Type | Default | -|--------|--------------------------------------------------------------------------------| +| ------ | ------------------------------------------------------------------------------ | | string | [MessageInputContextValue['text']](../contexts/message-input-context.mdx#text) | ### wordReplace @@ -99,7 +101,7 @@ The text value of the underlying `textarea` component. Function to override the default emojiReplace behavior on the `wordReplace` prop of the `textarea` component. | Type | -|----------------------------------------------------------| +| -------------------------------------------------------- | | (word: string, emojiIndex?: NimbleEmojiIndex) =\> string | ## DefaultTriggerProvider @@ -121,9 +123,19 @@ provider component to the `Channel` component via the `TriggerProvider` prop. An If true, updates the CSS class name of the `div` container and renders a smaller version of the picker. | Type | Default | -|---------|---------| +| ------- | ------- | | boolean | false | +## LinkPreviewList Props + +###
Required
linkPreviews + +An array of `LinkPreview` objects - a union type of `LinkPreviewState` and `OGAttachment`. The array is derived from `linkPreviews` Map of `MessageInputContextValue`. + +| Type | +| ------------- | +| LinkPreview[] | + ## QuotedMessagePreview Props ###
Required
quotedMessage @@ -131,7 +143,7 @@ If true, updates the CSS class name of the `div` container and renders a smaller The existing message to be quoted by the next sent message. | Type | -|--------| +| ------ | | object | ## SendButton Props @@ -141,7 +153,7 @@ The existing message to be quoted by the next sent message. Function to send a message to the currently active channel. | Type | -|-------------------------------------------| +| ----------------------------------------- | | (event: React.BaseSyntheticEvent) => void | ## UploadsPreview diff --git a/docusaurus/docs/React/components/utility-components/channel-header.mdx b/docusaurus/docs/React/components/utility-components/channel-header.mdx index cbff948b8..d4f2bcaab 100644 --- a/docusaurus/docs/React/components/utility-components/channel-header.mdx +++ b/docusaurus/docs/React/components/utility-components/channel-header.mdx @@ -56,8 +56,8 @@ A boolean for showing a little indicator below the title if the `channel` is liv A custom UI component to display menu icon. -| Type | Default | -| --------- | ----------------------------------------------------------------------------------------------------- | +| Type | Default | +| --------- | ------------------------------------------------------------------------------------------------------------- | | component | [MenuIcon](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelHeader/icons.tsx) | ### title diff --git a/docusaurus/docs/React/components/utility-components/channel-preview-ui.mdx b/docusaurus/docs/React/components/utility-components/channel-preview-ui.mdx index 0cb089c93..24c77d235 100644 --- a/docusaurus/docs/React/components/utility-components/channel-preview-ui.mdx +++ b/docusaurus/docs/React/components/utility-components/channel-preview-ui.mdx @@ -18,13 +18,12 @@ This UI component is very customizable; below is an example of how to use the `P see [ChannelPreview Code Example](../../guides/customization/channel-list-preview.mdx#the-preview-prop-component). ```tsx -const YourCustomChannelPreview = (props) => { +const YourCustomChannelPreview = (previewProps) => { // render custom preview info here + return ; }; - } -/>; +; ``` ## Props @@ -59,7 +58,7 @@ The currently selected channel object. The custom UI component to display the avatar for the channel. | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### channelUpdateCount @@ -75,7 +74,7 @@ A number that forces the update of the preview component on channel update. Custom class for the channel preview root | Type | -|--------| +| ------ | | string | ### displayImage @@ -86,8 +85,8 @@ Image of channel to display. | ------ | | string | - ### displayTitle + Title of channel to display. | Type | @@ -95,6 +94,7 @@ Title of channel to display. | string | ### lastMessage + The last message received in a channel. | Type | @@ -102,6 +102,7 @@ The last message received in a channel. | StreamMessage | ### latestMessage + Latest message preview to display. Will be either a string or a JSX.Element rendering markdown. | Type | @@ -113,7 +114,7 @@ Latest message preview to display. Will be either a string or a JSX.Element rend Custom handler invoked when the `ChannelPreview` is clicked. The SDK uses `ChannelPreview` to display items of channel search results. There, behind the scenes, the new active channel is set. | Type | -|-----------------------------------| +| --------------------------------- | | (event: React.MouseEvent) => void | ### Preview @@ -121,7 +122,7 @@ Custom handler invoked when the `ChannelPreview` is clicked. The SDK uses `Chann The UI component to use for display. | Type | Default | -|-----------|------------------------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------------------------- | | component | | ### setActiveChannel @@ -133,6 +134,7 @@ The setter function for a selected `channel`. | ChatContextValue['setActiveChannel'] | ### unread + The number of unread Messages. | Type | diff --git a/docusaurus/docs/React/components/utility-components/channel-preview.mdx b/docusaurus/docs/React/components/utility-components/channel-preview.mdx index 9e37fa147..d17228f7b 100644 --- a/docusaurus/docs/React/components/utility-components/channel-preview.mdx +++ b/docusaurus/docs/React/components/utility-components/channel-preview.mdx @@ -56,7 +56,7 @@ The currently selected channel object. The custom UI component to display the avatar for the `channel`. | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### channelUpdateCount @@ -72,16 +72,15 @@ A number that forces the update of the preview component on channel update. Custom class for the channel preview root | Type | -|--------| +| ------ | | string | - ### onSelect Custom handler invoked when the `ChannelPreview` is clicked. The SDK uses `ChannelPreview` to display items of channel search results. There, behind the scenes, the new active channel is set. | Type | -|-----------------------------------| +| --------------------------------- | | (event: React.MouseEvent) => void | ### Preview @@ -89,7 +88,7 @@ Custom handler invoked when the `ChannelPreview` is clicked. The SDK uses `Chann The UI component to use for display. | Type | Default | -|-----------|------------------------------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------------------------------- | | component | | ### setActiveChannel diff --git a/docusaurus/docs/React/components/utility-components/channel-search.mdx b/docusaurus/docs/React/components/utility-components/channel-search.mdx index 614d0cbf4..52d4ed613 100644 --- a/docusaurus/docs/React/components/utility-components/channel-search.mdx +++ b/docusaurus/docs/React/components/utility-components/channel-search.mdx @@ -3,6 +3,7 @@ id: channel_search sidebar_position: 5 title: ChannelSearch --- + import ImageShowcase from '@site/src/components/ImageShowcase'; import GHComponentLink from '../../_docusaurus-components/GHComponentLink'; @@ -56,7 +57,7 @@ If opted in the use of [theme version 2](../../theming/introduction.mdx), the `C The input naturally transitions between 3 states regardless of theme version: | Input state | Input | Search results | -|---------------|----------------------------------|----------------| +| ------------- | -------------------------------- | -------------- | | inactive | not focused | not rendered | | focused | focused and empty | not rendered | | active search | contains non-empty search string | rendered | @@ -66,13 +67,11 @@ It is possible to jump directly from active search state to inactive by pressing Once the search results container is rendered it transitions between the following states: | Search results state | Search results | -|---------------------------|-------------------------------------------------------------------| +| ------------------------- | ----------------------------------------------------------------- | | loading | the search API call is in progress | | empty search (result) | the search API call returned an empty array | | non-empty search (result) | the search API call returned an array of channel and user objects | - - ### SearchInput component The shows that the component renders a single text input element. User can provide a custom [`SearchInput`](./#searchinput) component implementation though. @@ -92,8 +91,16 @@ The `SearchBar` is rendered with [app menu icon](./#menuicon) if a custom [`AppM Inactive search bar with an app menu, alt: 'Image of an inactive search bar state with an app menu' }, - { image: ImageInactiveSearchBarWithAppMenu, caption: Inactive search bar without an app menu, alt: 'Image of an inactive search bar state without an app menu' }, + { + image: ImageInactiveSearchBarNoAppMenu, + caption: Inactive search bar with an app menu, + alt: 'Image of an inactive search bar state with an app menu', + }, + { + image: ImageInactiveSearchBarWithAppMenu, + caption: Inactive search bar without an app menu, + alt: 'Image of an inactive search bar state without an app menu', + }, ]} /> @@ -104,8 +111,16 @@ Once the input is focused, a return-arrow button occurs with [`ExitSearchIcon`]( Active search bar with empty input, alt: 'Image of an active search bar state with empty input' }, - { image: ImageActiveSearchBarWithText, caption: Active search bar with search query, alt: 'Image of an active search bar with search query' }, + { + image: ImageActiveSearchBarNoText, + caption: Active search bar with empty input, + alt: 'Image of an active search bar state with empty input', + }, + { + image: ImageActiveSearchBarWithText, + caption: Active search bar with search query, + alt: 'Image of an active search bar with search query', + }, ]} /> @@ -128,10 +143,26 @@ The default styling of the first two states are as follows: Search results list content when loading (theme v1), alt: 'Image of search results list content when loading' }, - { image: ImageSearchResultsEmptyThemeV1, caption: Empty search results (theme v1), alt: 'Image of empty search results' }, - { image: ImageSearchResultsLoadingThemeV2, caption: Search results list content when loading (theme v2), alt: 'Image of search results list content when loading' }, - { image: ImageSearchResultsEmptyThemeV2, caption: Empty search results (theme v2), alt: 'Image of empty search results' }, + { + image: ImageSearchResultsLoadingThemeV1, + caption: Search results list content when loading (theme v1), + alt: 'Image of search results list content when loading', + }, + { + image: ImageSearchResultsEmptyThemeV1, + caption: Empty search results (theme v1), + alt: 'Image of empty search results', + }, + { + image: ImageSearchResultsLoadingThemeV2, + caption: Search results list content when loading (theme v2), + alt: 'Image of search results list content when loading', + }, + { + image: ImageSearchResultsEmptyThemeV2, + caption: Empty search results (theme v2), + alt: 'Image of empty search results', + }, ]} /> @@ -142,26 +173,41 @@ The search results can be rendered in place of the channel list or above the cha Search results displayed inline (theme v1), alt: 'Image of search results displayed inline (theme v1)' }, - { image: ImageSearchResultsPopupThemeV1, caption: Search results displayed floating above the channel list (theme v1), alt: 'Image of search results displayed floating above the channel list (theme v1)' }, - { image: ImageSearchResultsInlineThemeV2, caption: Search results displayed inline (theme v2), alt: 'Image of search results displayed inline (theme v2)' }, - { image: ImageSearchResultsPopupThemeV2, caption: Search results displayed floating above the channel list (theme v2), alt: 'Image of search results displayed floating above the channel list (theme v2)' }, + { + image: ImageSearchResultsInlineThemeV1, + caption: Search results displayed inline (theme v1), + alt: 'Image of search results displayed inline (theme v1)', + }, + { + image: ImageSearchResultsPopupThemeV1, + caption: Search results displayed floating above the channel list (theme v1), + alt: 'Image of search results displayed floating above the channel list (theme v1)', + }, + { + image: ImageSearchResultsInlineThemeV2, + caption: Search results displayed inline (theme v2), + alt: 'Image of search results displayed inline (theme v2)', + }, + { + image: ImageSearchResultsPopupThemeV2, + caption: Search results displayed floating above the channel list (theme v2), + alt: 'Image of search results displayed floating above the channel list (theme v2)', + }, ]} /> #### Keep the search results open on channel select -The `ChannelSearch` offers possibility to keep the search results open meanwhile the user clicks between the search results. This behavior is controlled by [`clearSearchOnClickOutside`](./#clearsearchonclickoutside) flag. The selected channel is added to the channel list if it was not present there before the search. - +The `ChannelSearch` offers possibility to keep the search results open meanwhile the user clicks between the search results. This behavior is controlled by [`clearSearchOnClickOutside`](./#clearsearchonclickoutside) flag. The selected channel is added to the channel list if it was not present there before the search. ## Props ### AppMenu -Application menu / drop-down to be displayed when clicked on [`MenuIcon`](./#menuicon). Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is only available with the use of the [theming v2](../../theming/introduction.mdx). No default component is provided by the SDK. The library does not provide any CSS for `AppMenu`. Consult the customization tutorial on how to [add AppMenu to your application](../../guides/customization/channel-search.mdx/#adding-menu). The component is passed a prop `close`, which is a function that can be called to hide the app menu (e.g. on menu item selection). +Application menu / drop-down to be displayed when clicked on [`MenuIcon`](./#menuicon). Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is only available with the use of the [theming v2](../../theming/introduction.mdx). No default component is provided by the SDK. The library does not provide any CSS for `AppMenu`. Consult the customization tutorial on how to [add AppMenu to your application](../../guides/customization/channel-search.mdx/#adding-menu). The component is passed a prop `close`, which is a function that can be called to hide the app menu (e.g. on menu item selection). | Type | Default | -|-----------------------|-------------| +| --------------------- | ----------- | | `React.ComponentType` | `undefined` | ### channelType @@ -172,13 +218,12 @@ The type of `channel` to create on user result selection. | --------------------------------------------------------------- | ----------- | | `livestream` \| `messaging` \| `team` \| `gaming` \| `commerce` | `messaging` | - ### ClearInputIcon Custom icon component used as a content of the button used to clear the search input. Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is rendered with `themeVersion` `'2'` only. | Type | Default | -|-----------------------|-----------------------------------------------------------------| +| --------------------- | --------------------------------------------------------------- | | `React.ComponentType` | | ### clearSearchOnClickOutside @@ -186,7 +231,7 @@ Custom icon component used as a content of the button used to clear the search i Signals that the search state / results should be cleared on every click outside the search input (e.g. selecting a search result or exiting the search UI), defaults to `true`. If set to `false`, the search results are kept in the UI meanwhile the user changes between the channels. | Type | Default | -|-----------|---------| +| --------- | ------- | | `boolean` | `true` | ### disabled @@ -194,7 +239,7 @@ Signals that the search state / results should be cleared on every click outside Disables execution of the search queries and makes the search text input element disabled. Defaults to `false`. | Type | Default | -|-----------|---------| +| --------- | ------- | | `boolean` | `false` | ### ExitSearchIcon @@ -202,7 +247,7 @@ Disables execution of the search queries and makes the search text input element Custom icon component used as a content of the button used to exit the search UI. Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is rendered with `themeVersion` `'2'` only. | Type | Default | -|-----------------------|----------------------------------------------------------------------| +| --------------------- | -------------------------------------------------------------------- | | `React.ComponentType` | | ### MenuIcon @@ -210,7 +255,7 @@ Custom icon component used as a content of the button used to exit the search UI Custom icon component used as a content of the button used to invoke the [`AppMenu`](./#appmenu). Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is rendered with `themeVersion` `'2'` only. The menu icon button is displayed only if `AppMenu` component has been passed to `ChannelSearch` props. | Type | Default | -|-----------------------|--------------------------------------------------------------------| +| --------------------- | ------------------------------------------------------------------ | | `React.ComponentType` | | ### onSearch @@ -218,7 +263,7 @@ Custom icon component used as a content of the button used to invoke the [`AppMe Callback invoked with every search input change handler. SDK user can provide own implementation. The prop is used by the `ChannelList` component to set a flag determining that the search has been initiated. If the search has been initiated and search result are to be displayed instead of the list of loaded channels ([`popupResults` flag](./#popupresults) is set to `false`), then the list of loaded channels is not rendered. This logic is executed despite passing custom implementation of `onSearch` function to `ChanneList` props. | Type | -|-----------------------------------------------| +| --------------------------------------------- | | `React.ChangeEventHandler` | ### onSearchExit @@ -226,7 +271,7 @@ Callback invoked with every search input change handler. SDK user can provide ow Callback invoked when the search UI is deactivated. The `ChannelList` component uses it to set a flag that the search has been terminated and search results are not expected to be displayed in place of the list of loaded channels. And so the `ChannelList` renders the list of loaded channels. This logic is executed despite passing custom implementation of `onSearchExit` function to `ChanneList` props. | Type | -|--------------| +| ------------ | | `() => void` | ### onSelectResult @@ -237,8 +282,8 @@ Custom handler function to run on search result item selection. If not provided 2. adding the selected channel to the channel list 3. clearing the search results, if [`clearSearchOnClickOutside` flag](./#clearsearchonclickoutside) is set to true (default) -| Type | -| ----------------------------------------------- | +| Type | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | (
    `params: ChannelSearchFunctionParams,`
    `result: ChannelOrUserResponse`
) => Promise \| void | ### placeholder @@ -246,7 +291,7 @@ Custom handler function to run on search result item selection. If not provided Custom placeholder text to be displayed in the search input. Can be passed down from `ChannelList` via its `additionalChannelSearchProps`. If using custom i18n translations, it is preferable to change the placeholder value in your translations files under the key `'Search'`. | Type | Default | -|----------|------------| +| -------- | ---------- | | `string` | `'Search'` | ### popupResults @@ -254,23 +299,23 @@ Custom placeholder text to be displayed in the search input. Can be passed down Display search results as an absolutely positioned popup, defaults to false and shows inline. | Type | Default | -|-----------|---------| +| --------- | ------- | | `boolean` | `false` | ### SearchBar Custom component to be rendered instead of the default . This component is displayed only if `themeVersion` is `'2'`. With the theme version 1 only the `SearchInput` is rendered. The default `SearchBar` component is a composite of multiple buttons and a search input. You can find more information about its features in an [above section](./#searchbar-component). -| Type | Default | -| ------------------- |---------------| -| `React.ComponentType` | | +| Type | Default | +| -------------------------------------- | ----------------------------------------------------------------------- | +| `React.ComponentType` | | ### SearchEmpty Custom UI component to display empty search results. | Type | Default | -|-----------------------|--------------------------------------------------------------------------------------| +| --------------------- | ------------------------------------------------------------------------------------ | | `React.ComponentType` | | ### searchForChannels @@ -278,7 +323,7 @@ Custom UI component to display empty search results. Boolean to search for channels as well as users in the server query, default is `false` and just searches for users. | Type | Default | -|-----------|---------| +| --------- | ------- | | `boolean` | `false` | ### searchFunction @@ -290,7 +335,7 @@ Custom search function to override default. The first argument should expect an - `setSearching` - signals that the HTTP search request is in progress | Type | -|-----------------------------------------------------------------------------------------------------| +| --------------------------------------------------------------------------------------------------- | | (`params: ChannelSearchFunctionParams, event: React.BaseSyntheticEvent` ) => Promise \| void | ### SearchInput @@ -298,7 +343,7 @@ Custom search function to override default. The first argument should expect an Custom UI component to display the search text input. | Type | Default | -|-----------------------------------------|-----------------------------------------------------------------------------| +| --------------------------------------- | --------------------------------------------------------------------------- | | `React.ComponentType` | | ### SearchLoading @@ -306,7 +351,7 @@ Custom UI component to display the search text input. Custom UI component to display the search loading state. Rendered within the `SearchResults` component. | Type | Default | -|-----------------------|----------------------------| +| --------------------- | -------------------------- | | `React.ComponentType` | a div with: 'Searching...' | ### searchQueryParams @@ -316,24 +361,23 @@ Object containing filters/sort/options overrides for user / channel search. The `filters` attribute (`SearchQueryParams.userFilters.filters`) can be either `UserFilters` object describing the filter query or a function with a single argument of the search / filter (query) string. The function is then expected to derive and return the `UserFilters` from the provided query string. | Type | -|-----------------------------------------| +| --------------------------------------- | | `SearchQueryParams` | ### SearchResultsHeader Custom UI component to display the search results header. -| Type | Default | -| --------- | --------- | -| `React.ComponentType` | | - +| Type | Default | +| --------------------- | -------------------------------------------------------------------------------------------- | +| `React.ComponentType` | | ### SearchResultItem Custom UI component to display a search result list item. | Type | Default | -|------------------------------------------------------------------|-------------------------------------------------------------------------------------------| +| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `React.ComponentType>` | | ### SearchResultsList @@ -341,5 +385,5 @@ Custom UI component to display a search result list item. Custom UI component to display all the search results. | Type | Default | -|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------| +| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `React.ComponentType>` | | diff --git a/docusaurus/docs/React/components/utility-components/indicators.mdx b/docusaurus/docs/React/components/utility-components/indicators.mdx index c95f9e696..ce474c71a 100644 --- a/docusaurus/docs/React/components/utility-components/indicators.mdx +++ b/docusaurus/docs/React/components/utility-components/indicators.mdx @@ -85,8 +85,8 @@ The type of error. The type of list that will display this indicator, and this type will conditionally render a message. -| Type | -| ---------------------- | +| Type | +| ---------------------------------- | | 'channel' \| 'message' \| 'thread' | ## LoadingErrorIndicatorProps @@ -158,7 +158,7 @@ Boolean for if there is a next page to load. A UI button component that handles pagination logic. | Type | Default | -|-----------|------------------------------------------------------------------------------| +| --------- | ---------------------------------------------------------------------------- | | component | | ### refreshing @@ -184,7 +184,7 @@ A boolean that indicates if the `LoadMoreButton` should be displayed at the top Custom UI component to display user's avatar. | Type | Default | -|-----------|------------------------------------------------------------| +| --------- | ---------------------------------------------------------- | | component | | ### avatarSize diff --git a/docusaurus/docs/React/guides/channel-list-infinite-scroll.mdx b/docusaurus/docs/React/guides/channel-list-infinite-scroll.mdx index 8d049cf6e..62915c948 100644 --- a/docusaurus/docs/React/guides/channel-list-infinite-scroll.mdx +++ b/docusaurus/docs/React/guides/channel-list-infinite-scroll.mdx @@ -14,15 +14,15 @@ This example demonstrates how to implement infinite scroll with existing SDK com The SDK provides own [`InfiniteScroll`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/InfiniteScrollPaginator/InfiniteScroll.tsx) component. This component implements the [`PaginatorProps`](https://github.com/GetStream/stream-chat-react/blob/master/src/types/types.ts) interface. As this interface is implemented by the [`LoadMorePaginator`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/LoadMore/LoadMorePaginator.ts) too, we can just pass the `InfiniteScroll` into the `ChannelList` prop `Paginator`. ```tsx -import { - ChannelList, - InfiniteScroll, -} from 'stream-chat-react'; - - +import { ChannelList, InfiniteScroll } from 'stream-chat-react'; + +; ``` If you would like to adjust the configuration parameters like `threshold`, `reverse` (`PaginatorProps`) or `useCapture`, etc. (`InfiniteScrollProps`), you can create a wrapper component where these props can be set: @@ -65,8 +65,6 @@ You can change the container height so that not all channels are visible at once ```css .str-chat__channel-list-messenger-react__main { - max-height: 50%; + max-height: 50%; } ``` - - diff --git a/docusaurus/docs/React/guides/customization/channel-search.mdx b/docusaurus/docs/React/guides/customization/channel-search.mdx index ddc129a5e..cad9c3acb 100644 --- a/docusaurus/docs/React/guides/customization/channel-search.mdx +++ b/docusaurus/docs/React/guides/customization/channel-search.mdx @@ -320,15 +320,15 @@ and for only channels that the current logged in user is a member of. See the ex const customSearchFunction = async ( props: ChannelSearchFunctionParams, event: { target: { value: SetStateAction } }, - client: StreamChat + client: StreamChat, ) => { const { setResults, setSearching, setQuery } = props; const value = event.target.value; const filters = { - name: { $autocomplete: value }, - members: { $in: client.userID } - } + name: { $autocomplete: value }, + members: { $in: client.userID }, + }; setSearching(true); setQuery(value); @@ -348,7 +348,7 @@ const { client } = useChatContext(); }, }} showChannelSearch -/> +/>; ``` ### Adding menu @@ -365,8 +365,7 @@ import type { AppMenuProps } from 'stream-chat-react'; import './AppMenu.scss'; -export const AppMenu = ({close}: AppMenuProps) => { - +export const AppMenu = ({ close }: AppMenuProps) => { const handleSelect = useCallback(() => { // custom logic... close?.(); @@ -375,13 +374,19 @@ export const AppMenu = ({close}: AppMenuProps) => { return (
    -
  • Profile
  • -
  • New Group
  • -
  • Sign Out
  • +
  • + Profile +
  • +
  • + New Group +
  • +
  • + Sign Out +
); -} +}; ``` ```scss @@ -408,7 +413,7 @@ export const AppMenu = ({close}: AppMenuProps) => { &__item { list-style: none; margin: 0; - padding: .5rem 1rem; + padding: 0.5rem 1rem; &:hover { background-color: lightgrey; @@ -422,20 +427,20 @@ export const AppMenu = ({close}: AppMenuProps) => { import { AppMenu } from './components/AppMenu'; const App = () => ( - - - - - - - - - - - + + + + + + + + + + + ); ``` diff --git a/docusaurus/docs/React/guides/customization/giphy-preview.mdx b/docusaurus/docs/React/guides/customization/giphy-preview.mdx index a93b3457e..103434529 100644 --- a/docusaurus/docs/React/guides/customization/giphy-preview.mdx +++ b/docusaurus/docs/React/guides/customization/giphy-preview.mdx @@ -1,6 +1,6 @@ --- id: giphy_preview -sidebar_position: 20 +sidebar_position: 2 title: Giphy Preview --- diff --git a/docusaurus/docs/React/guides/customization/link-preview.mdx b/docusaurus/docs/React/guides/customization/link-preview.mdx new file mode 100644 index 000000000..3fa6f575d --- /dev/null +++ b/docusaurus/docs/React/guides/customization/link-preview.mdx @@ -0,0 +1,395 @@ +--- +id: link-previews +sidebar_position: 21 +title: Link Previews in Message Input +--- + +import LinkPreviewMessageInput from '../../assets/link-preview-message-input.png'; +import LinkPreviewEditMessageForm from '../../assets/link-preview-edit-message-form.png'; + +The purpose of link previews in the `MessageInput` is to provide visual guides of what a user may expect to be rendered later in the `MessageList` by [`Card` component](../../../components/message-components/attachment#card) among message attachments. + +## Rendering of link previews + +The link previews are rendered using `LinkPreviewList`. The component accepts a single prop `linkPreviews` which is an array of `LinkPreview` objects. + +### The default LinkPreviewList component + +The default `LinkPreviewList` component lists all the successfully loaded previews. + +The default link preview UI is implemented for: + +**Message input** + + + +
+ +**Edit message form** + + + +### Enabling link previews + +Link previews have to be enabled in two places: + +- [**channel config property `url-enrichment`**](https://getstream.io/chat/docs/javascript/channel-level_settings/?language=javascript&q=url_enrichment#list-of-settings-that-can-be-overridden) - enabled by default +- `enrichURLForPreview` prop - disabled by default + +Those who have not previously disabled `url-enrichment` in the channel config, can enable link previews in `MessageInput` by setting `enrichURLForPreview` in one of the following places: + +**Channel props** + +```tsx +import { + Channel, + ChannelHeader, + VirtualizedMessageList as MessageList, + MessageInput, + Thread, + Window, +} from 'stream-chat-react'; + +const App = () => ( + // highlight-next-line + + + + + + + + +); + +export default App; +``` + +**MessageList or VirtualizedMessageList (applied to EditMessageForm)** + +```tsx +import { Channel, VirtualizedMessageList as MessageList } from 'stream-chat-react'; + +const App = () => ( + + {/* ... */} + // highlight-start + + // highlight-end + {/* ... */} + +); + +export default App; +``` + +**Thread (applied to MessageInput)** + +```tsx +import { Channel, Thread } from 'stream-chat-react'; + +const App = () => ( + + {/* ... */} + // highlight-start + + // highlight-end + +); + +export default App; +``` + +**MessageInput** + +```tsx +import { Channel, MessageInput } from 'stream-chat-react'; + +const App = () => ( + + {/* ... */} + // highlight-next-line + + {/* ... */} + +); + +export default App; +``` + +## Link Preview customization + +### Custom rendering of link previews + +If the default link previews UI does not meet our expectations, we can provide a custom component. To render our own `LinkPreviewList`, we just need to pass it to `Channel` prop `LinkPreviewList`. The component will be passed `linkPreviews`, an array of `LinkPreview` objects. + +```tsx +import { Channel, LinkPreviewListProps, LinkPreviewState } from 'stream-chat-react'; + +import { LinkPreviewCardLoaded, LinkPreviewCardLoading } from './LinkPreviewCard'; + +const CustomLinkPreviewList = ({ linkPreviews }: LinkPreviewListProps) => { + const showLinkPreviews = linkPreviews.length > 0; + + if (!showLinkPreviews) return null; + + return ( +
+ {Array.from(linkPreviews.values()).map((linkPreview) => { + switch (linkPreview.state) { + case LinkPreviewState.LOADED: + return ( + + ); + case LinkPreviewState.LOADING: + return ( + + ); + case LinkPreviewState.QUEUED: + return ( + + ); + default: + return null; + } + })} +
+ ); +}; + +const App = () => ( + // highlight-next-line + + {/* ... */} + +); + +export default App; +``` + +### Link preview states + +In the above example we can notice, that the `LinkPreview` object comes with property `state`. This property can be used to determine, how the preview for a given link should be rendered. These are the possible states a link preview can acquire: + +```typescript +enum LinkPreviewState { + /** Link preview has been dismissed using MessageInputContextValue.dismissLinkPreview **/ + DISMISSED = 'dismissed', + /** Link preview could not be loaded, the enrichment request has failed. **/ + FAILED = 'failed', + /** Link preview has been successfully loaded. **/ + LOADED = 'loaded', + /** The enrichment query is in progress for a given link. **/ + LOADING = 'loading', + /** The link is scheduled for enrichment. **/ + QUEUED = 'queued', +} +``` + +### Behavior customization + +The following aspect of link preview management in `MessageInput` can be customized: + +- The debounce interval for the URL discovery and enrichment requests. +- URL discovery +- Link preview dismissal + +In general, the behavior can be customized in two ways: + +1. globally through `Channel` props (`enrichURLForPreviewConfig`) +2. with more granularity over `MessageList`, `VirtualizedMessageList`, `Thread`, `MessageInput` props (`additionalMessageInputProps.urlEnrichmentConfig`) + +#### Custom debounce interval + +The default debounce interval is 1.5 seconds. The URL discovery and enrichment will thus not start until the stops typing for at least 1.5 seconds. This interval can be increased or decreased by passing `debounceURLEnrichmentMs` configuration parameter. + +Global debounce interval configuration over `Channel`: + +```tsx +import { + Channel, + MessageInput, + Thread, + VirtualizedMessageList as MessageList, +} from 'stream-chat-react'; + +const debounceURLEnrichmentMs = 1000; + +const App = () => ( + + + + + +); +``` + +Local debounce configuration: + +```tsx +import { + Channel, + MessageInput, + Thread, + VirtualizedMessageList as MessageList, +} from 'stream-chat-react'; + +const debounceURLEnrichmentMs = 1000; +const additionalMessageInputProps = { + urlEnrichmentConfig: { debounceURLEnrichmentMs }, +}; + +const App = () => ( + + + + + +); +``` + +#### Custom text parsing function + +If the default link discovery functionality is not sufficient, this can be overridden by providing `findURLFn` custom function. The requirement is that the function returns an array of strings - links - that will be later used to scrape the data. + +The parameter set globally over `Channel` props: + +```tsx +import { + Channel, + MessageInput, + Thread, + VirtualizedMessageList as MessageList, +} from 'stream-chat-react'; + +import { searchForURLs } from '../utils'; + +const App = () => ( + + + + + +); +``` + +Local configuration of URL discovery function: + +```tsx +import { + Channel, + MessageInput, + Thread, + VirtualizedMessageList as MessageList, +} from 'stream-chat-react'; + +import { searchForURLs } from '../utils'; + +const additionalMessageInputProps = { + urlEnrichmentConfig: { findURLFn: searchForURLs }, +}; + +const App = () => ( + + + + + +); +``` + +#### Custom actions on link preview dismissal + +When a link preview is dismissed, it's state is set to `'dismissed'`. This behavior can be expanded (not changed) by providing `onLinkPreviewDismissed` callback. The callback is invoked at the beginning of the dismissal procedure. It is then followed by state update marking the given URL preview as `'dismissed'`. + +The `onLinkPreviewDismissed` callback can be passed to `Channel` prop `enrichURLForPreviewConfig`: + +```tsx +import { + Channel, + LinkPreview, + MessageInput, + Thread, + VirtualizedMessageList as MessageList, +} from 'stream-chat-react'; + +const onLinkPreviewDismissed = (linkPreview: LinkPreview) => { + // custom logic to invoke, when a given link preview is dismissed +}; + +const App = () => ( + + + + + +); +``` + +The configuration can be passed individually to `MessageList`, `VirtualizedMessageList`, `Thread`, `MessageInput`: + +```tsx +import { + Channel, + LinkPreview, + MessageInput, + Thread, + VirtualizedMessageList as MessageList, +} from 'stream-chat-react'; + +const onLinkPreviewDismissed = (linkPreview: LinkPreview) => { + // custom logic to invoke, when a given link preview is dismissed +}; + +const additionalMessageInputProps = { + urlEnrichmentConfig: { onLinkPreviewDismissed }, +}; + +const App = () => ( + + + + + +); +``` + +### EnrichURLsController API + +In case we would aspire at implementing custom `MessageInput` components that would require control over link previews, we can access the API over the `MessageInputContext` value. This is the API that allows us: + +- to trigger URL search in message text and enrichment - `findAndEnqueueURLsToEnrich` +- cancel the URL enrichment (for example when submitting a message) - `cancelURLEnrichment` +- dismiss the loaded link previews assigning them with state `dismissed` - `dismissLinkPreview` + +```tsx +import { useMessageInputContext } from 'stream-chat-react'; + +const CustomMessageInputUI = () => { + const { + cancelURLEnrichment, + findAndEnqueueURLsToEnrich, + dismissLinkPreview, + } = useMessageInputContext(); + + // ... +}; + +const App = () => { + + {/* ... */} + ; +}; +``` + +:::note +The `findAndEnqueueURLsToEnrich` function serves as an indicator, whether the link preview feature is actually enabled in the application. +::: diff --git a/docusaurus/docs/React/guides/customization/override-submit-handler.mdx b/docusaurus/docs/React/guides/customization/override-submit-handler.mdx index 6dbb91688..0757e08be 100644 --- a/docusaurus/docs/React/guides/customization/override-submit-handler.mdx +++ b/docusaurus/docs/React/guides/customization/override-submit-handler.mdx @@ -17,7 +17,7 @@ conclusion of the underlying `textarea` element's [`handleSubmit`](https://githu function. :::note -You do not have to implement your custom submit handler, if the only thing you need is to pass custom message data to the underlying API call. In that case you can use the [`handleSubmit`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/hooks/useSubmitHandler.ts) function from the [`MessageInputContext`](../../components/contexts/message-input-context.mdx). The `handleSubmit` function allows you to pass custom message data through its second parameter `customMessageData`. This applies to sending a new message as well as updating an existing one. In order for this to work, you will have to implement custom message input components and pass them to [`Channel`](../../components/core-components/channel.mdx) props `EditMessageInput` or `Input` respectively. +You do not have to implement your custom submit handler, if the only thing you need is to pass custom message data to the underlying API call. In that case you can use the [`handleSubmit`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/hooks/useSubmitHandler.ts) function from the [`MessageInputContext`](../../components/contexts/message-input-context.mdx). The `handleSubmit` function allows you to pass custom message data through its second parameter `customMessageData`. This applies to sending a new message as well as updating an existing one. In order for this to work, you will have to implement custom message input components and pass them to [`Channel`](../../components/core-components/channel.mdx) props `EditMessageInput` or `Input` respectively. ::: The `overrideSubmitHandler` function receives three arguments, the message to be sent, the `cid` (channel type prepended to channel id) @@ -109,7 +109,6 @@ const App = () => ( Override Submit Handler - ## Adding custom message data If you would like to store custom data on your message objects, you can pass additional parameters to the [`sendMessage`](../../components/contexts/channel-action-context.mdx#sendmessage) function you retrieve from `ChannelActionContext`. @@ -138,10 +137,10 @@ const ChannelInner = () => { - + - ) -} + ); +}; ``` In your browser's Developer tools Network tab you can now observe, that the message payload includes the custom field: diff --git a/docusaurus/docs/React/guides/customization/persist-input-text-in-local-storage.mdx b/docusaurus/docs/React/guides/customization/persist-input-text-in-local-storage.mdx index 53eb630d6..aadca6ad8 100644 --- a/docusaurus/docs/React/guides/customization/persist-input-text-in-local-storage.mdx +++ b/docusaurus/docs/React/guides/customization/persist-input-text-in-local-storage.mdx @@ -7,8 +7,8 @@ title: Storing message drafts In this recipe, we would like to demonstrate how you can start storing unsent user's messages as drafts. The whole implementation turns around the use of `MessageInput`'s prop `getDefaultValue` and custom change event handler. We will store the messages in localStorage. - ## Building the draft storage logic + Below, we have a simple logic to store all the message text drafts in a localStorage object under the key `@chat/drafts`. ```ts @@ -21,7 +21,7 @@ const removeDraft = (key: string) => { if (drafts[key]) { delete drafts[key]; - localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)) + localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)); } }; @@ -31,25 +31,19 @@ const updateDraft = (key: string, value: string) => { if (!value) { delete drafts[key]; } else { - drafts[key] = value + drafts[key] = value; } - localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)) -} + localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)); +}; ``` On top of this logic we build a hook that exposes the change handler functions for both thread and main `MessageInput` components as well as functions for `MessageInput`'s `getDefaultValue` prop. We also have to override the `MessageInput`'s default submit handler, because we want to remove the draft from storage when a message is sent. ```ts import { ChangeEvent, useCallback } from 'react'; -import { - MessageToSend, - useChannelActionContext, - useChannelStateContext, -} from 'stream-chat-react'; -import type { - Message -} from 'stream-chat'; +import { MessageToSend, useChannelActionContext, useChannelStateContext } from 'stream-chat-react'; +import type { Message } from 'stream-chat'; const STORAGE_KEY = '@chat/drafts'; @@ -60,7 +54,7 @@ const removeDraft = (key: string) => { if (drafts[key]) { delete drafts[key]; - localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)) + localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)); } }; @@ -70,25 +64,31 @@ const updateDraft = (key: string, value: string) => { if (!value) { delete drafts[key]; } else { - drafts[key] = value + drafts[key] = value; } - localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)) -} + localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts)); +}; // highlight-start const useDraftAPI = () => { const { channel, thread } = useChannelStateContext(); const { sendMessage } = useChannelActionContext(); - const handleInputChange = useCallback((e: ChangeEvent) => { - updateDraft(channel.cid, e.target.value); - }, [channel.cid]) + const handleInputChange = useCallback( + (e: ChangeEvent) => { + updateDraft(channel.cid, e.target.value); + }, + [channel.cid], + ); - const handleThreadInputChange = useCallback((e: ChangeEvent) => { - if (!thread) return; - updateDraft(`${channel.cid}:${thread.id}`, e.target.value); - }, [channel.cid, thread]); + const handleThreadInputChange = useCallback( + (e: ChangeEvent) => { + if (!thread) return; + updateDraft(`${channel.cid}:${thread.id}`, e.target.value); + }, + [channel.cid, thread], + ); const getMainInputDraft = useCallback(() => { const drafts = getDrafts(); @@ -102,11 +102,13 @@ const useDraftAPI = () => { }, [channel.cid, thread]); const overrideSubmitHandler = useCallback( - async (message: MessageToSend, channelCid: string, customMessageData?: Partial,) => { - await sendMessage(message, customMessageData); - const key = message.parent ? `${channelCid}:${message.parent.id}` : channelCid; - removeDraft(key); - }, [sendMessage]) + async (message: MessageToSend, channelCid: string, customMessageData?: Partial) => { + await sendMessage(message, customMessageData); + const key = message.parent ? `${channelCid}:${message.parent.id}` : channelCid; + removeDraft(key); + }, + [sendMessage], + ); return { getMainInputDraft, @@ -114,8 +116,8 @@ const useDraftAPI = () => { handleInputChange, handleThreadInputChange, overrideSubmitHandler, - } -} + }; +}; // highlight-end ``` @@ -135,33 +137,35 @@ const ChannelWindow = () => { handleInputChange, handleThreadInputChange, overrideSubmitHandler, - } = useDraftAPI() + } = useDraftAPI(); return ( <> - - - + + + - + - ) -} + ); +}; // In your application you will probably initiate the client in a React effect. const chatClient = StreamChat.getInstance(''); @@ -174,9 +178,9 @@ const sort: ChannelSort = { last_message_at: -1, updated_at: -1 }; const App = () => { return ( - + - + ); diff --git a/docusaurus/docs/React/guides/customization/system-message.mdx b/docusaurus/docs/React/guides/customization/system-message.mdx index 5aaa78969..711eb08d5 100644 --- a/docusaurus/docs/React/guides/customization/system-message.mdx +++ b/docusaurus/docs/React/guides/customization/system-message.mdx @@ -39,51 +39,84 @@ To see your custom component in action, try muting a user by using the '/' comma display: flex; flex-direction: column; background: red; /* For browsers that do not support gradients */ - background: -webkit-linear-gradient(left, orange , yellow, green, cyan, blue, violet); /* For Safari 5.1 to 6.0 */ - background: -o-linear-gradient(right, orange, yellow, green, cyan, blue, violet); /* For Opera 11.1 to 12.0 */ - background: -moz-linear-gradient(right, orange, yellow, green, cyan, blue, violet); /* For Firefox 3.6 to 15 */ - background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet); /* Standard syntax (must be last) */ + background: -webkit-linear-gradient( + left, + orange, + yellow, + green, + cyan, + blue, + violet + ); /* For Safari 5.1 to 6.0 */ + background: -o-linear-gradient( + right, + orange, + yellow, + green, + cyan, + blue, + violet + ); /* For Opera 11.1 to 12.0 */ + background: -moz-linear-gradient( + right, + orange, + yellow, + green, + cyan, + blue, + violet + ); /* For Firefox 3.6 to 15 */ + background: linear-gradient( + to right, + orange, + yellow, + green, + cyan, + blue, + violet + ); /* Standard syntax (must be last) */ } ``` ```jsx const CustomSystemMessage = (props: EventComponentProps) => { - const { Avatar = DefaultAvatar, message } = props; - - const { created_at = '', text, type, user } = message; - const date = created_at.toString(); - - if (type === 'system') { - return ( -
- Event: {text} at {date} - - Actor: - {user?.name} - -
- ) - } - - return null; -} + const { Avatar = DefaultAvatar, message } = props; + + const { created_at = '', text, type, user } = message; + const date = created_at.toString(); + + if (type === 'system') { + return ( +
+ + Event: {text} at {date} + + + Actor: + {user?.name} + +
+ ); + } + + return null; +}; const App = () => ( - - - - - - - - - - - + + + + + + + + + + + ); ``` - ### The Result: Custom System Message in the Message List diff --git a/docusaurus/docs/React/guides/multiple-channel-lists.mdx b/docusaurus/docs/React/guides/multiple-channel-lists.mdx index c67eff3d1..c50a03c53 100644 --- a/docusaurus/docs/React/guides/multiple-channel-lists.mdx +++ b/docusaurus/docs/React/guides/multiple-channel-lists.mdx @@ -17,14 +17,14 @@ The `ChannelList` components will retrieve a `channel` from `client.activeChanne By using the `channelRenderFilterFn` prop we can apply custom filtering logic to the list of `channels` that are rendered. Since we have access to the entire `channel` object, we can filter on type, custom fields, or other. ```tsx - const customChannelFilterFunction = (channels: Channel[]) => { - return channels.filter(/** your custom filter logic */); - }; +const customChannelFilterFunction = (channels: Channel[]) => { + return channels.filter(/** your custom filter logic */); +}; - -``` \ No newline at end of file +; +``` diff --git a/docusaurus/docs/React/guides/theming/input-ui.mdx b/docusaurus/docs/React/guides/theming/input-ui.mdx index dfcffb8d1..4dada205d 100644 --- a/docusaurus/docs/React/guides/theming/input-ui.mdx +++ b/docusaurus/docs/React/guides/theming/input-ui.mdx @@ -45,13 +45,13 @@ Here's an example of overriding the default `EmojiIcon` component: ```jsx const CustomEmojiIcon = () => { - const { t } = useTranslationContext(); + const { t } = useTranslationContext(); - return ( -
- {t('Open -
- ); + return ( +
+ {t('Open +
+ ); }; @@ -59,7 +59,7 @@ const CustomEmojiIcon = () => { - +; ``` ### Custom Triggers @@ -154,52 +154,51 @@ and use its return values to build functionality: ```jsx import { - ChatAutoComplete, - EmojiIconLarge, - EmojiPicker, - SendButton, - Tooltip, - useMessageInputContext, - useTranslationContext, + ChatAutoComplete, + EmojiIconLarge, + EmojiPicker, + SendButton, + Tooltip, + useMessageInputContext, + useTranslationContext, } from 'stream-chat-react'; export const CustomMessageInput = () => { - const { t } = useTranslationContext(); - - const { - closeEmojiPicker, - emojiPickerIsOpen, - handleEmojiKeyDown, - handleSubmit, - openEmojiPicker, - } = useMessageInputContext(); - - return ( -
-
-
- -
- - {emojiPickerIsOpen ? t('Close emoji picker') : t('Open emoji picker')} - - - - -
- -
- -
-
- ); + const { t } = useTranslationContext(); + + const { + closeEmojiPicker, + emojiPickerIsOpen, + handleEmojiKeyDown, + handleSubmit, + openEmojiPicker, + } = useMessageInputContext(); + + return ( +
+
+
+ +
+ + {emojiPickerIsOpen ? t('Close emoji picker') : t('Open emoji picker')} + + + + +
+ +
+ +
+
+ ); }; ``` diff --git a/docusaurus/docs/React/guides/theming/translations.mdx b/docusaurus/docs/React/guides/theming/translations.mdx index 6cd05b4a9..65304a384 100644 --- a/docusaurus/docs/React/guides/theming/translations.mdx +++ b/docusaurus/docs/React/guides/theming/translations.mdx @@ -251,6 +251,7 @@ i18n.registerTranslation( } ); ``` + 2. Provide your own `Momentjs` object ```js @@ -270,7 +271,7 @@ const i18n = new Streami18n({ 3. Provide your own Dayjs object ```js -import Dayjs from 'dayjs' +import Dayjs from 'dayjs'; import 'dayjs/locale/nl'; import 'dayjs/locale/it'; @@ -278,10 +279,11 @@ import 'dayjs/locale/it'; import 'dayjs/min/locales'; const i18n = new Streami18n({ - language: 'nl', - DateTimeParser: Dayjs -}) + language: 'nl', + DateTimeParser: Dayjs, +}); ``` + If you would like to stick with english language for dates and times in Stream components, you can set `disableDateTimeTranslations` to true. ### Translating Messages @@ -307,7 +309,7 @@ The `Streami18n` class wraps [`i18next`](https://www.npmjs.com/package/i18next) ### Class Constructor Options | Option | Description | Type | Default | -|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|------------| +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------- | | language | connected user's language | string | 'en' | | translationsForLanguage | overrides existing component text | object | {} | | disableDateTimeTranslations | disables translation of date times | boolean | false | diff --git a/docusaurus/docs/React/guides/typescript.mdx b/docusaurus/docs/React/guides/typescript.mdx index 75ef94884..e25569681 100644 --- a/docusaurus/docs/React/guides/typescript.mdx +++ b/docusaurus/docs/React/guides/typescript.mdx @@ -126,5 +126,3 @@ custom code example. The current default types can be seen in the [`stream-chat-react` component library](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts). Any additional custom types will extend these defaults. Core to understanding this pattern is how generics can be [applied to JSX elements](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#generic-type-arguments-in-jsx-elements). - - diff --git a/docusaurus/docs/React/hooks/channel-list-hooks.mdx b/docusaurus/docs/React/hooks/channel-list-hooks.mdx index c186f61a1..b55eb6489 100644 --- a/docusaurus/docs/React/hooks/channel-list-hooks.mdx +++ b/docusaurus/docs/React/hooks/channel-list-hooks.mdx @@ -15,11 +15,11 @@ To view the list of these custom handler functions to override (if there is one) ### useChannelDeletedListener -A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/hooks/useChannelDeletedListener.ts) that handles the deletion of a channel from the list. +A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/hooks/useChannelDeletedListener.ts) that handles the deletion of a channel from the list. ### useChannelHiddenListener -A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/hooks/useChannelHiddenListener.ts) that handles the hiding of a channel from the list. +A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/hooks/useChannelHiddenListener.ts) that handles the hiding of a channel from the list. ### useChannelTruncatedListener @@ -64,4 +64,3 @@ A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/c ### useMobileNavigation A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/hooks/useMobileNavigation.ts) that handles the opening and closing of a mobile navigation via Javascript click event listeners, not Stream events. - diff --git a/docusaurus/docs/React/hooks/message-hooks.mdx b/docusaurus/docs/React/hooks/message-hooks.mdx index 866441165..aba036871 100644 --- a/docusaurus/docs/React/hooks/message-hooks.mdx +++ b/docusaurus/docs/React/hooks/message-hooks.mdx @@ -110,7 +110,7 @@ const MyCustomMessageComponent = () => { return (
{message.text} - +
); }; diff --git a/docusaurus/docs/React/resources/resources.mdx b/docusaurus/docs/React/resources/resources.mdx index 53d93b120..b9acf6743 100644 --- a/docusaurus/docs/React/resources/resources.mdx +++ b/docusaurus/docs/React/resources/resources.mdx @@ -22,28 +22,28 @@ Here's a list of external and internal resource links that could be helpful duri > ### React Documentation -* [React - Getting Started](https://reactjs.org/docs/getting-started.html) +- [React - Getting Started](https://reactjs.org/docs/getting-started.html) -* [React - Context](https://reactjs.org/docs/context.html) +- [React - Context](https://reactjs.org/docs/context.html) > ### Internal Articles - * [Stream Chat Glossary](https://getstream.zendesk.com/hc/en-us/articles/1500007212082-Stream-Chat-Glossary) +- [Stream Chat Glossary](https://getstream.zendesk.com/hc/en-us/articles/1500007212082-Stream-Chat-Glossary) - * [Stream Chat Success Checklist](https://getstream.zendesk.com/hc/en-us/articles/1500007673721-Stream-Chat-Success-Checklist) +- [Stream Chat Success Checklist](https://getstream.zendesk.com/hc/en-us/articles/1500007673721-Stream-Chat-Success-Checklist) - * [React - Customizing Message Actions](https://getstream.zendesk.com/hc/en-us/articles/1500008025241--Customizing-Message-Actions-in-React-Chat) +- [React - Customizing Message Actions](https://getstream.zendesk.com/hc/en-us/articles/1500008025241--Customizing-Message-Actions-in-React-Chat) - * [Stream Webhooks](https://getstream.zendesk.com/hc/en-us/articles/1500006478421-How-can-I-use-the-Stream-Webhook-to-send-customers-emails-based-on-Chat-events-) +- [Stream Webhooks](https://getstream.zendesk.com/hc/en-us/articles/1500006478421-How-can-I-use-the-Stream-Webhook-to-send-customers-emails-based-on-Chat-events-) - * [Stream Rate limits](https://getstream.zendesk.com/hc/en-us/articles/360056792833-Rate-limits-and-HTTP-429-Errors) +- [Stream Rate limits](https://getstream.zendesk.com/hc/en-us/articles/360056792833-Rate-limits-and-HTTP-429-Errors) - * [Stream API → Client → Server](https://getstream.zendesk.com/hc/en-us/articles/360061669873-How-do-the-Chat-Client-Server-Stream-API-communicate-with-each-other-) +- [Stream API → Client → Server](https://getstream.zendesk.com/hc/en-us/articles/360061669873-How-do-the-Chat-Client-Server-Stream-API-communicate-with-each-other-) - * [Stream Chat Moderation](https://getstream.zendesk.com/hc/en-us/articles/360041455753) +- [Stream Chat Moderation](https://getstream.zendesk.com/hc/en-us/articles/360041455753) - * [Stream User Roles and Permissions](https://getstream.zendesk.com/hc/en-us/articles/360053064274-User-Roles-and-Permission-Policies-Chat) +- [Stream User Roles and Permissions](https://getstream.zendesk.com/hc/en-us/articles/360053064274-User-Roles-and-Permission-Policies-Chat) - * [Stream queryChannels filters](https://getstream.zendesk.com/hc/en-us/articles/360057461213-Filters-for-queryChannels-Chat) +- [Stream queryChannels filters](https://getstream.zendesk.com/hc/en-us/articles/360057461213-Filters-for-queryChannels-Chat) - * [Stream Unread Messages](https://getstream.zendesk.com/hc/en-us/articles/360042753154-How-do-I-retrieve-unread-messages-Chat) +- [Stream Unread Messages](https://getstream.zendesk.com/hc/en-us/articles/360042753154-How-do-I-retrieve-unread-messages-Chat) diff --git a/src/components/AutoCompleteTextarea/Textarea.jsx b/src/components/AutoCompleteTextarea/Textarea.jsx index a998c173d..8867cb2f1 100644 --- a/src/components/AutoCompleteTextarea/Textarea.jsx +++ b/src/components/AutoCompleteTextarea/Textarea.jsx @@ -57,6 +57,7 @@ export class ReactTextareaAutocomplete extends React.Component { }; } + // FIXME: unused method getSelectionPosition = () => { if (!this.textareaRef) return null; @@ -66,6 +67,7 @@ export class ReactTextareaAutocomplete extends React.Component { }; }; + // FIXME: unused method getSelectedText = () => { if (!this.textareaRef) return null; const { selectionEnd, selectionStart } = this.textareaRef; diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 9d91609e5..a220c2147 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -79,6 +79,8 @@ import type { DefaultStreamChatGenerics, GiphyVersions, ImageAttachmentSizeHandler, + SendMessageOptions, + UpdateMessageOptions, VideoAttachmentSizeHandler, } from '../../types/types'; import { useChannelContainerClasses } from './hooks/useChannelContainerClasses'; @@ -86,6 +88,7 @@ import { getImageAttachmentConfiguration, getVideoAttachmentConfiguration, } from '../Attachment/attachment-sizing'; +import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews'; export type ChannelProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -97,6 +100,8 @@ export type ChannelProps< 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) */ + AttachmentPreviewList?: ComponentContextValue['AttachmentPreviewList']; /** Optional UI component to override the default suggestion Header component, defaults to and accepts same props as: [Header](https://github.com/GetStream/stream-chat-react/blob/master/src/components/AutoCompleteTextarea/Header.tsx) */ AutocompleteSuggestionHeader?: ComponentContextValue['AutocompleteSuggestionHeader']; /** Optional UI component to override the default suggestion Item component, defaults to and accepts same props as: [Item](https://github.com/GetStream/stream-chat-react/blob/master/src/components/AutoCompleteTextarea/Item.js) */ @@ -119,11 +124,13 @@ export type ChannelProps< 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; @@ -141,8 +148,15 @@ export type ChannelProps< 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) */ + /** 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` */ @@ -155,6 +169,8 @@ export type ChannelProps< imageAttachmentSizeHandler?: ImageAttachmentSizeHandler; /** 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 **/ + LinkPreviewList?: ComponentContextValue['LinkPreviewList']; /** Custom UI component to be shown if the channel query fails, defaults to and accepts same props as: [LoadingErrorIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Loading/LoadingErrorIndicator.tsx) */ 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) */ @@ -293,6 +309,7 @@ const ChannelInner = < doUpdateMessageRequest, dragAndDropWindow = false, emojiData = defaultEmojiData, + enrichURLForPreviewConfig, LoadingErrorIndicator = DefaultLoadingErrorIndicator, LoadingIndicator = DefaultLoadingIndicator, maxNumberOfFiles, @@ -655,6 +672,7 @@ const ChannelInner = < const doSendMessage = async ( message: MessageToSend | StreamMessage, customMessageData?: Partial>, + options?: SendMessageOptions, ) => { const { attachments, id, mentioned_users = [], parent_id, text } = message; @@ -677,9 +695,9 @@ const ChannelInner = < let messageResponse: void | SendMessageAPIResponse; if (doSendMessageRequest) { - messageResponse = await doSendMessageRequest(channel.cid, messageData); + messageResponse = await doSendMessageRequest(channel.cid, messageData, options); } else { - messageResponse = await channel.sendMessage(messageData); + messageResponse = await channel.sendMessage(messageData, options); } let existingMessage; @@ -731,6 +749,7 @@ const ChannelInner = < text = '', }: MessageToSend, customMessageData?: Partial>, + options?: SendMessageOptions, ) => { channel.state.filterErrorMessages(); @@ -751,7 +770,7 @@ const ChannelInner = < updateMessage(messagePreview); - await doSendMessage(messagePreview, customMessageData); + await doSendMessage(messagePreview, customMessageData, options); }; const retrySendMessage = async (message: StreamMessage) => { @@ -761,6 +780,11 @@ const ChannelInner = < status: 'sending', }); + if (message.attachments) { + // remove scraped attachments added during the message composition in MessageInput to prevent sync issues + message.attachments = message.attachments.filter((attachment) => !attachment.og_scrape_url); + } + await doSendMessage(message); }; @@ -852,13 +876,17 @@ const ChannelInner = < channel, channelCapabilitiesArray, channelConfig, + debounceURLEnrichmentMs: enrichURLForPreviewConfig?.debounceURLEnrichmentMs, dragAndDropWindow, + enrichURLForPreview: props.enrichURLForPreview, + findURLFn: enrichURLForPreviewConfig?.findURLFn, giphyVersion: props.giphyVersion || 'fixed_height', imageAttachmentSizeHandler: props.imageAttachmentSizeHandler || getImageAttachmentConfiguration, maxNumberOfFiles, multipleUploads, mutes, notifications, + onLinkPreviewDismissed: enrichURLForPreviewConfig?.onLinkPreviewDismissed, quotedMessage, shouldGenerateVideoThumbnail: props.shouldGenerateVideoThumbnail || true, videoAttachmentSizeHandler: props.videoAttachmentSizeHandler || getVideoAttachmentConfiguration, @@ -886,12 +914,22 @@ const ChannelInner = < skipMessageDataMemoization, updateMessage, }), - [channel.cid, loadMore, loadMoreNewer, quotedMessage, jumpToMessage, jumpToLatestMessage], + [ + channel.cid, + enrichURLForPreviewConfig?.findURLFn, + enrichURLForPreviewConfig?.onLinkPreviewDismissed, + loadMore, + loadMoreNewer, + quotedMessage, + jumpToMessage, + jumpToLatestMessage, + ], ); const componentContextValue: ComponentContextValue = useMemo( () => ({ Attachment: props.Attachment || DefaultAttachment, + AttachmentPreviewList: props.AttachmentPreviewList, AutocompleteSuggestionHeader: props.AutocompleteSuggestionHeader, AutocompleteSuggestionItem: props.AutocompleteSuggestionItem, AutocompleteSuggestionList: props.AutocompleteSuggestionList, @@ -905,6 +943,7 @@ const ChannelInner = < GiphyPreviewMessage: props.GiphyPreviewMessage, HeaderComponent: props.HeaderComponent, Input: props.Input, + LinkPreviewList: props.LinkPreviewList, LoadingIndicator: props.LoadingIndicator, Message: props.Message || MessageSimple, MessageDeleted: props.MessageDeleted, diff --git a/src/components/Channel/__tests__/Channel.test.js b/src/components/Channel/__tests__/Channel.test.js index 0ce7c4d4d..6cd9484ce 100644 --- a/src/components/Channel/__tests__/Channel.test.js +++ b/src/components/Channel/__tests__/Channel.test.js @@ -1,3 +1,4 @@ +import { nanoid } from 'nanoid'; import React, { useEffect } from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; @@ -13,8 +14,10 @@ import { useComponentContext } from '../../../context/ComponentContext'; import { useEmojiContext } from '../../../context/EmojiContext'; import { generateChannel, + generateFileAttachment, generateMember, generateMessage, + generateScrapedDataAttachment, generateUser, getOrCreateChannelApi, getTestClientWithUser, @@ -94,7 +97,7 @@ describe('Channel', () => { const { messages: channelMessages } = useChannelStateContext(); return channelMessages.map( - ({ id, status, text }) => status !== 'failed' &&
{text}
, + ({ id, status, text }) => status !== 'failed' &&
{text}
, ); }; @@ -680,6 +683,7 @@ describe('Channel', () => { expect(doSendMessageRequest).toHaveBeenCalledWith( channel.cid, expect.objectContaining(message), + undefined, ), ); }); @@ -710,7 +714,9 @@ describe('Channel', () => { renderComponent({}, ({ editMessage }) => { editMessage(updatedMessage); }); - await waitFor(() => expect(clientUpdateMessageSpy).toHaveBeenCalledWith(updatedMessage)); + await waitFor(() => + expect(clientUpdateMessageSpy).toHaveBeenCalledWith(updatedMessage, undefined, undefined), + ); }); it('should use doUpdateMessageRequest for the editMessage callback if provided', async () => { @@ -721,7 +727,7 @@ describe('Channel', () => { }); await waitFor(() => - expect(doUpdateMessageRequest).toHaveBeenCalledWith(channel.cid, messages[0]), + expect(doUpdateMessageRequest).toHaveBeenCalledWith(channel.cid, messages[0], undefined), ); }); @@ -770,6 +776,45 @@ describe('Channel', () => { }); }); + it('should remove scraped attachment on retry-sending message', async () => { + // flag to prevent infinite loop + let hasSent = false; + let hasRetried = false; + const fileAttachment = generateFileAttachment(); + const scrapedAttachment = generateScrapedDataAttachment(); + const attachments = [fileAttachment, scrapedAttachment]; + const messageObject = { attachments, text: 'bla bla' }; + const sendMessageSpy = jest + .spyOn(channel, 'sendMessage') + .mockImplementationOnce(() => Promise.reject()); + + await act(() => { + renderComponent( + { children: }, + ({ messages: contextMessages, retrySendMessage, sendMessage }) => { + if (!hasSent) { + sendMessage(messageObject); + hasSent = true; + } else if (!hasRetried && contextMessages.some(({ status }) => status === 'failed')) { + // retry + useMockedApis(chatClient, [sendMessageApi(generateMessage(messageObject))]); + retrySendMessage(messageObject); + hasRetried = true; + } + }, + ); + }); + + expect(sendMessageSpy).not.toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ attachments: [scrapedAttachment] }), + ); + expect(sendMessageSpy).not.toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ attachments: [fileAttachment] }), + ); + }); + it('should allow removing messages', async () => { let allMessagesRemoved = false; const removeSpy = jest.spyOn(channel.state, 'removeMessage'); diff --git a/src/components/Channel/hooks/useCreateChannelStateContext.ts b/src/components/Channel/hooks/useCreateChannelStateContext.ts index 806ca0ea5..8914abc2d 100644 --- a/src/components/Channel/hooks/useCreateChannelStateContext.ts +++ b/src/components/Channel/hooks/useCreateChannelStateContext.ts @@ -19,9 +19,12 @@ export const useCreateChannelStateContext = < channel, channelCapabilitiesArray = [], channelConfig, + debounceURLEnrichmentMs, dragAndDropWindow, + enrichURLForPreview, giphyVersion, error, + findURLFn, hasMore, hasMoreNewer, imageAttachmentSizeHandler, @@ -35,6 +38,7 @@ export const useCreateChannelStateContext = < multipleUploads, mutes, notifications, + onLinkPreviewDismissed, pinnedMessages, quotedMessage, read = {}, @@ -99,8 +103,11 @@ export const useCreateChannelStateContext = < channel, channelCapabilities, channelConfig, + debounceURLEnrichmentMs, dragAndDropWindow, + enrichURLForPreview, error, + findURLFn, giphyVersion, hasMore, hasMoreNewer, @@ -114,6 +121,7 @@ export const useCreateChannelStateContext = < multipleUploads, mutes, notifications, + onLinkPreviewDismissed, pinnedMessages, quotedMessage, read, @@ -130,7 +138,10 @@ export const useCreateChannelStateContext = < }), [ channelId, + debounceURLEnrichmentMs, + enrichURLForPreview, error, + findURLFn, hasMore, hasMoreNewer, highlightedMessageId, @@ -141,6 +152,7 @@ export const useCreateChannelStateContext = < memoizedMessageData, memoizedThreadMessageData, notificationsLength, + onLinkPreviewDismissed, quotedMessage, readUsersLength, readUsersLastReads, diff --git a/src/components/Channel/hooks/useEditMessageHandler.ts b/src/components/Channel/hooks/useEditMessageHandler.ts index 1befb040b..f5067e887 100644 --- a/src/components/Channel/hooks/useEditMessageHandler.ts +++ b/src/components/Channel/hooks/useEditMessageHandler.ts @@ -2,13 +2,14 @@ import { useChatContext } from '../../../context/ChatContext'; import type { StreamChat, UpdatedMessage } from 'stream-chat'; -import type { DefaultStreamChatGenerics } from '../../../types/types'; +import type { DefaultStreamChatGenerics, UpdateMessageOptions } from '../../../types/types'; type UpdateHandler< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics > = ( cid: string, updatedMessage: UpdatedMessage, + options?: UpdateMessageOptions, ) => ReturnType['updateMessage']>; export const useEditMessageHandler = < @@ -18,10 +19,10 @@ export const useEditMessageHandler = < ) => { const { channel, client } = useChatContext('useEditMessageHandler'); - return (updatedMessage: UpdatedMessage) => { + return (updatedMessage: UpdatedMessage, options?: UpdateMessageOptions) => { if (doUpdateMessageRequest && channel) { - return Promise.resolve(doUpdateMessageRequest(channel.cid, updatedMessage)); + return Promise.resolve(doUpdateMessageRequest(channel.cid, updatedMessage, options)); } - return client.updateMessage(updatedMessage); + return client.updateMessage(updatedMessage, undefined, options); }; }; diff --git a/src/components/MessageInput/LinkPreviewList.tsx b/src/components/MessageInput/LinkPreviewList.tsx new file mode 100644 index 000000000..ec311d861 --- /dev/null +++ b/src/components/MessageInput/LinkPreviewList.tsx @@ -0,0 +1,70 @@ +import clsx from 'clsx'; +import React, { useState } from 'react'; +import { useChannelStateContext, useMessageInputContext } from '../../context'; +import type { LinkPreview } from './types'; +import { LinkPreviewState } from './types'; +import { CloseIcon, LinkIcon } from './icons'; +import { PopperTooltip } from '../Tooltip'; +import { useEnterLeaveHandlers } from '../Tooltip/hooks'; + +export type LinkPreviewListProps = { + linkPreviews: LinkPreview[]; +}; + +export const LinkPreviewList = ({ linkPreviews }: LinkPreviewListProps) => { + const { quotedMessage } = useChannelStateContext(); + const showLinkPreviews = linkPreviews.length > 0 && !quotedMessage; + + if (!showLinkPreviews) return null; + + return ( +
+ {Array.from(linkPreviews.values()).map((linkPreview) => + linkPreview.state === LinkPreviewState.LOADED ? ( + + ) : null, + )} +
+ ); +}; + +type LinkPreviewProps = { + linkPreview: LinkPreview; +}; + +const LinkPreviewCard = ({ linkPreview }: LinkPreviewProps) => { + const { dismissLinkPreview } = useMessageInputContext(); + const { handleEnter, handleLeave, tooltipVisible } = useEnterLeaveHandlers(); + const [referenceElement, setReferenceElement] = useState(null); + return ( +
+ + {linkPreview.og_scrape_url} + +
+ +
+
+
{linkPreview.title}
+
{linkPreview.text}
+
+ +
+ ); +}; diff --git a/src/components/MessageInput/MessageInput.tsx b/src/components/MessageInput/MessageInput.tsx index 56481a889..46e9d4b94 100644 --- a/src/components/MessageInput/MessageInput.tsx +++ b/src/components/MessageInput/MessageInput.tsx @@ -6,7 +6,7 @@ import { MessageInputFlat } from './MessageInputFlat'; import { useCooldownTimer } from './hooks/useCooldownTimer'; import { useCreateMessageInputContext } from './hooks/useCreateMessageInputContext'; -import { FileUpload, ImageUpload, useMessageInputState } from './hooks/useMessageInputState'; +import { useMessageInputState } from './hooks/useMessageInputState'; import { StreamMessage, useChannelStateContext } from '../../context/ChannelStateContext'; import { useComponentContext } from '../../context/ComponentContext'; @@ -17,7 +17,13 @@ import type { Channel, SendFileAPIResponse } from 'stream-chat'; import type { SearchQueryParams } from '../ChannelSearch/hooks/useChannelSearch'; import type { MessageToSend } from '../../context/ChannelActionContext'; -import type { CustomTrigger, DefaultStreamChatGenerics } from '../../types/types'; +import type { + CustomTrigger, + DefaultStreamChatGenerics, + SendMessageOptions, +} from '../../types/types'; +import type { URLEnrichmentConfig } from './hooks/useLinkPreviews'; +import type { FileUpload, ImageUpload } from './types'; export type MessageInputProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -72,6 +78,7 @@ export type MessageInputProps< message: MessageToSend, channelCid: string, customMessageData?: Partial>, + options?: SendMessageOptions, ) => Promise | void; /** When replying in a thread, the parent message object */ parent?: StreamMessage; @@ -88,6 +95,8 @@ export type MessageInputProps< * ``` */ shouldSubmit?: (event: KeyboardEvent) => boolean; + /** Configuration parameters for link previews. */ + urlEnrichmentConfig?: URLEnrichmentConfig; useMentionsTransliteration?: boolean; }; diff --git a/src/components/MessageInput/MessageInputFlat.tsx b/src/components/MessageInput/MessageInputFlat.tsx index 443fc6fce..2dceb86c8 100644 --- a/src/components/MessageInput/MessageInputFlat.tsx +++ b/src/components/MessageInput/MessageInputFlat.tsx @@ -18,7 +18,8 @@ import { QuotedMessagePreview as DefaultQuotedMessagePreview, QuotedMessagePreviewHeader, } from './QuotedMessagePreview'; -import { AttachmentPreviewList } from './AttachmentPreviewList'; +import { AttachmentPreviewList as DefaultAttachmentPreviewList } from './AttachmentPreviewList'; +import { LinkPreviewList as DefaultLinkPreviewList } from './LinkPreviewList'; import { UploadsPreview } from './UploadsPreview'; import { ChatAutoComplete } from '../ChatAutoComplete/ChatAutoComplete'; @@ -94,6 +95,7 @@ const MessageInputV1 = < FileUploadIcon = DefaultFileUploadIcon, QuotedMessagePreview = DefaultQuotedMessagePreview, SendButton = DefaultSendButton, + AttachmentPreviewList = UploadsPreview, } = useComponentContext('MessageInputFlat'); return ( @@ -115,7 +117,7 @@ const MessageInputV1 = < )}
- {isUploadEnabled && } + {isUploadEnabled && }
@@ -184,8 +186,10 @@ const MessageInputV2 = < closeEmojiPicker, cooldownRemaining, emojiPickerIsOpen, + findAndEnqueueURLsToEnrich, handleSubmit, isUploadEnabled, + linkPreviews, maxFilesLeft, message, numberOfUploads, @@ -196,9 +200,11 @@ const MessageInputV2 = < } = useMessageInputContext('MessageInputV2'); const { + AttachmentPreviewList = DefaultAttachmentPreviewList, CooldownTimer = DefaultCooldownTimer, EmojiIcon = DefaultEmojiPickerIcon, FileUploadIcon = DefaultUploadIcon, + LinkPreviewList = DefaultLinkPreviewList, QuotedMessagePreview = DefaultQuotedMessagePreview, SendButton = DefaultSendButton, } = useComponentContext('MessageInputV2'); @@ -235,6 +241,9 @@ const MessageInputV2 = < return ( <>
+ {findAndEnqueueURLsToEnrich && ( + + )} {isDragActive && (
{displayQuotedMessage && } - {isUploadEnabled && !!numberOfUploads && }
diff --git a/src/components/MessageInput/__tests__/LinkPreviewList.test.js b/src/components/MessageInput/__tests__/LinkPreviewList.test.js new file mode 100644 index 000000000..6d10f5274 --- /dev/null +++ b/src/components/MessageInput/__tests__/LinkPreviewList.test.js @@ -0,0 +1,1385 @@ +import debounce from 'lodash.debounce'; +import { MessageInputFlat } from '../MessageInputFlat'; +import { + generateChannel, + generateMember, + generateMessage, + generateScrapedAudioAttachment, + generateScrapedDataAttachment, + generateScrapedImageAttachment, + generateUser, + getOrCreateChannelApi, + getTestClientWithUser, + useMockedApis, +} from '../../../mock-builders'; +import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + ChatProvider, + MessageProvider, + useChatContext, + useMessageInputContext, +} from '../../../context'; +import React, { useEffect } from 'react'; +import { Chat } from '../../Chat'; +import { Channel } from '../../Channel'; +import { MessageActionsBox } from '../../MessageActions'; +import { MessageInput } from '../MessageInput'; + +import '@testing-library/jest-dom'; +import { SendButton } from '../icons'; + +// Mock out lodash debounce implementation, so it calls the debounced method immediately +jest.mock('lodash.debounce', () => + jest.fn((fn) => { + //eslint-disable-next-line + fn.cancel = jest.fn(); + //eslint-disable-next-line + fn.flush = jest.fn(); + return fn; + }), +); + +const inputPlaceholder = 'Type your message'; +const userId = 'userId'; +const username = 'username'; +const mentionId = 'mention-id'; +const mentionName = 'mention-name'; +const user1 = generateUser({ id: userId, name: username }); +const mentionUser = generateUser({ + id: mentionId, + name: mentionName, +}); +const mainListMessage = generateMessage({ user: user1 }); +const threadMessage = generateMessage({ + parent_id: mainListMessage.id, + type: 'reply', + user: user1, +}); +const mockedChannel = generateChannel({ + members: [generateMember({ user: user1 }), generateMember({ user: mentionUser })], + messages: [mainListMessage], + thread: [threadMessage], +}); + +const defaultMessageContextValue = { + getMessageActions: () => ['delete', 'edit', 'quote'], + handleDelete: () => {}, + handleFlag: () => {}, + handleMute: () => {}, + handlePin: () => {}, + isMyMessage: () => true, + message: mainListMessage, + setEditingState: () => {}, +}; + +let chatClient; +let channel; + +const initQuotedMessagePreview = async (message) => { + await waitFor(() => expect(screen.queryByText(message.text)).not.toBeInTheDocument()); + + const quoteButton = await screen.findByText(/^reply$/i); + await waitFor(() => expect(quoteButton).toBeInTheDocument()); + + act(() => { + fireEvent.click(quoteButton); + }); +}; + +const ActiveChannelSetter = ({ activeChannel }) => { + const { setActiveChannel } = useChatContext(); + useEffect(() => { + setActiveChannel(activeChannel); + }, [activeChannel]); + return null; +}; + +const ChatContextOverrider = ({ children, contextOverrides }) => { + const context = useChatContext(); + return {children}; +}; + +const makeRenderFn = (InputComponent) => async ({ + messageInputProps = {}, + channelProps = {}, + chatContextOverrides = {}, + client = chatClient, + messageContextOverrides = {}, + messageActionsBoxProps = {}, +} = {}) => { + // circumvents not so good decision to render SendButton conditionally + const InputContainer = () => { + const { handleSubmit, message } = useMessageInputContext(); + return ( + <> + + {!!message && } + + ); + }; + + let renderResult; + await act(() => { + renderResult = render( + + + + + + + + + + + , + ); + }); + const submit = async () => { + const submitButton = renderResult.findByText('Send') || renderResult.findByTitle('Send'); + fireEvent.click(await submitButton); + }; + + return { submit, ...renderResult }; +}; + +const tearDown = () => { + cleanup(); + jest.clearAllMocks(); +}; + +describe('Link preview', () => { + const LINK_PREVIEW_TEST_ID = 'link-preview-card'; + const LINK_PREVIEW_DISMISS_BTN_TEST_ID = 'link-preview-card-dismiss-btn'; + const CHAT_CONTEXT_OVERRIDES_COMMON = { themeVersion: '2' }; + const MESSAGE_INPUT_PROPS_COMMON = { + urlEnrichmentConfig: { enrichURLForPreview: true }, + }; + const renderComponent = makeRenderFn(MessageInputFlat); + const scrapedData = generateScrapedDataAttachment({ + og_scrape_url: 'http://getstream.io', + title: 'http://getstream.io', + }); + const scrapedData1 = generateScrapedDataAttachment({ + og_scrape_url: 'http://getstream.io', + title: 'http://getstream.io', + }); + const scrapedData2 = generateScrapedDataAttachment({ + og_scrape_url: 'http://getstream.io/', + title: 'http://getstream.io/', + }); + const scrapedData3 = generateScrapedDataAttachment({ + og_scrape_url: 'http://getstream.io/abc', + title: 'http://getstream.io/abc', + }); + + beforeEach(async () => { + chatClient = await getTestClientWithUser({ id: user1.id }); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); + channel = chatClient.channel('messaging', mockedChannel.id); + }); + + afterEach(tearDown); + + it('does not request URL enrichment if disabled in channel config', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const { + channel: { config }, + } = generateChannel({ config: { url_enrichment: false } }); + channel.getConfig = () => config; + const enrichSpy = jest.spyOn(chatClient, 'enrichURL'); + await renderComponent({ + channelProps: { channel }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).not.toHaveBeenCalled(); + }); + + it('does not request URL enrichment + not render if link previews are not enabled through Channel props', async () => { + const enrichSpy = jest.spyOn(chatClient, 'enrichURL'); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + }); + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: '', + }, + }); + }); + + expect(enrichSpy).not.toHaveBeenCalled(); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).not.toBeInTheDocument(); + }); + + it('request URL enrichment + render if link previews are enabled through Channel props', async () => { + const enrichSpy = jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValue({ duration: '10ms', ...scrapedData }); + await renderComponent({ + channelProps: { enrichURLForPreview: true }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + }); + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).toHaveBeenCalledWith(scrapedData.og_scrape_url); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).toBeInTheDocument(); + }); + + it('does not render if link previews are disabled through MessageInput props', async () => { + const enrichSpy = jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValue({ duration: '10ms', ...scrapedData }); + await renderComponent({ + channelProps: { enrichURLForPreview: true }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { urlEnrichmentConfig: { enrichURLForPreview: false } }, + }); + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).not.toHaveBeenCalled(); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).not.toBeInTheDocument(); + }); + + it('does not render if no text', async () => { + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: '', + }, + }); + }); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).not.toBeInTheDocument(); + }); + + it('does not render if no URLs found', async () => { + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: 'X', + }, + }); + }); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).not.toBeInTheDocument(); + }); + + it('does not render queued or loading links', async () => { + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: 'X https://getstream.io', + }, + }); + }); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).not.toBeInTheDocument(); + }); + + it('does not render failed-to-fetch links', async () => { + jest.spyOn(chatClient, 'enrichURL').mockRejectedValueOnce(new Error()); + await renderComponent({ + chatContextOverrides: { ...CHAT_CONTEXT_OVERRIDES_COMMON }, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: 'X https://getstream.io', + }, + }); + }); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).not.toBeInTheDocument(); + }); + + it('renders for URL with protocol', async () => { + jest.spyOn(chatClient, 'enrichURL').mockResolvedValueOnce({ duration: '10ms', ...scrapedData }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + await waitFor(() => { + expect(linkPreviews).toBeInTheDocument(); + }); + }); + + it('renders for URL without protocol', async () => { + jest.spyOn(chatClient, 'enrichURL').mockResolvedValueOnce({ duration: '10ms', ...scrapedData }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + const linkPreviews = await screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(1); + expect(linkPreviews[0]).toHaveTextContent(scrapedData.og_scrape_url); + }); + + it('does not render with quoted message', async () => { + const { message } = defaultMessageContextValue; + jest.spyOn(chatClient, 'enrichURL').mockResolvedValueOnce({ duration: '10ms', ...scrapedData }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageContextOverrides: { message }, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + await initQuotedMessagePreview(message); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + const linkPreview = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreview).not.toBeInTheDocument(); + }); + + it('renders for all the URLs', async () => { + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData2 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData3 }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + let linkPreviews = await screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(1); + expect(linkPreviews[0]).toHaveTextContent(scrapedData1.og_scrape_url); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url}`, + }, + }); + }); + linkPreviews = await screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(2); + expect(linkPreviews[0]).toHaveTextContent(scrapedData1.og_scrape_url); + expect(linkPreviews[1]).toHaveTextContent(scrapedData2.og_scrape_url); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url} ${scrapedData3.og_scrape_url}`, + }, + }); + }); + linkPreviews = await screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(3); + expect(linkPreviews[0]).toHaveTextContent(scrapedData1.og_scrape_url); + expect(linkPreviews[1]).toHaveTextContent(scrapedData2.og_scrape_url); + expect(linkPreviews[2]).toHaveTextContent(scrapedData3.og_scrape_url); + }); + + it('renders for all the pasted URLs', async () => { + const pastedText = `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url} ${scrapedData3.og_scrape_url}`; + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData2 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData3 }); + + const pastedItem = { + getAsString: (cb) => cb(pastedText), + kind: 'string', + }; + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.paste(await screen.findByPlaceholderText(inputPlaceholder), { + clipboardData: { + items: [ + { + ...pastedItem, + type: 'text/plain', + }, + { + ...pastedItem, + type: 'text/html', + }, + ], + }, + }); + }); + + await waitFor(() => { + const linkPreviews = screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(3); + expect(linkPreviews[0]).toHaveTextContent(scrapedData1.og_scrape_url); + expect(linkPreviews[1]).toHaveTextContent(scrapedData2.og_scrape_url); + expect(linkPreviews[2]).toHaveTextContent(scrapedData3.og_scrape_url); + }); + }); + + it('renders as single preview if duplicates are present', async () => { + jest.spyOn(chatClient, 'enrichURL').mockResolvedValueOnce({ duration: '10ms', ...scrapedData }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + let linkPreviews = await screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(1); + expect(linkPreviews[0]).toHaveTextContent(scrapedData.og_scrape_url); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url} ${scrapedData.og_scrape_url}`, + }, + }); + }); + linkPreviews = await screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(1); + expect(linkPreviews[0]).toHaveTextContent(scrapedData.og_scrape_url); + }); + + it('prevent duplicate URL enrichment for URLs, where enrichment has not failed', async () => { + const enrichSpy = jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValue({ duration: '10ms', ...scrapedData }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).toHaveBeenCalledWith(scrapedData.og_scrape_url); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url} ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).toHaveBeenCalledTimes(1); + }); + + it('request enrichment for duplicate URLs where enrichment previously failed', async () => { + const enrichSpy = jest.spyOn(chatClient, 'enrichURL').mockRejectedValueOnce(new Error()); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).toHaveBeenCalledWith(scrapedData.og_scrape_url); + expect(enrichSpy).toHaveBeenCalledTimes(1); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url} ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).toHaveBeenCalledWith(scrapedData.og_scrape_url); + expect(enrichSpy).toHaveBeenCalledTimes(2); + }); + + it('is unmounted when the URL is removed from the textarea value', async () => { + jest.spyOn(chatClient, 'enrichURL').mockResolvedValueOnce({ duration: '10ms', ...scrapedData }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + let linkPreviews = screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(1); + expect(linkPreviews[0]).toHaveTextContent(scrapedData.og_scrape_url); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url.slice(0, -1)}`, + }, + }); + }); + linkPreviews = screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(0); + }); + + it('is unmounted and disabled when dismissed', async () => { + const typedText = `X ${scrapedData.og_scrape_url}`; + jest.spyOn(chatClient, 'enrichURL').mockResolvedValueOnce({ duration: '10ms', ...scrapedData }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: typedText, + }, + }); + }); + let linkPreviews = screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(1); + expect(linkPreviews[0]).toHaveTextContent(scrapedData.og_scrape_url); + + await act(async () => { + fireEvent.click(await screen.findByTestId(LINK_PREVIEW_DISMISS_BTN_TEST_ID)); + }); + linkPreviews = screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(0); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: typedText, + }, + }); + }); + + expect(screen.queryByTestId(LINK_PREVIEW_TEST_ID)).not.toBeInTheDocument(); + }); + + it('are sent as attachments to posted message with skip_enrich_url:true', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); + + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData2 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData3 }); + const { submit } = await renderComponent({ + channelProps: { channel }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url} ${scrapedData3.og_scrape_url}`, + }, + }); + }); + await act(() => submit()); + + expect(sendMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attachments: expect.arrayContaining([ + expect.objectContaining(scrapedData1), + expect.objectContaining(scrapedData2), + expect.objectContaining(scrapedData3), + ]), + }), + expect.objectContaining({ skip_enrich_url: true }), + ); + }); + + it('are not sent as attachments to posted message with skip_enrich_url:true if dismissed', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); + + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData2 }); + const { submit } = await renderComponent({ + channelProps: { channel }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + const dismissButtons = await screen.findAllByTestId(LINK_PREVIEW_DISMISS_BTN_TEST_ID); + fireEvent.click(dismissButtons[1]); + }); + + await act(() => submit()); + expect(sendMessageSpy.mock.calls[0][0].attachments).toHaveLength(1); + expect(sendMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attachments: expect.arrayContaining([expect.objectContaining(scrapedData1)]), + }), + expect.objectContaining({ skip_enrich_url: true }), + ); + }); + + it('does not add failed link previews among attachments', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); + + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + jest.spyOn(chatClient, 'enrichURL').mockRejectedValueOnce(); + const { submit } = await renderComponent({ + channelProps: { channel }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url}`, + }, + }); + }); + + await act(() => submit()); + + expect(sendMessageSpy.mock.calls[0][0].attachments).toHaveLength(1); + expect(sendMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attachments: expect.arrayContaining([expect.objectContaining(scrapedData1)]), + }), + expect.objectContaining({ skip_enrich_url: true }), + ); + }); + + it('does not add dismissed link previews among attachments', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData2 }); + const { submit } = await renderComponent({ + channelProps: { channel }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData2.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + const dismissButtons = await screen.findAllByTestId(LINK_PREVIEW_DISMISS_BTN_TEST_ID); + fireEvent.click(dismissButtons[1]); + }); + + await act(() => submit()); + + expect(sendMessageSpy.mock.calls[0][0].attachments).toHaveLength(1); + expect(sendMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attachments: expect.arrayContaining([expect.objectContaining(scrapedData1)]), + }), + expect.objectContaining({ skip_enrich_url: true }), + ); + }); + + it('does not render duplicate link previews', async () => { + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + const linkPreviews = await screen.queryAllByTestId(LINK_PREVIEW_TEST_ID); + // eslint-disable-next-line + expect(linkPreviews).toHaveLength(1); + expect(linkPreviews[0]).toHaveTextContent(scrapedData1.og_scrape_url); + }); + + it('are sent as attachments to posted message with skip_enrich_url:true on message update', async () => { + const editMessageSpy = jest.spyOn(chatClient, 'updateMessage').mockImplementation(); + const existingMessage = generateMessage({ attachments: [scrapedData1] }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + + const { submit } = await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + ...MESSAGE_INPUT_PROPS_COMMON, + message: existingMessage, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url} Y`, + }, + }); + }); + + await act(() => submit()); + + expect(editMessageSpy.mock.calls[0][0].attachments).toHaveLength(1); + expect(editMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attachments: expect.arrayContaining([expect.objectContaining(scrapedData1)]), + }), + undefined, + expect.objectContaining({ skip_enrich_url: true }), + ); + }); + + it('are sent with updated attachments on message update', async () => { + const editMessageSpy = jest.spyOn(chatClient, 'updateMessage').mockImplementation(); + const scrapedAudioAttachment = generateScrapedAudioAttachment({ + og_scrape_url: 'http://getstream.io/audio', + }); + const scrapedImageAttachment = generateScrapedImageAttachment({ + og_scrape_url: 'http://getstream.io/image', + }); + const existingMessage = generateMessage({ attachments: [scrapedAudioAttachment] }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedImageAttachment }); + + const { submit } = await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + ...MESSAGE_INPUT_PROPS_COMMON, + message: existingMessage, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedImageAttachment.og_scrape_url}`, + }, + }); + }); + + await act(() => submit()); + + expect(editMessageSpy.mock.calls[0][0].attachments).toHaveLength(1); + expect(editMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attachments: expect.arrayContaining([expect.objectContaining(scrapedImageAttachment)]), + }), + undefined, + expect.objectContaining({ skip_enrich_url: true }), + ); + }); + + it('should not submit updated scraped data when enrichment in preview is disabled', async () => { + const editMessageSpy = jest.spyOn(chatClient, 'updateMessage').mockImplementation(); + const scrapedAudioAttachment = generateScrapedAudioAttachment({ + og_scrape_url: 'http://getstream.io/audio', + }); + const scrapedImageAttachment = generateScrapedImageAttachment({ + og_scrape_url: 'http://getstream.io/image', + }); + const existingMessage = generateMessage({ attachments: [scrapedAudioAttachment] }); + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedImageAttachment }); + + const { submit } = await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + message: existingMessage, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedImageAttachment.og_scrape_url}`, + }, + }); + }); + + await act(() => submit()); + + expect(editMessageSpy.mock.calls[0][0].attachments).toHaveLength(1); + expect(editMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attachments: expect.arrayContaining([expect.objectContaining(scrapedAudioAttachment)]), + }), + undefined, + undefined, + ); + }); + + it('submit new message with skip_url_enrich:false if no link previews managed to get loaded', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); + let resolveEnrichURLPromise; + jest + .spyOn(chatClient, 'enrichURL') + // eslint-disable-next-line no-unused-vars + .mockImplementationOnce(() => new Promise((res) => (resolveEnrichURLPromise = res))); + + const { submit } = await renderComponent({ + channelProps: { channel }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(() => submit()); + + expect(sendMessageSpy.mock.calls[0][0].attachments).toHaveLength(0); + expect(sendMessageSpy.mock.calls[0][1].skip_enrich_url).toBe(false); + }); + + it('submit updated message with skip_url_enrich:false if no link previews managed to get loaded', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const scrapedAudioAttachment = generateScrapedAudioAttachment({ + og_scrape_url: 'http://getstream.io/audio', + }); + const scrapedImageAttachment = generateScrapedImageAttachment({ + og_scrape_url: 'http://getstream.io/image', + }); + const existingMessage = generateMessage({ attachments: [scrapedAudioAttachment] }); + + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); + + let resolveEnrichURLPromise; + jest + .spyOn(chatClient, 'enrichURL') + // eslint-disable-next-line no-unused-vars + .mockImplementationOnce(() => new Promise((res) => (resolveEnrichURLPromise = res))); + + const { submit } = await renderComponent({ + channelProps: { channel }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + ...MESSAGE_INPUT_PROPS_COMMON, + existingMessage, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedImageAttachment.og_scrape_url}`, + }, + }); + }); + + await act(() => submit()); + + expect(sendMessageSpy.mock.calls[0][0].attachments).toHaveLength(0); + expect(sendMessageSpy.mock.calls[0][1].skip_enrich_url).toBe(false); + }); + + it('should not be shown after submit if the URL enrichment has not finished', async () => { + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + const { submit } = await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: MESSAGE_INPUT_PROPS_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(() => submit()); + const linkPreviews = await screen.queryByTestId(LINK_PREVIEW_TEST_ID); + expect(linkPreviews).not.toBeInTheDocument(); + }); + + it('are retrieved with custom search function', async () => { + // on purpose returning scrapedData2 + const findURLFn = jest.fn().mockReturnValueOnce([scrapedData2.og_scrape_url]); + const typedText = `X ${scrapedData1.og_scrape_url}`; + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + urlEnrichmentConfig: { + enrichURLForPreview: true, + findURLFn, + }, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: typedText, + }, + }); + }); + + expect(findURLFn).toHaveBeenCalledWith(typedText); + }); + + it('calls findURLFn passed to Channel', async () => { + const enrichSpy = jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValue({ duration: '10ms', ...scrapedData }); + const findURLFnChannel = jest.fn().mockReturnValueOnce([scrapedData.og_scrape_url]); + await renderComponent({ + channelProps: { + enrichURLForPreview: true, + enrichURLForPreviewConfig: { findURLFn: findURLFnChannel }, + }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).toHaveBeenCalledTimes(1); + expect(findURLFnChannel).toHaveBeenCalledTimes(1); + }); + + it('gives preference to findURLFn passed directly to MessageInput over the channel state context', async () => { + const enrichSpy = jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValue({ duration: '10ms', ...scrapedData }); + const findURLFnChannel = jest.fn().mockReturnValueOnce([scrapedData.og_scrape_url]); + const findURLFnMsgInput = jest.fn().mockReturnValueOnce([scrapedData.og_scrape_url]); + await renderComponent({ + channelProps: { + enrichURLForPreview: true, + enrichURLForPreviewConfig: { findURLFn: findURLFnChannel }, + }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { urlEnrichmentConfig: { findURLFn: findURLFnMsgInput } }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(enrichSpy).toHaveBeenCalledTimes(1); + expect(findURLFnChannel).not.toHaveBeenCalled(); + expect(findURLFnMsgInput).toHaveBeenCalledTimes(1); + }); + + it('enables custom handling of dismissal', async () => { + const onLinkPreviewDismissed = jest.fn(); + const typedText = `X ${scrapedData1.og_scrape_url}`; + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + await renderComponent({ + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + urlEnrichmentConfig: { + enrichURLForPreview: true, + onLinkPreviewDismissed, + }, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: typedText, + }, + }); + }); + + await act(async () => { + fireEvent.click(await screen.findByTestId(LINK_PREVIEW_DISMISS_BTN_TEST_ID)); + }); + + expect(onLinkPreviewDismissed).toHaveBeenCalledTimes(1); + }); + + it('calls onLinkPreviewDismissed passed to Channel', async () => { + const enrichSpy = jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValue({ duration: '10ms', ...scrapedData }); + const onLinkPreviewDismissedChannel = jest.fn(); + await renderComponent({ + channelProps: { + enrichURLForPreview: true, + enrichURLForPreviewConfig: { onLinkPreviewDismissed: onLinkPreviewDismissedChannel }, + }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.click(await screen.findByTestId(LINK_PREVIEW_DISMISS_BTN_TEST_ID)); + }); + + expect(enrichSpy).toHaveBeenCalledTimes(1); + expect(onLinkPreviewDismissedChannel).toHaveBeenCalledWith( + expect.objectContaining(scrapedData), + ); + }); + + it('gives preference to onLinkPreviewDismissed passed directly to MessageInput over the channel state context', async () => { + const enrichSpy = jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValue({ duration: '10ms', ...scrapedData }); + const onLinkPreviewDismissedChannel = jest.fn(); + const onLinkPreviewDismissedInput = jest.fn(); + await renderComponent({ + channelProps: { + enrichURLForPreview: true, + enrichURLForPreviewConfig: { onLinkPreviewDismissed: onLinkPreviewDismissedChannel }, + }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + urlEnrichmentConfig: { + onLinkPreviewDismissed: onLinkPreviewDismissedInput, + }, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + await act(async () => { + fireEvent.click(await screen.findByTestId(LINK_PREVIEW_DISMISS_BTN_TEST_ID)); + }); + + expect(enrichSpy).toHaveBeenCalledTimes(1); + expect(onLinkPreviewDismissedInput).toHaveBeenCalledWith(expect.objectContaining(scrapedData)); + expect(onLinkPreviewDismissedChannel).not.toHaveBeenCalled(); + }); + + it('gives preference to debounceURLEnrichmentMs passed to Channel', async () => { + const debounceURLEnrichmentMsChannel = 500; + await renderComponent({ + channelProps: { + enrichURLForPreview: true, + enrichURLForPreviewConfig: { debounceURLEnrichmentMs: debounceURLEnrichmentMsChannel }, + }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(debounce).toHaveBeenCalledWith(expect.any(Function), debounceURLEnrichmentMsChannel, { + leading: false, + trailing: true, + }); + }); + + it('gives preference to debounceURLEnrichmentMs passed directly over the channel state context', async () => { + const debounceURLEnrichmentMsChannel = 500; + const debounceURLEnrichmentMsInput = 1000; + await renderComponent({ + channelProps: { + enrichURLForPreview: true, + enrichURLForPreviewConfig: { debounceURLEnrichmentMs: debounceURLEnrichmentMsChannel }, + }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + messageInputProps: { + urlEnrichmentConfig: { + debounceURLEnrichmentMs: debounceURLEnrichmentMsInput, + }, + }, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(debounce).toHaveBeenCalledWith(expect.any(Function), debounceURLEnrichmentMsInput, { + leading: false, + trailing: true, + }); + }); + + it('are rendered in custom LinkPreviewList component', async () => { + jest + .spyOn(chatClient, 'enrichURL') + .mockResolvedValueOnce({ duration: '10ms', ...scrapedData1 }); + const customTestId = 'custom-link-preview'; + const CustomLinkPreviewList = () =>
; + await renderComponent({ + channelProps: { enrichURLForPreview: true, LinkPreviewList: CustomLinkPreviewList }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + }); + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData.og_scrape_url}`, + }, + }); + }); + + expect(await screen.queryByTestId(customTestId)).toBeInTheDocument(); + expect(await screen.queryByTestId(LINK_PREVIEW_TEST_ID)).not.toBeInTheDocument(); + }); + + it('link preview state is cleared after message submission', async () => { + const channel = chatClient.channel('messaging', mockedChannel.id); + const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); + let resolveEnrichURLPromise; + jest + .spyOn(chatClient, 'enrichURL') + .mockImplementationOnce(() => new Promise((res) => (resolveEnrichURLPromise = res))); + + const { submit } = await renderComponent({ + channelProps: { channel, enrichURLForPreview: true }, + chatContextOverrides: CHAT_CONTEXT_OVERRIDES_COMMON, + }); + + await act(async () => { + fireEvent.change(await screen.findByPlaceholderText(inputPlaceholder), { + target: { + value: `X ${scrapedData1.og_scrape_url}`, + }, + }); + }); + + await act(() => submit()); + + expect(screen.queryByTestId(LINK_PREVIEW_TEST_ID)).not.toBeInTheDocument(); + expect(sendMessageSpy.mock.calls[0][0].attachments).toHaveLength(0); + expect(sendMessageSpy.mock.calls[0][1].skip_enrich_url).toBe(false); + + await act(() => { + resolveEnrichURLPromise({ duration: '10ms', ...scrapedData1 }); + }); + + await waitFor(() => { + expect(screen.queryByTestId(LINK_PREVIEW_TEST_ID)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/MessageInput/__tests__/MessageInput.test.js b/src/components/MessageInput/__tests__/MessageInput.test.js index ca4ef8f4d..319d6e6b9 100644 --- a/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/src/components/MessageInput/__tests__/MessageInput.test.js @@ -650,6 +650,7 @@ function axeNoViolations(container) { expect.objectContaining({ text: messageText, }), + undefined, ); await axeNoViolations(container); }); @@ -700,6 +701,7 @@ function axeNoViolations(container) { expect(calledMock).toHaveBeenCalledWith( expect.stringMatching(/.+:.+/), expect.objectContaining(customMessageData), + undefined, ); }); await axeNoViolations(container); @@ -729,6 +731,7 @@ function axeNoViolations(container) { }), channel.cid, customMessageData, + undefined, ); await axeNoViolations(container); }); @@ -771,6 +774,7 @@ function axeNoViolations(container) { }), ]), }), + undefined, ); await axeNoViolations(container); }); @@ -804,6 +808,7 @@ function axeNoViolations(container) { }), ]), }), + undefined, ); await axeNoViolations(container); }); @@ -839,6 +844,7 @@ function axeNoViolations(container) { }), ]), }), + undefined, ); await axeNoViolations(container); }); @@ -869,6 +875,7 @@ function axeNoViolations(container) { }), channel.cid, undefined, + undefined, ); await axeNoViolations(container); }); @@ -928,6 +935,7 @@ function axeNoViolations(container) { }), channel.cid, undefined, + undefined, ); await axeNoViolations(container); @@ -1002,6 +1010,7 @@ function axeNoViolations(container) { mentioned_users: [{ id: userId, name: username }], text: message.text, }), + undefined, ); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -1035,6 +1044,7 @@ function axeNoViolations(container) { expect.objectContaining({ mentioned_users: expect.arrayContaining([mentionId]), }), + undefined, ); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -1068,6 +1078,7 @@ function axeNoViolations(container) { expect.objectContaining({ mentioned_users: [], }), + undefined, ); const results = await axe(container); expect(results).toHaveNoViolations(); diff --git a/src/components/MessageInput/hooks/useCreateMessageInputContext.ts b/src/components/MessageInput/hooks/useCreateMessageInputContext.ts index d152bc295..a4e672da8 100644 --- a/src/components/MessageInput/hooks/useCreateMessageInputContext.ts +++ b/src/components/MessageInput/hooks/useCreateMessageInputContext.ts @@ -13,6 +13,7 @@ export const useCreateMessageInputContext = < additionalTextareaProps, attachments, autocompleteTriggers, + cancelURLEnrichment, clearEditingState, closeCommandsList, closeEmojiPicker, @@ -21,6 +22,7 @@ export const useCreateMessageInputContext = < cooldownRemaining, disabled, disableMentions, + dismissLinkPreview, doFileUploadRequest, doImageUploadRequest, emojiIndex, @@ -29,6 +31,7 @@ export const useCreateMessageInputContext = < errorHandler, fileOrder, fileUploads, + findAndEnqueueURLsToEnrich, focus, grow, handleChange, @@ -38,6 +41,7 @@ export const useCreateMessageInputContext = < imageUploads, insertText, isUploadEnabled, + linkPreviews, maxFilesLeft, maxRows, mentionAllAppUsers, @@ -79,6 +83,7 @@ export const useCreateMessageInputContext = < // eslint-disable-next-line .map(([_, value]) => value.state) .join(); + const linkPreviewsValue = Array.from(linkPreviews.values()).join(); const mentionedUsersLength = mentioned_users.length; const parentId = parent?.id; @@ -87,6 +92,7 @@ export const useCreateMessageInputContext = < additionalTextareaProps, attachments, autocompleteTriggers, + cancelURLEnrichment, clearEditingState, closeCommandsList, closeEmojiPicker, @@ -95,6 +101,7 @@ export const useCreateMessageInputContext = < cooldownRemaining, disabled, disableMentions, + dismissLinkPreview, doFileUploadRequest, doImageUploadRequest, emojiIndex, @@ -103,6 +110,7 @@ export const useCreateMessageInputContext = < errorHandler, fileOrder, fileUploads, + findAndEnqueueURLsToEnrich, focus, grow, handleChange, @@ -112,6 +120,7 @@ export const useCreateMessageInputContext = < imageUploads, insertText, isUploadEnabled, + linkPreviews, maxFilesLeft, maxRows, mentionAllAppUsers, @@ -144,13 +153,17 @@ export const useCreateMessageInputContext = < useMentionsTransliteration, }), [ + cancelURLEnrichment, cooldownInterval, cooldownRemaining, + dismissLinkPreview, editing, emojiPickerIsOpen, fileUploadsValue, + findAndEnqueueURLsToEnrich, imageUploadsValue, isUploadEnabled, + linkPreviewsValue, mentionedUsersLength, parentId, publishTypingEvent, diff --git a/src/components/MessageInput/hooks/useFileState.ts b/src/components/MessageInput/hooks/useFileState.ts index fc8443996..f692a9619 100644 --- a/src/components/MessageInput/hooks/useFileState.ts +++ b/src/components/MessageInput/hooks/useFileState.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import type { FileUpload } from './useMessageInputState'; +import type { FileUpload } from '../types'; export const useFileState = >(file: T) => useMemo( diff --git a/src/components/MessageInput/hooks/useLinkPreviews.ts b/src/components/MessageInput/hooks/useLinkPreviews.ts new file mode 100644 index 000000000..c9255ce84 --- /dev/null +++ b/src/components/MessageInput/hooks/useLinkPreviews.ts @@ -0,0 +1,176 @@ +import { find } from 'linkifyjs'; +import { Dispatch, useCallback, useEffect, useRef } from 'react'; +import debounce from 'lodash.debounce'; +import { useChannelStateContext, useChatContext } from '../../../context'; +import type { MessageInputReducerAction, MessageInputState } from './useMessageInputState'; +import type { DefaultStreamChatGenerics } from '../../../types/types'; +import type { LinkPreview, LinkPreviewMap } from '../types'; +import { LinkPreviewState, SetLinkPreviewMode } from '../types'; +import type { DebouncedFunc } from 'lodash'; + +export type URLEnrichmentConfig = { + /** Number of milliseconds to debounce firing the URL enrichment queries when typing. The default value is 1500(ms). */ + debounceURLEnrichmentMs?: number; + /** Allows for toggling the URL enrichment and link previews in `MessageInput`. By default, the feature is disabled. */ + enrichURLForPreview?: boolean; + /** Custom function to identify URLs in a string and request OG data */ + findURLFn?: (text: string) => string[]; + /** Custom function to react to link preview dismissal */ + onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void; +}; + +type UseEnrichURLsParams< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +> = URLEnrichmentConfig & { + dispatch: Dispatch>; + linkPreviews: MessageInputState['linkPreviews']; +}; + +export type EnrichURLsController = { + /** Function cancels all the scheduled or in-progress URL enrichment queries and resets the state. */ + cancelURLEnrichment: () => void; + /** Function called when a single link preview is dismissed. */ + dismissLinkPreview: (linkPreview: LinkPreview) => void; + /** Function that triggers the search for URLs and their enrichment. */ + findAndEnqueueURLsToEnrich?: DebouncedFunc<(text: string, mode?: SetLinkPreviewMode) => void>; +}; + +export const useLinkPreviews = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>({ + debounceURLEnrichmentMs: debounceURLEnrichmentMsInputContext, + dispatch, + enrichURLForPreview = false, + findURLFn: findURLFnInputContext, + linkPreviews, + onLinkPreviewDismissed: onLinkPreviewDismissedInputContext, +}: UseEnrichURLsParams): EnrichURLsController => { + const { client } = useChatContext(); + // FIXME: the value of channelConfig is stale due to omitting it from the memoization deps in useCreateChannelStateContext + const { + channelConfig, + debounceURLEnrichmentMs: debounceURLEnrichmentMsChannelContext, + findURLFn: findURLFnChannelContext, + onLinkPreviewDismissed: onLinkPreviewDismissedChannelContext, + } = useChannelStateContext(); + + const shouldDiscardEnrichQueries = useRef(false); + + const findURLFn = findURLFnInputContext ?? findURLFnChannelContext; + const onLinkPreviewDismissed = + onLinkPreviewDismissedInputContext ?? onLinkPreviewDismissedChannelContext; + const debounceURLEnrichmentMs = + debounceURLEnrichmentMsInputContext ?? debounceURLEnrichmentMsChannelContext ?? 1500; + + const dismissLinkPreview = useCallback( + (linkPreview: LinkPreview) => { + onLinkPreviewDismissed?.(linkPreview); + const previewToRemoveMap = new Map(); + linkPreview.state = LinkPreviewState.DISMISSED; + previewToRemoveMap.set(linkPreview.og_scrape_url, linkPreview); + dispatch({ + linkPreviews: previewToRemoveMap, + mode: SetLinkPreviewMode.UPSERT, + type: 'setLinkPreviews', + }); + }, + [onLinkPreviewDismissed], + ); + + const findAndEnqueueURLsToEnrich = useCallback( + debounce( + (text: string, mode = SetLinkPreviewMode.SET) => { + const urls = findURLFn + ? findURLFn(text) + : find(text, 'url').reduce((acc, link) => { + if (link.isLink) acc.push(link.href); + return acc; + }, []); + + shouldDiscardEnrichQueries.current = urls.length === 0; + + dispatch({ + linkPreviews: urls.reduce((acc, url) => { + acc.set(url, { og_scrape_url: url, state: LinkPreviewState.QUEUED }); + return acc; + }, new Map()), + mode, + type: 'setLinkPreviews', + }); + }, + debounceURLEnrichmentMs, + { leading: false, trailing: true }, + ), + [debounceURLEnrichmentMs, shouldDiscardEnrichQueries, findURLFn], + ); + + const cancelURLEnrichment = useCallback(() => { + findAndEnqueueURLsToEnrich.cancel(); + findAndEnqueueURLsToEnrich(''); + findAndEnqueueURLsToEnrich.flush(); + }, [findAndEnqueueURLsToEnrich]); + + useEffect(() => { + const enqueuedLinks = Array.from(linkPreviews.values()).reduce( + (acc, linkPreview) => { + if (linkPreview.state === 'queued') { + const loadingLinkPreview: LinkPreview = { + ...linkPreview, + state: LinkPreviewState.LOADING, + }; + acc.set(linkPreview.og_scrape_url, loadingLinkPreview); + } + return acc; + }, + new Map(), + ); + + if (!enqueuedLinks.size) return; + + dispatch({ + linkPreviews: enqueuedLinks, + mode: SetLinkPreviewMode.UPSERT, + type: 'setLinkPreviews', + }); + + enqueuedLinks.forEach((linkPreview) => { + client + .enrichURL(linkPreview.og_scrape_url) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .then(({ duration, ...ogAttachment }) => { + if (shouldDiscardEnrichQueries.current) return; + + const linkPreviewsMap = new Map(); + linkPreviewsMap.set(linkPreview.og_scrape_url, { + ...ogAttachment, + state: LinkPreviewState.LOADED, + }); + + dispatch({ + linkPreviews: linkPreviewsMap, + mode: SetLinkPreviewMode.UPSERT, + type: 'setLinkPreviews', + }); + }) + .catch(() => { + const linkPreviewsMap = new Map(); + linkPreviewsMap.set(linkPreview.og_scrape_url, { + ...linkPreview, + state: LinkPreviewState.FAILED, + }); + dispatch({ + linkPreviews: linkPreviewsMap, + mode: SetLinkPreviewMode.UPSERT, + type: 'setLinkPreviews', + }); + }); + }); + }, [shouldDiscardEnrichQueries, linkPreviews]); + + return { + cancelURLEnrichment, + dismissLinkPreview, + findAndEnqueueURLsToEnrich: + channelConfig?.url_enrichment && enrichURLForPreview ? findAndEnqueueURLsToEnrich : undefined, + }; +}; diff --git a/src/components/MessageInput/hooks/useMessageInputState.ts b/src/components/MessageInput/hooks/useMessageInputState.ts index 3508dfa1d..72d5d5c33 100644 --- a/src/components/MessageInput/hooks/useMessageInputState.ts +++ b/src/components/MessageInput/hooks/useMessageInputState.ts @@ -12,48 +12,18 @@ import { usePasteHandler } from './usePasteHandler'; import type { EmojiData, NimbleEmojiIndex } from 'emoji-mart'; import type { FileLike } from '../../ReactFileUtilities'; -import type { Attachment, Message, UserResponse } from 'stream-chat'; +import type { Attachment, Message, OGAttachment, UserResponse } from 'stream-chat'; import type { MessageInputProps } from '../MessageInput'; -import type { CustomTrigger, DefaultStreamChatGenerics } from '../../../types/types'; - -export type FileUpload = { - file: { - name: string; - lastModified?: number; - lastModifiedDate?: Date; - size?: number; - type?: string; - uri?: string; - }; - id: string; - state: 'finished' | 'failed' | 'uploading'; - thumb_url?: string; - url?: string; -}; - -export type ImageUpload< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics -> = { - file: { - name: string; - height?: number; - lastModified?: number; - lastModifiedDate?: Date; - size?: number; - type?: string; - uri?: string; - width?: number; - }; - id: string; - state: 'finished' | 'failed' | 'uploading'; - previewUri?: string; - url?: string; -} & Pick< - Attachment, - 'og_scrape_url' | 'title' | 'title_link' | 'author_name' | 'text' ->; +import type { + CustomTrigger, + DefaultStreamChatGenerics, + SendMessageOptions, +} from '../../../types/types'; +import { EnrichURLsController, useLinkPreviews } from './useLinkPreviews'; +import type { FileUpload, ImageUpload, LinkPreviewMap } from '../types'; +import { LinkPreviewState, SetLinkPreviewMode } from '../types'; export type MessageInputState< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics @@ -64,6 +34,7 @@ export type MessageInputState< fileUploads: Record; imageOrder: string[]; imageUploads: Record; + linkPreviews: LinkPreviewMap; mentioned_users: UserResponse[]; setText: (text: string) => void; text: string; @@ -101,6 +72,12 @@ type SetFileUploadAction = { url?: string; }; +type SetLinkPreviewsAction = { + linkPreviews: LinkPreviewMap; + mode: SetLinkPreviewMode; + type: 'setLinkPreviews'; +}; + type RemoveImageUploadAction = { id: string; type: 'removeImageUpload'; @@ -126,13 +103,14 @@ export type MessageInputReducerAction< | ClearAction | SetImageUploadAction | SetFileUploadAction + | SetLinkPreviewsAction | RemoveImageUploadAction | RemoveFileUploadAction | AddMentionedUserAction; export type MessageInputHookProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics -> = { +> = EnrichURLsController & { closeEmojiPicker: React.MouseEventHandler; emojiPickerRef: React.MutableRefObject; handleChange: React.ChangeEventHandler; @@ -140,6 +118,7 @@ export type MessageInputHookProps< handleSubmit: ( event: React.BaseSyntheticEvent, customMessageData?: Partial>, + options?: SendMessageOptions, ) => void; insertText: (textToInsert: string) => void; isUploadEnabled: boolean; @@ -167,6 +146,7 @@ const makeEmptyMessageInputState = < fileUploads: {}, imageOrder: [], imageUploads: {}, + linkPreviews: new Map(), mentioned_users: [], setText: () => null, text: '', @@ -234,6 +214,16 @@ const initState = < {}, ) ?? {}; + const linkPreviews = + message.attachments?.reduce((acc, attachment) => { + if (!attachment.og_scrape_url) return acc; + acc.set(attachment.og_scrape_url, { + ...(attachment as OGAttachment), + state: LinkPreviewState.LOADED, + }); + return acc; + }, new Map()) ?? new Map(); + const imageOrder = Object.keys(imageUploads); const fileOrder = Object.keys(fileUploads); @@ -249,6 +239,7 @@ const initState = < fileUploads, imageOrder, imageUploads, + linkPreviews, mentioned_users, setText: () => null, text: message.text || '', @@ -306,6 +297,38 @@ const messageInputReducer = < }; } + case 'setLinkPreviews': { + const linkPreviews = new Map(state.linkPreviews); + + if (action.mode === SetLinkPreviewMode.REMOVE) { + Array.from(action.linkPreviews.keys()).forEach((key) => { + linkPreviews.delete(key); + }); + } else { + Array.from(action.linkPreviews.values()).reduce((acc, linkPreview) => { + const existingPreview = acc.get(linkPreview.og_scrape_url); + const alreadyEnqueued = + linkPreview.state === LinkPreviewState.QUEUED && + existingPreview?.state !== LinkPreviewState.FAILED; + + if (existingPreview && alreadyEnqueued) return acc; + acc.set(linkPreview.og_scrape_url, linkPreview); + return acc; + }, linkPreviews); + + if (action.mode === SetLinkPreviewMode.SET) { + Array.from(state.linkPreviews.keys()).forEach((key) => { + if (!action.linkPreviews.get(key)) linkPreviews.delete(key); + }); + } + } + + return { + ...state, + linkPreviews, + }; + } + case 'removeImageUpload': { if (!state.imageUploads[action.id]) return state; // cannot remove anything const newImageUploads = { ...state.imageUploads }; @@ -363,11 +386,19 @@ export const useMessageInputState = < MessageInputHookProps & CommandsListState & MentionsListState => { - const { additionalTextareaProps, closeEmojiPickerOnClick, getDefaultValue, message } = props; + const { + additionalTextareaProps, + closeEmojiPickerOnClick, + getDefaultValue, + message, + urlEnrichmentConfig, + } = props; - const { channelCapabilities = {}, channelConfig } = useChannelStateContext( - 'useMessageInputState', - ); + const { + channelCapabilities = {}, + channelConfig, + enrichURLForPreview: enrichURLForPreviewChannelContext, + } = useChannelStateContext('useMessageInputState'); const defaultValue = getDefaultValue?.() || additionalTextareaProps?.defaultValue; const initialStateValue = @@ -385,10 +416,19 @@ export const useMessageInputState = < initState, ); + const enrichURLsController = useLinkPreviews({ + dispatch, + linkPreviews: state.linkPreviews, + ...urlEnrichmentConfig, + enrichURLForPreview: + urlEnrichmentConfig?.enrichURLForPreview ?? enrichURLForPreviewChannelContext, + }); + const { handleChange, insertText, textareaRef } = useMessageInputText( props, state, dispatch, + enrichURLsController.findAndEnqueueURLsToEnrich, ); const [showCommandsList, setShowCommandsList] = useState(false); @@ -443,11 +483,17 @@ export const useMessageInputState = < state, dispatch, numberOfUploads, + enrichURLsController, ); const isUploadEnabled = channelConfig?.uploads !== false && channelCapabilities['upload-file'] !== false; - const { onPaste } = usePasteHandler(uploadNewFiles, insertText, isUploadEnabled); + const { onPaste } = usePasteHandler( + uploadNewFiles, + insertText, + isUploadEnabled, + enrichURLsController.findAndEnqueueURLsToEnrich, + ); const onSelectUser = useCallback((item: UserResponse) => { dispatch({ type: 'addMentionedUser', user: item }); @@ -459,6 +505,7 @@ export const useMessageInputState = < return { ...state, + ...enrichURLsController, closeCommandsList, /** * TODO: fix the below at some point because this type casting is wrong diff --git a/src/components/MessageInput/hooks/useMessageInputText.ts b/src/components/MessageInput/hooks/useMessageInputText.ts index cc17f764c..4e71ca756 100644 --- a/src/components/MessageInput/hooks/useMessageInputText.ts +++ b/src/components/MessageInput/hooks/useMessageInputText.ts @@ -5,6 +5,7 @@ import type { MessageInputProps } from '../MessageInput'; import { useChannelStateContext } from '../../../context/ChannelStateContext'; import type { CustomTrigger, DefaultStreamChatGenerics } from '../../../types/types'; +import type { EnrichURLsController } from './useLinkPreviews'; export const useMessageInputText = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -13,6 +14,7 @@ export const useMessageInputText = < props: MessageInputProps, state: MessageInputState, dispatch: React.Dispatch>, + findAndEnqueueURLsToEnrich?: EnrichURLsController['findAndEnqueueURLsToEnrich'], ) => { const { channel } = useChannelStateContext('useMessageInputText'); const { additionalTextareaProps, focus, parent, publishTypingEvent = true } = props; @@ -89,11 +91,14 @@ export const useMessageInputText = < getNewText: () => newText, type: 'setText', }); + + findAndEnqueueURLsToEnrich?.(newText); + if (publishTypingEvent && newText && channel) { logChatPromiseExecution(channel.keystroke(parent?.id), 'start typing event'); } }, - [channel, parent, publishTypingEvent], + [channel, findAndEnqueueURLsToEnrich, parent, publishTypingEvent], ); return { diff --git a/src/components/MessageInput/hooks/usePasteHandler.ts b/src/components/MessageInput/hooks/usePasteHandler.ts index 4c6fded2e..5a648cebf 100644 --- a/src/components/MessageInput/hooks/usePasteHandler.ts +++ b/src/components/MessageInput/hooks/usePasteHandler.ts @@ -4,11 +4,14 @@ import { dataTransferItemsToFiles, FileLike, } from '../../ReactFileUtilities'; +import type { EnrichURLsController } from './useLinkPreviews'; +import { SetLinkPreviewMode } from '../types'; export const usePasteHandler = ( uploadNewFiles: (files: FileList | FileLike[] | File[]) => void, insertText: (textToInsert: string) => void, isUploadEnabled: boolean, + findAndEnqueueURLsToEnrich?: EnrichURLsController['findAndEnqueueURLsToEnrich'], ) => { const onPaste = useCallback( (clipboardEvent: React.ClipboardEvent) => { @@ -45,6 +48,8 @@ export const usePasteHandler = ( if (plainTextPromise) { const pastedText = await plainTextPromise; insertText(pastedText); + findAndEnqueueURLsToEnrich?.(pastedText, SetLinkPreviewMode.UPSERT); + findAndEnqueueURLsToEnrich?.flush(); } })(clipboardEvent); }, diff --git a/src/components/MessageInput/hooks/useSubmitHandler.ts b/src/components/MessageInput/hooks/useSubmitHandler.ts index 8947f608f..8ac25c438 100644 --- a/src/components/MessageInput/hooks/useSubmitHandler.ts +++ b/src/components/MessageInput/hooks/useSubmitHandler.ts @@ -9,6 +9,8 @@ import type { MessageInputReducerAction, MessageInputState } from './useMessageI import type { MessageInputProps } from '../MessageInput'; import type { CustomTrigger, DefaultStreamChatGenerics } from '../../../types/types'; +import type { EnrichURLsController } from './useLinkPreviews'; +import { LinkPreviewState } from '../types'; const getAttachmentTypeFromMime = (mime: string) => { if (mime.includes('video/')) return 'video'; @@ -24,6 +26,7 @@ export const useSubmitHandler = < state: MessageInputState, dispatch: React.Dispatch>, numberOfUploads: number, + enrichURLsController: EnrichURLsController, ) => { const { clearEditingState, message, overrideSubmitHandler, parent, publishTypingEvent } = props; @@ -33,10 +36,12 @@ export const useSubmitHandler = < fileUploads, imageOrder, imageUploads, + linkPreviews, mentioned_users, text, } = state; + const { cancelURLEnrichment, findAndEnqueueURLsToEnrich } = enrichURLsController; const { channel } = useChannelStateContext('useSubmitHandler'); const { addNotification, editMessage, sendMessage } = useChannelActionContext( 'useSubmitHandler', @@ -127,7 +132,39 @@ export const useSubmitHandler = < return addNotification(t('Wait until all attachments have uploaded'), 'error'); } - const newAttachments = getAttachmentsFromUploads(); + let attachmentsFromUploads = getAttachmentsFromUploads(); + let attachmentsFromLinkPreviews: Attachment[] = []; + let someLinkPreviewsLoading; + let someLinkPreviewsDismissed; + if (findAndEnqueueURLsToEnrich) { + // filter out all the attachments scraped before the message was edited - only if the scr + attachmentsFromUploads = attachmentsFromUploads.filter( + (attachment) => !attachment.og_scrape_url, + ); + // prevent showing link preview in MessageInput after the message has been sent + cancelURLEnrichment(); + someLinkPreviewsLoading = Array.from(linkPreviews.values()).some((linkPreview) => + [LinkPreviewState.QUEUED, LinkPreviewState.LOADING].includes(linkPreview.state), + ); + someLinkPreviewsDismissed = Array.from(linkPreviews.values()).some( + (linkPreview) => linkPreview.state === LinkPreviewState.DISMISSED, + ); + + if (!someLinkPreviewsLoading) { + attachmentsFromLinkPreviews = Array.from(linkPreviews.values()) + .filter( + (linkPreview) => + linkPreview.state === LinkPreviewState.LOADED && + !attachmentsFromUploads.find( + (attFromUpload) => attFromUpload.og_scrape_url === linkPreview.og_scrape_url, + ), + ) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(({ state: linkPreviewState, ...ogAttachment }) => ogAttachment as Attachment); + } + } + + const newAttachments = [...attachmentsFromUploads, ...attachmentsFromLinkPreviews]; // Instead of checking if a user is still mentioned every time the text changes, // just filter out non-mentioned users before submit, which is cheaper @@ -145,16 +182,25 @@ export const useSubmitHandler = < mentioned_users: actualMentionedUsers, text, }; - + // scraped attachments are added only if all enrich queries has completed. Otherwise, the scraping has to be done server-side. + const linkPreviewsEnabled = !!findAndEnqueueURLsToEnrich; + const skip_enrich_url = + linkPreviewsEnabled && + ((!someLinkPreviewsLoading && attachmentsFromLinkPreviews.length > 0) || + someLinkPreviewsDismissed); + const sendOptions = linkPreviewsEnabled ? { skip_enrich_url } : undefined; if (message) { delete message.i18n; try { - await editMessage(({ - ...message, - ...updatedMessage, - ...customMessageData, - } as unknown) as UpdatedMessage); + await editMessage( + ({ + ...message, + ...updatedMessage, + ...customMessageData, + } as unknown) as UpdatedMessage, + sendOptions, + ); clearEditingState?.(); dispatch({ type: 'clear' }); @@ -173,6 +219,7 @@ export const useSubmitHandler = < }, channel.cid, customMessageData, + sendOptions, ); } else { await sendMessage( @@ -181,6 +228,7 @@ export const useSubmitHandler = < parent, }, customMessageData, + sendOptions, ); } diff --git a/src/components/MessageInput/icons.tsx b/src/components/MessageInput/icons.tsx index 54ac1b7c4..b3309de47 100644 --- a/src/components/MessageInput/icons.tsx +++ b/src/components/MessageInput/icons.tsx @@ -176,6 +176,17 @@ export const DownloadIcon = () => ( ); +export const LinkIcon = () => ( + + + +); + export const SendIconV1 = () => { const { t } = useTranslationContext('SendButton'); return ( diff --git a/src/components/MessageInput/index.ts b/src/components/MessageInput/index.ts index baf5ce71d..2a7cd048e 100644 --- a/src/components/MessageInput/index.ts +++ b/src/components/MessageInput/index.ts @@ -10,3 +10,4 @@ export * from './MessageInputFlat'; export * from './MessageInputSmall'; export * from './QuotedMessagePreview'; export * from './UploadsPreview'; +export * from './types'; diff --git a/src/components/MessageInput/types.ts b/src/components/MessageInput/types.ts new file mode 100644 index 000000000..7c43be1d3 --- /dev/null +++ b/src/components/MessageInput/types.ts @@ -0,0 +1,67 @@ +import type { Attachment, OGAttachment } from 'stream-chat'; +import type { DefaultStreamChatGenerics } from '../../types/types'; + +type AttachmentLoadingState = 'uploading' | 'finished' | 'failed'; + +export type FileUpload = { + file: { + name: string; + lastModified?: number; + lastModifiedDate?: Date; + size?: number; + type?: string; + uri?: string; + }; + id: string; + state: AttachmentLoadingState; + thumb_url?: string; + url?: string; +}; +export type ImageUpload< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +> = { + file: { + name: string; + height?: number; + lastModified?: number; + lastModifiedDate?: Date; + size?: number; + type?: string; + uri?: string; + width?: number; + }; + id: string; + state: AttachmentLoadingState; + previewUri?: string; + url?: string; +} & Pick< + Attachment, + 'og_scrape_url' | 'title' | 'title_link' | 'author_name' | 'text' +>; + +export enum LinkPreviewState { + /** Link preview has been dismissed using MessageInputContextValue.dismissLinkPreview **/ + DISMISSED = 'dismissed', + /** Link preview could not be loaded, the enrichment request has failed. **/ + FAILED = 'failed', + /** Link preview has been successfully loaded. **/ + LOADED = 'loaded', + /** The enrichment query is in progress for a given link. **/ + LOADING = 'loading', + /** The link is scheduled for enrichment. **/ + QUEUED = 'queued', +} + +export type LinkURL = string; + +export type LinkPreview = OGAttachment & { + state: LinkPreviewState; +}; + +export enum SetLinkPreviewMode { + UPSERT, + SET, + REMOVE, +} + +export type LinkPreviewMap = Map; diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx index d89053f48..1a4f56acb 100644 --- a/src/components/MessageList/VirtualizedMessageList.tsx +++ b/src/components/MessageList/VirtualizedMessageList.tsx @@ -376,6 +376,7 @@ const VirtualizedMessageListWithContext = < return ( = Partial, 'customMessageActions' | 'messageActions'>> & { +> = Partial, PropsDrilledToMessage>> & { /** Additional props to be passed the underlying [`react-virtuoso` virtualized list dependency](https://virtuoso.dev/virtuoso-api-reference/) */ additionalVirtuosoProps?: VirtuosoProps; /** If true, picking a reaction from the `ReactionSelector` component will close the selector */ diff --git a/src/context/ChannelActionContext.tsx b/src/context/ChannelActionContext.tsx index c8c48459e..15b721238 100644 --- a/src/context/ChannelActionContext.tsx +++ b/src/context/ChannelActionContext.tsx @@ -15,7 +15,12 @@ import type { StreamMessage } from './ChannelStateContext'; import type { ChannelStateReducerAction } from '../components/Channel/channelState'; import type { CustomMentionHandler } from '../components/Message/hooks/useMentionsHandler'; -import type { DefaultStreamChatGenerics, UnknownType } from '../types/types'; +import type { + DefaultStreamChatGenerics, + SendMessageOptions, + UnknownType, + UpdateMessageOptions, +} from '../types/types'; export type MessageAttachments< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics @@ -47,6 +52,7 @@ export type ChannelActionContextValue< dispatch: React.Dispatch>; editMessage: ( message: UpdatedMessage, + options?: UpdateMessageOptions, ) => Promise | void>; jumpToLatestMessage: () => Promise; jumpToMessage: (messageId: string, limit?: number) => Promise; @@ -64,6 +70,7 @@ export type ChannelActionContextValue< sendMessage: ( message: MessageToSend, customMessageData?: Partial>, + options?: SendMessageOptions, ) => Promise; setQuotedMessage: React.Dispatch< React.SetStateAction | undefined> diff --git a/src/context/ChannelStateContext.tsx b/src/context/ChannelStateContext.tsx index 6bc8dd035..b44f8bb52 100644 --- a/src/context/ChannelStateContext.tsx +++ b/src/context/ChannelStateContext.tsx @@ -15,6 +15,7 @@ import type { UnknownType, VideoAttachmentSizeHandler, } from '../types/types'; +import type { URLEnrichmentConfig } from '../components/MessageInput/hooks/useLinkPreviews'; export type ChannelNotifications = Array<{ id: string; @@ -66,10 +67,14 @@ export type ChannelStateContextValue< shouldGenerateVideoThumbnail: boolean; videoAttachmentSizeHandler: VideoAttachmentSizeHandler; acceptedFiles?: string[]; + debounceURLEnrichmentMs?: URLEnrichmentConfig['debounceURLEnrichmentMs']; dragAndDropWindow?: boolean; + enrichURLForPreview?: URLEnrichmentConfig['enrichURLForPreview']; + findURLFn?: URLEnrichmentConfig['findURLFn']; giphyVersion?: GiphyVersions; maxNumberOfFiles?: number; mutes?: Array>; + onLinkPreviewDismissed?: URLEnrichmentConfig['onLinkPreviewDismissed']; watcher_count?: number; }; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 740f7f1ef..9de91337b 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -33,6 +33,7 @@ import type { TypingIndicatorProps } from '../components/TypingIndicator/TypingI import type { CustomTrigger, DefaultStreamChatGenerics, UnknownType } from '../types/types'; import type { CooldownTimerProps } from '../components'; +import type { LinkPreviewListProps } from '../components/MessageInput/LinkPreviewList'; export type ComponentContextValue< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -40,6 +41,7 @@ export type ComponentContextValue< > = { Attachment: React.ComponentType>; Message: React.ComponentType>; + AttachmentPreviewList?: React.ComponentType; AutocompleteSuggestionHeader?: React.ComponentType; AutocompleteSuggestionItem?: React.ComponentType>; AutocompleteSuggestionList?: React.ComponentType>; @@ -53,6 +55,7 @@ export type ComponentContextValue< GiphyPreviewMessage?: React.ComponentType>; HeaderComponent?: React.ComponentType; Input?: React.ComponentType>; + LinkPreviewList?: React.ComponentType; LoadingIndicator?: React.ComponentType; MessageDeleted?: React.ComponentType>; MessageListNotifications?: React.ComponentType; diff --git a/src/types/types.ts b/src/types/types.ts index be7b38c1f..1cb81fb07 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -129,3 +129,19 @@ export type VideoAttachmentSizeHandler = ( element: HTMLElement, shouldGenerateVideoThumbnail: boolean, ) => VideoAttachmentConfiguration; + +// todo: fix export from stream-chat - for some reason not exported +export type SendMessageOptions = { + force_moderation?: boolean; + is_pending_message?: boolean; + keep_channel_hidden?: boolean; + pending?: boolean; + pending_message_metadata?: Record; + skip_enrich_url?: boolean; + skip_push?: boolean; +}; + +// todo: fix export from stream-chat - for some reason not exported +export type UpdateMessageOptions = { + skip_enrich_url?: boolean; +};