UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

880 lines (879 loc) 40.9 kB
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from 'react'; import clsx from 'clsx'; import debounce from 'lodash.debounce'; import defaultsDeep from 'lodash.defaultsdeep'; import throttle from 'lodash.throttle'; import { localMessageToNewMessagePayload } from 'stream-chat'; import { initialState, makeChannelReducer } from './channelState'; import { useCreateChannelStateContext } from './hooks/useCreateChannelStateContext'; import { useCreateTypingContext } from './hooks/useCreateTypingContext'; import { useEditMessageHandler } from './hooks/useEditMessageHandler'; import { useIsMounted } from './hooks/useIsMounted'; import { useMentionsHandlers } from './hooks/useMentionsHandlers'; import { LoadingErrorIndicator as DefaultLoadingErrorIndicator } from '../Loading'; import { LoadingChannel as DefaultLoadingIndicator } from './LoadingChannel'; import { ChannelActionProvider, ChannelStateProvider, TypingProvider, useChatContext, useTranslationContext, WithComponents, } from '../../context'; import { CHANNEL_CONTAINER_ID } from './constants'; import { DEFAULT_HIGHLIGHT_DURATION, DEFAULT_INITIAL_CHANNEL_PAGE_SIZE, DEFAULT_JUMP_TO_PAGE_SIZE, DEFAULT_NEXT_CHANNEL_PAGE_SIZE, DEFAULT_THREAD_PAGE_SIZE, } from '../../constants/limits'; import { hasMoreMessagesProbably } from '../MessageList'; import { getChatContainerClass, useChannelContainerClasses, useImageFlagEmojisOnWindowsClass, } from './hooks/useChannelContainerClasses'; import { findInMsgSetByDate, findInMsgSetById, makeAddNotifications } from './utils'; import { useThreadContext } from '../Threads'; import { getChannel } from '../../utils'; import { getImageAttachmentConfiguration, getVideoAttachmentConfiguration, } from '../Attachment/attachment-sizing'; import { useSearchFocusedMessage } from '../../experimental/Search/hooks'; const ChannelContainer = ({ children, className: additionalClassName, ...props }) => { const { customClasses, theme } = useChatContext('Channel'); const { channelClass, chatClass } = useChannelContainerClasses({ customClasses, }); const className = clsx(chatClass, theme, channelClass, additionalClassName); return (React.createElement("div", { id: CHANNEL_CONTAINER_ID, ...props, className: className }, children)); }; const UnMemoizedChannel = (props) => { const { channel: propsChannel, EmptyPlaceholder = null, LoadingErrorIndicator, LoadingIndicator = DefaultLoadingIndicator, } = props; const { channel: contextChannel, channelsQueryState } = useChatContext('Channel'); const channel = propsChannel || contextChannel; if (channelsQueryState.queryInProgress === 'reload' && LoadingIndicator) { return (React.createElement(ChannelContainer, null, React.createElement(LoadingIndicator, null))); } if (channelsQueryState.error && LoadingErrorIndicator) { return (React.createElement(ChannelContainer, null, React.createElement(LoadingErrorIndicator, { error: channelsQueryState.error }))); } if (!channel?.cid) { return React.createElement(ChannelContainer, null, EmptyPlaceholder); } return React.createElement(ChannelInner, { ...props, channel: channel, key: channel.cid }); }; const ChannelInner = (props) => { const { activeUnreadHandler, channel, channelQueryOptions: propChannelQueryOptions, children, doDeleteMessageRequest, doMarkReadRequest, doSendMessageRequest, doUpdateMessageRequest, initializeOnMount = true, LoadingErrorIndicator = DefaultLoadingErrorIndicator, LoadingIndicator = DefaultLoadingIndicator, markReadOnMount = true, onMentionsClick, onMentionsHover, skipMessageDataMemoization, } = props; const channelQueryOptions = useMemo(() => defaultsDeep(propChannelQueryOptions, { messages: { limit: DEFAULT_INITIAL_CHANNEL_PAGE_SIZE }, }), [propChannelQueryOptions]); const { client, customClasses, latestMessageDatesByChannels, mutes, searchController } = useChatContext('Channel'); const { t } = useTranslationContext('Channel'); const chatContainerClass = getChatContainerClass(customClasses?.chatContainer); const windowsEmojiClass = useImageFlagEmojisOnWindowsClass(); const thread = useThreadContext(); const [channelConfig, setChannelConfig] = useState(channel.getConfig()); const [notifications, setNotifications] = useState([]); const notificationTimeouts = useRef([]); const [channelUnreadUiState, _setChannelUnreadUiState] = useState(); const channelReducer = useMemo(() => makeChannelReducer(), []); const [state, dispatch] = useReducer(channelReducer, // channel.initialized === false if client.channel().query() was not called, e.g. ChannelList is not used // => Channel will call channel.watch() in useLayoutEffect => state.loading is used to signal the watch() call state { ...initialState, hasMore: channel.state.messagePagination.hasPrev, loading: !channel.initialized, messages: channel.state.messages, }); const jumpToMessageFromSearch = useSearchFocusedMessage(); const isMounted = useIsMounted(); const originalTitle = useRef(''); const lastRead = useRef(undefined); const online = useRef(true); const clearHighlightedMessageTimeoutId = useRef(null); const channelCapabilitiesArray = channel.data?.own_capabilities; const throttledCopyStateFromChannel = throttle(() => dispatch({ channel, type: 'copyStateFromChannelOnEvent' }), 500, { leading: true, trailing: true, }); const setChannelUnreadUiState = useMemo(() => throttle(_setChannelUnreadUiState, 200, { leading: true, trailing: false, }), []); const markRead = useMemo(() => throttle(async (options) => { const { updateChannelUiUnreadState = true } = options ?? {}; if (channel.disconnected || !channelConfig?.read_events) { return; } lastRead.current = new Date(); try { if (doMarkReadRequest) { doMarkReadRequest(channel, updateChannelUiUnreadState ? setChannelUnreadUiState : undefined); } else { const markReadResponse = await channel.markRead(); // markReadResponse.event can be null in case of a user that is not a member of a channel being marked read // in that case event is null and we should not set unread UI if (updateChannelUiUnreadState && markReadResponse?.event) { _setChannelUnreadUiState({ last_read: lastRead.current, last_read_message_id: markReadResponse.event.last_read_message_id, unread_messages: 0, }); } } if (activeUnreadHandler) { activeUnreadHandler(0, originalTitle.current); } else if (originalTitle.current) { document.title = originalTitle.current; } } catch (e) { console.error(t('Failed to mark channel as read')); } }, 500, { leading: true, trailing: false }), [ activeUnreadHandler, channel, channelConfig, doMarkReadRequest, setChannelUnreadUiState, t, ]); const handleEvent = async (event) => { if (event.message) { dispatch({ channel, message: event.message, type: 'updateThreadOnEvent', }); } // ignore the event if it is not targeted at the current channel. // Event targeted at this channel or globally targeted event should lead to state refresh if (event.type === 'user.messages.deleted' && event.cid && event.cid !== channel.cid) return; if (event.type === 'user.watching.start' || event.type === 'user.watching.stop') return; if (event.type === 'typing.start' || event.type === 'typing.stop') { return dispatch({ channel, type: 'setTyping' }); } if (event.type === 'connection.changed' && typeof event.online === 'boolean') { online.current = event.online; } if (event.type === 'message.new') { const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; if (mainChannelUpdated) { if (document.hidden && channelConfig?.read_events && !channel.muteStatus().muted) { const unread = channel.countUnread(lastRead.current); if (activeUnreadHandler) { activeUnreadHandler(unread, originalTitle.current); } else { document.title = `(${unread}) ${originalTitle.current}`; } } } if (event.message?.user?.id === client.userID && event?.message?.created_at && event?.message?.cid) { const messageDate = new Date(event.message.created_at); const cid = event.message.cid; if (!latestMessageDatesByChannels[cid] || latestMessageDatesByChannels[cid].getTime() < messageDate.getTime()) { latestMessageDatesByChannels[cid] = messageDate; } } } if (event.type === 'user.deleted') { const oldestID = channel.state?.messages?.[0]?.id; /** * As the channel state is not normalized we re-fetch the channel data. Thus, we avoid having to search for user references in the channel state. */ // FIXME: we should use channelQueryOptions if they are available await channel.query({ messages: { id_lt: oldestID, limit: DEFAULT_NEXT_CHANNEL_PAGE_SIZE }, watchers: { limit: DEFAULT_NEXT_CHANNEL_PAGE_SIZE }, }); } if (event.type === 'notification.mark_unread') _setChannelUnreadUiState((prev) => { if (!(event.last_read_at && event.user)) return prev; return { first_unread_message_id: event.first_unread_message_id, last_read: new Date(event.last_read_at), last_read_message_id: event.last_read_message_id, unread_messages: event.unread_messages ?? 0, }; }); if (event.type === 'channel.truncated' && event.cid === channel.cid) { _setChannelUnreadUiState(undefined); } throttledCopyStateFromChannel(); }; // useLayoutEffect here to prevent spinner. Use Suspense when it is available in stable release useLayoutEffect(() => { let errored = false; let done = false; (async () => { if (!channel.initialized && initializeOnMount) { try { // if active channel has been set without id, we will create a temporary channel id from its member IDs // to keep track of the /query request in progress. This is the same approach of generating temporary id // that the JS client uses to keep track of channel in client.activeChannels const members = []; if (!channel.id && channel.data?.members) { for (const member of channel.data.members) { let userId; if (typeof member === 'string') { userId = member; } else if (typeof member === 'object') { const { user, user_id } = member; userId = user_id || user?.id; } if (userId) { members.push(userId); } } } await getChannel({ channel, client, members, options: channelQueryOptions }); const config = channel.getConfig(); setChannelConfig(config); } catch (e) { dispatch({ error: e, type: 'setError' }); errored = true; } } done = true; originalTitle.current = document.title; if (!errored) { dispatch({ channel, hasMore: channel.state.messagePagination.hasPrev, type: 'initStateFromChannel', }); if (client.user?.id && channel.state.read[client.user.id]) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { user, ...ownReadState } = channel.state.read[client.user.id]; _setChannelUnreadUiState(ownReadState); } /** * TODO: maybe pass last_read to the countUnread method to get proper value * combined with channel.countUnread adjustment (_countMessageAsUnread) * to allow counting own messages too * * const lastRead = channel.state.read[client.userID as string].last_read; */ if (channel.countUnread() > 0 && markReadOnMount) markRead({ updateChannelUiUnreadState: false }); // The more complex sync logic is done in Chat client.on('connection.changed', handleEvent); client.on('connection.recovered', handleEvent); client.on('user.updated', handleEvent); client.on('user.deleted', handleEvent); client.on('user.messages.deleted', handleEvent); channel.on(handleEvent); } })(); const notificationTimeoutsRef = notificationTimeouts.current; return () => { if (errored || !done) return; channel?.off(handleEvent); client.off('connection.changed', handleEvent); client.off('connection.recovered', handleEvent); client.off('user.deleted', handleEvent); notificationTimeoutsRef.forEach(clearTimeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ channel.cid, channelQueryOptions, doMarkReadRequest, channelConfig?.read_events, initializeOnMount, ]); useEffect(() => { if (!state.thread) return; const message = state.messages?.find((m) => m.id === state.thread?.id); if (message) dispatch({ message, type: 'setThread' }); }, [state.messages, state.thread]); const handleHighlightedMessageChange = useCallback(({ highlightDuration, highlightedMessageId, }) => { dispatch({ channel, highlightedMessageId, type: 'jumpToMessageFinished', }); if (clearHighlightedMessageTimeoutId.current) { clearTimeout(clearHighlightedMessageTimeoutId.current); } clearHighlightedMessageTimeoutId.current = setTimeout(() => { if (searchController._internalState.getLatestValue().focusedMessage) { searchController._internalState.partialNext({ focusedMessage: undefined }); } clearHighlightedMessageTimeoutId.current = null; dispatch({ type: 'clearHighlightedMessage' }); }, highlightDuration ?? DEFAULT_HIGHLIGHT_DURATION); }, [channel, searchController]); useEffect(() => { if (!jumpToMessageFromSearch?.id) return; handleHighlightedMessageChange({ highlightedMessageId: jumpToMessageFromSearch.id }); }, [jumpToMessageFromSearch, handleHighlightedMessageChange]); /** MESSAGE */ // Adds a temporary notification to message list, will be removed after 5 seconds const addNotification = useMemo(() => makeAddNotifications(setNotifications, notificationTimeouts.current), []); // eslint-disable-next-line react-hooks/exhaustive-deps const loadMoreFinished = useCallback(debounce((hasMore, messages) => { if (!isMounted.current) return; dispatch({ hasMore, messages, type: 'loadMoreFinished' }); }, 2000, { leading: true, trailing: true }), []); const loadMore = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { if (!online.current || !window.navigator.onLine || !channel.state.messagePagination.hasPrev) return 0; // prevent duplicate loading events... const oldestMessage = state?.messages?.[0]; if (state.loadingMore || state.loadingMoreNewer || oldestMessage?.status !== 'received') { return 0; } dispatch({ loadingMore: true, type: 'setLoadingMore' }); const oldestID = oldestMessage?.id; const perPage = limit; let queryResponse; try { queryResponse = await channel.query({ messages: { id_lt: oldestID, limit: perPage }, watchers: { limit: perPage }, }); } catch (e) { console.warn('message pagination request failed with error', e); dispatch({ loadingMore: false, type: 'setLoadingMore' }); return 0; } loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); return queryResponse.messages.length; }; const loadMoreNewer = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { if (!online.current || !window.navigator.onLine || !channel.state.messagePagination.hasNext) return 0; const newestMessage = state?.messages?.[state?.messages?.length - 1]; if (state.loadingMore || state.loadingMoreNewer) return 0; dispatch({ loadingMoreNewer: true, type: 'setLoadingMoreNewer' }); const newestId = newestMessage?.id; const perPage = limit; let queryResponse; try { queryResponse = await channel.query({ messages: { id_gt: newestId, limit: perPage }, watchers: { limit: perPage }, }); } catch (e) { console.warn('message pagination request failed with error', e); dispatch({ loadingMoreNewer: false, type: 'setLoadingMoreNewer' }); return 0; } dispatch({ hasMoreNewer: channel.state.messagePagination.hasNext, messages: channel.state.messages, type: 'loadMoreNewerFinished', }); return queryResponse.messages.length; }; const jumpToMessage = useCallback(async (messageId, messageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, highlightDuration = DEFAULT_HIGHLIGHT_DURATION) => { dispatch({ loadingMore: true, type: 'setLoadingMore' }); await channel.state.loadMessageIntoState(messageId, undefined, messageLimit); loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); handleHighlightedMessageChange({ highlightDuration, highlightedMessageId: messageId, }); }, [channel, handleHighlightedMessageChange, loadMoreFinished]); const jumpToLatestMessage = useCallback(async () => { await channel.state.loadMessageIntoState('latest'); loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); dispatch({ type: 'jumpToLatestMessage', }); }, [channel, loadMoreFinished]); const jumpToFirstUnreadMessage = useCallback(async (queryMessageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, highlightDuration = DEFAULT_HIGHLIGHT_DURATION) => { if (!channelUnreadUiState?.unread_messages) return; let lastReadMessageId = channelUnreadUiState?.last_read_message_id; let firstUnreadMessageId = channelUnreadUiState?.first_unread_message_id; let isInCurrentMessageSet = false; if (firstUnreadMessageId) { const result = findInMsgSetById(firstUnreadMessageId, channel.state.messages); isInCurrentMessageSet = result.index !== -1; } else if (lastReadMessageId) { const result = findInMsgSetById(lastReadMessageId, channel.state.messages); isInCurrentMessageSet = !!result.target; firstUnreadMessageId = result.index > -1 ? channel.state.messages[result.index + 1]?.id : undefined; } else { const lastReadTimestamp = channelUnreadUiState.last_read.getTime(); const { index: lastReadMessageIndex, target: lastReadMessage } = findInMsgSetByDate(channelUnreadUiState.last_read, channel.state.messages, true); if (lastReadMessage) { firstUnreadMessageId = channel.state.messages[lastReadMessageIndex + 1]?.id; isInCurrentMessageSet = !!firstUnreadMessageId; lastReadMessageId = lastReadMessage.id; } else { dispatch({ loadingMore: true, type: 'setLoadingMore' }); let messages; try { messages = (await channel.query({ messages: { created_at_around: channelUnreadUiState.last_read.toISOString(), limit: queryMessageLimit, }, }, 'new')).messages; } catch (e) { addNotification(t('Failed to jump to the first unread message'), 'error'); loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); return; } const firstMessageWithCreationDate = messages.find((msg) => msg.created_at); if (!firstMessageWithCreationDate) { addNotification(t('Failed to jump to the first unread message'), 'error'); loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); return; } const firstMessageTimestamp = new Date(firstMessageWithCreationDate.created_at).getTime(); if (lastReadTimestamp < firstMessageTimestamp) { // whole channel is unread firstUnreadMessageId = firstMessageWithCreationDate.id; } else { const result = findInMsgSetByDate(channelUnreadUiState.last_read, messages); lastReadMessageId = result.target?.id; } loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); } } if (!firstUnreadMessageId && !lastReadMessageId) { addNotification(t('Failed to jump to the first unread message'), 'error'); return; } if (!isInCurrentMessageSet) { dispatch({ loadingMore: true, type: 'setLoadingMore' }); try { const targetId = (firstUnreadMessageId ?? lastReadMessageId); await channel.state.loadMessageIntoState(targetId, undefined, queryMessageLimit); /** * if the index of the last read message on the page is beyond the half of the page, * we have arrived to the oldest page of the channel */ const indexOfTarget = channel.state.messages.findIndex((message) => message.id === targetId); loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); firstUnreadMessageId = firstUnreadMessageId ?? channel.state.messages[indexOfTarget + 1]?.id; } catch (e) { addNotification(t('Failed to jump to the first unread message'), 'error'); loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); return; } } if (!firstUnreadMessageId) { addNotification(t('Failed to jump to the first unread message'), 'error'); return; } if (!channelUnreadUiState.first_unread_message_id) _setChannelUnreadUiState({ ...channelUnreadUiState, first_unread_message_id: firstUnreadMessageId, last_read_message_id: lastReadMessageId, }); handleHighlightedMessageChange({ highlightDuration, highlightedMessageId: firstUnreadMessageId, }); }, [ addNotification, channel, handleHighlightedMessageChange, loadMoreFinished, t, channelUnreadUiState, ]); const deleteMessage = useCallback(async (message) => { if (!message?.id) { throw new Error('Cannot delete a message - missing message ID.'); } let deletedMessage; if (doDeleteMessageRequest) { deletedMessage = await doDeleteMessageRequest(message); } else { const result = await client.deleteMessage(message.id); deletedMessage = result.message; } return deletedMessage; }, [client, doDeleteMessageRequest]); const updateMessage = (updatedMessage) => { // add the message to the local channel state channel.state.addMessageSorted(updatedMessage, true); dispatch({ channel, parentId: state.thread && updatedMessage.parent_id, type: 'copyMessagesFromChannel', }); }; const doSendMessage = async ({ localMessage, message, options, }) => { try { let messageResponse; if (doSendMessageRequest) { messageResponse = await doSendMessageRequest(channel, message, options); } else { messageResponse = await channel.sendMessage(message, options); } let existingMessage = undefined; for (let i = channel.state.messages.length - 1; i >= 0; i--) { const msg = channel.state.messages[i]; if (msg.id && msg.id === message.id) { existingMessage = msg; break; } } const responseTimestamp = new Date(messageResponse?.message?.updated_at || 0).getTime(); const existingMessageTimestamp = existingMessage?.updated_at?.getTime() || 0; const responseIsTheNewest = responseTimestamp > existingMessageTimestamp; // Replace the message payload after send is completed // We need to check for the newest message payload, because on slow network, the response can arrive later than WS events message.new, message.updated. // Always override existing message in status "sending" if (messageResponse?.message && (responseIsTheNewest || existingMessage?.status === 'sending')) { updateMessage({ ...messageResponse.message, status: 'received', }); } } catch (error) { // error response isn't usable so needs to be stringified then parsed const stringError = JSON.stringify(error); const parsedError = (stringError ? JSON.parse(stringError) : {}); // Handle the case where the message already exists // (typically, when retrying to send a message). // If the message already exists, we can assume it was sent successfully, // so we update the message status to "received". // Right now, the only way to check this error is by checking // the combination of the error code and the error description, // since there is no special error code for duplicate messages. if (parsedError.code === 4 && error instanceof Error && error.message.includes('already exists')) { updateMessage({ ...localMessage, status: 'received', }); } else { updateMessage({ ...localMessage, error: parsedError, status: 'failed', }); thread?.upsertReplyLocally({ message: { ...localMessage, error: parsedError, status: 'failed', }, }); } } }; const sendMessage = async ({ localMessage, message, options, }) => { channel.state.filterErrorMessages(); thread?.upsertReplyLocally({ message: localMessage, }); updateMessage(localMessage); await doSendMessage({ localMessage, message, options }); }; const retrySendMessage = async (localMessage) => { updateMessage({ ...localMessage, error: undefined, status: 'sending', }); await doSendMessage({ localMessage, message: localMessageToNewMessagePayload(localMessage), }); }; const removeMessage = (message) => { channel.state.removeMessage(message); dispatch({ channel, parentId: state.thread && message.parent_id, type: 'copyMessagesFromChannel', }); }; /** THREAD */ const openThread = (message, event) => { event?.preventDefault(); dispatch({ channel, message, type: 'openThread' }); }; const closeThread = (event) => { event?.preventDefault(); dispatch({ type: 'closeThread' }); }; // eslint-disable-next-line react-hooks/exhaustive-deps const loadMoreThreadFinished = useCallback(debounce((threadHasMore, threadMessages) => { dispatch({ threadHasMore, threadMessages, type: 'loadMoreThreadFinished', }); }, 2000, { leading: true, trailing: true }), []); const loadMoreThread = async (limit = DEFAULT_THREAD_PAGE_SIZE) => { // FIXME: should prevent loading more, if state.thread.reply_count === channel.state.threads[parentID].length if (state.threadLoadingMore || !state.thread || !state.threadHasMore) return; dispatch({ type: 'startLoadingThread' }); const parentId = state.thread.id; if (!parentId) { return dispatch({ type: 'closeThread' }); } const oldMessages = channel.state.threads[parentId] || []; const oldestMessageId = oldMessages[0]?.id; try { const queryResponse = await channel.getReplies(parentId, { id_lt: oldestMessageId, limit, }); const threadHasMoreMessages = hasMoreMessagesProbably(queryResponse.messages.length, limit); const newThreadMessages = channel.state.threads[parentId] || []; // next set loadingMore to false so we can start asking for more data loadMoreThreadFinished(threadHasMoreMessages, newThreadMessages); } catch (e) { loadMoreThreadFinished(false, oldMessages); } }; const onMentionsHoverOrClick = useMentionsHandlers(onMentionsHover, onMentionsClick); const editMessage = useEditMessageHandler(doUpdateMessageRequest); const { typing, ...restState } = state; const channelStateContextValue = useCreateChannelStateContext({ ...restState, channel, channelCapabilitiesArray, channelConfig, channelUnreadUiState, giphyVersion: props.giphyVersion || 'fixed_height', imageAttachmentSizeHandler: props.imageAttachmentSizeHandler || getImageAttachmentConfiguration, mutes, notifications, shouldGenerateVideoThumbnail: props.shouldGenerateVideoThumbnail || true, videoAttachmentSizeHandler: props.videoAttachmentSizeHandler || getVideoAttachmentConfiguration, watcher_count: state.watcherCount, }); const channelActionContextValue = useMemo(() => ({ addNotification, closeThread, deleteMessage, dispatch, editMessage, jumpToFirstUnreadMessage, jumpToLatestMessage, jumpToMessage, loadMore, loadMoreNewer, loadMoreThread, markRead, onMentionsClick: onMentionsHoverOrClick, onMentionsHover: onMentionsHoverOrClick, openThread, removeMessage, retrySendMessage, sendMessage, setChannelUnreadUiState, skipMessageDataMemoization, updateMessage, }), // eslint-disable-next-line react-hooks/exhaustive-deps [ channel.cid, deleteMessage, loadMore, loadMoreNewer, markRead, jumpToFirstUnreadMessage, jumpToMessage, jumpToLatestMessage, setChannelUnreadUiState, ]); const componentContextValue = useMemo(() => ({ Attachment: props.Attachment, AttachmentPreviewList: props.AttachmentPreviewList, AttachmentSelector: props.AttachmentSelector, AttachmentSelectorInitiationButtonContents: props.AttachmentSelectorInitiationButtonContents, AudioRecorder: props.AudioRecorder, AutocompleteSuggestionItem: props.AutocompleteSuggestionItem, AutocompleteSuggestionList: props.AutocompleteSuggestionList, Avatar: props.Avatar, BaseImage: props.BaseImage, CooldownTimer: props.CooldownTimer, CustomMessageActionsList: props.CustomMessageActionsList, DateSeparator: props.DateSeparator, EditMessageInput: props.EditMessageInput, EmojiPicker: props.EmojiPicker, emojiSearchIndex: props.emojiSearchIndex, EmptyStateIndicator: props.EmptyStateIndicator, FileUploadIcon: props.FileUploadIcon, GiphyPreviewMessage: props.GiphyPreviewMessage, HeaderComponent: props.HeaderComponent, Input: props.Input, LinkPreviewList: props.LinkPreviewList, LoadingIndicator: props.LoadingIndicator, Message: props.Message, MessageActions: props.MessageActions, MessageBlocked: props.MessageBlocked, MessageBouncePrompt: props.MessageBouncePrompt, MessageDeleted: props.MessageDeleted, MessageIsThreadReplyInChannelButtonIndicator: props.MessageIsThreadReplyInChannelButtonIndicator, MessageListNotifications: props.MessageListNotifications, MessageNotification: props.MessageNotification, MessageOptions: props.MessageOptions, MessageRepliesCountButton: props.MessageRepliesCountButton, MessageStatus: props.MessageStatus, MessageSystem: props.MessageSystem, MessageTimestamp: props.MessageTimestamp, Modal: props.Modal, ModalGallery: props.ModalGallery, PinIndicator: props.PinIndicator, PollActions: props.PollActions, PollContent: props.PollContent, PollCreationDialog: props.PollCreationDialog, PollHeader: props.PollHeader, PollOptionSelector: props.PollOptionSelector, QuotedMessage: props.QuotedMessage, QuotedMessagePreview: props.QuotedMessagePreview, QuotedPoll: props.QuotedPoll, reactionOptions: props.reactionOptions, ReactionSelector: props.ReactionSelector, ReactionsList: props.ReactionsList, ReactionsListModal: props.ReactionsListModal, ReminderNotification: props.ReminderNotification, SendButton: props.SendButton, SendToChannelCheckbox: props.SendToChannelCheckbox, ShareLocationDialog: props.ShareLocationDialog, StartRecordingAudioButton: props.StartRecordingAudioButton, StopAIGenerationButton: props.StopAIGenerationButton, StreamedMessageText: props.StreamedMessageText, TextareaComposer: props.TextareaComposer, ThreadHead: props.ThreadHead, ThreadHeader: props.ThreadHeader, ThreadStart: props.ThreadStart, Timestamp: props.Timestamp, TypingIndicator: props.TypingIndicator, UnreadMessagesNotification: props.UnreadMessagesNotification, UnreadMessagesSeparator: props.UnreadMessagesSeparator, VirtualMessage: props.VirtualMessage, }), [ props.Attachment, props.AttachmentPreviewList, props.AttachmentSelector, props.AttachmentSelectorInitiationButtonContents, props.AudioRecorder, props.AutocompleteSuggestionItem, props.AutocompleteSuggestionList, props.Avatar, props.BaseImage, props.CooldownTimer, props.CustomMessageActionsList, props.DateSeparator, props.EditMessageInput, props.EmojiPicker, props.emojiSearchIndex, props.EmptyStateIndicator, props.FileUploadIcon, props.GiphyPreviewMessage, props.HeaderComponent, props.Input, props.LinkPreviewList, props.LoadingIndicator, props.Message, props.MessageActions, props.MessageBlocked, props.MessageBouncePrompt, props.MessageDeleted, props.MessageIsThreadReplyInChannelButtonIndicator, props.MessageListNotifications, props.MessageNotification, props.MessageOptions, props.MessageRepliesCountButton, props.MessageStatus, props.MessageSystem, props.MessageTimestamp, props.Modal, props.ModalGallery, props.PinIndicator, props.PollActions, props.PollContent, props.PollCreationDialog, props.PollHeader, props.PollOptionSelector, props.QuotedMessage, props.QuotedMessagePreview, props.QuotedPoll, props.reactionOptions, props.ReactionSelector, props.ReactionsList, props.ReactionsListModal, props.ReminderNotification, props.SendButton, props.SendToChannelCheckbox, props.ShareLocationDialog, props.StartRecordingAudioButton, props.StopAIGenerationButton, props.StreamedMessageText, props.TextareaComposer, props.ThreadHead, props.ThreadHeader, props.ThreadStart, props.Timestamp, props.TypingIndicator, props.UnreadMessagesNotification, props.UnreadMessagesSeparator, props.VirtualMessage, ]); const typingContextValue = useCreateTypingContext({ typing, }); if (state.error) { return (React.createElement(ChannelContainer, null, React.createElement(LoadingErrorIndicator, { error: state.error }))); } if (state.loading) { return (React.createElement(ChannelContainer, null, React.createElement(LoadingIndicator, null))); } if (!channel.watch) { return (React.createElement(ChannelContainer, null, React.createElement("div", null, t('Channel Missing')))); } return (React.createElement(ChannelContainer, { className: windowsEmojiClass }, React.createElement(ChannelStateProvider, { value: channelStateContextValue }, React.createElement(ChannelActionProvider, { value: channelActionContextValue }, React.createElement(WithComponents, { overrides: componentContextValue }, React.createElement(TypingProvider, { value: typingContextValue }, React.createElement("div", { className: clsx(chatContainerClass) }, children))))))); }; /** * A wrapper component that provides channel data and renders children. * The Channel component provides the following contexts: * - [ChannelStateContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_state_context/) * - [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) * - [ComponentContext](https://getstream.io/chat/docs/sdk/react/contexts/component_context/) * - [TypingContext](https://getstream.io/chat/docs/sdk/react/contexts/typing_context/) */ export const Channel = React.memo(UnMemoizedChannel);