UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

158 lines (157 loc) 10.9 kB
import clsx from 'clsx'; import React from 'react'; import { useEnrichedMessages, useMessageListElements, useScrollLocationLogic, useUnreadMessagesNotification, } from './hooks/MessageList'; import { useMarkRead } from './hooks/useMarkRead'; import { MessageNotification as DefaultMessageNotification } from './MessageNotification'; import { MessageListNotifications as DefaultMessageListNotifications } from './MessageListNotifications'; import { UnreadMessagesNotification as DefaultUnreadMessagesNotification } from './UnreadMessagesNotification'; import { useChannelActionContext } from '../../context/ChannelActionContext'; import { useChannelStateContext } from '../../context/ChannelStateContext'; import { DialogManagerProvider } from '../../context'; import { useChatContext } from '../../context/ChatContext'; import { useComponentContext } from '../../context/ComponentContext'; import { MessageListContextProvider } from '../../context/MessageListContext'; import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator'; import { InfiniteScroll } from '../InfiniteScrollPaginator/InfiniteScroll'; import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading'; import { defaultPinPermissions, MESSAGE_ACTIONS } from '../Message/utils'; import { TypingIndicator as DefaultTypingIndicator } from '../TypingIndicator'; import { MessageListMainPanel as DefaultMessageListMainPanel } from './MessageListMainPanel'; import { defaultRenderMessages } from './renderMessages'; import { useStableId } from '../UtilityComponents/useStableId'; import { DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD, DEFAULT_NEXT_CHANNEL_PAGE_SIZE, } from '../../constants/limits'; const MessageListWithContext = (props) => { const { channel, channelUnreadUiState, disableDateSeparator = false, groupStyles, hasMoreNewer = false, headerPosition, hideDeletedMessages = false, hideNewMessageSeparator = false, highlightedMessageId, internalInfiniteScrollProps: { threshold: loadMoreScrollThreshold = DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD, ...restInternalInfiniteScrollProps } = {}, jumpToLatestMessage = () => Promise.resolve(), loadMore: loadMoreCallback, loadMoreNewer: loadMoreNewerCallback, // @deprecated in favor of `channelCapabilities` - TODO: remove in next major release maxTimeBetweenGroupedMessages, messageActions = Object.keys(MESSAGE_ACTIONS), messageLimit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE, messages = [], noGroupByUser = false, notifications, pinPermissions = defaultPinPermissions, reactionDetailsSort, read, renderMessages = defaultRenderMessages, returnAllReadData = false, reviewProcessedMessage, showUnreadNotificationAlways, sortReactionDetails, sortReactions, suppressAutoscroll, threadList = false, unsafeHTML = false, } = props; const [listElement, setListElement] = React.useState(null); const [ulElement, setUlElement] = React.useState(null); const { customClasses } = useChatContext('MessageList'); const { EmptyStateIndicator = DefaultEmptyStateIndicator, LoadingIndicator = DefaultLoadingIndicator, MessageListMainPanel = DefaultMessageListMainPanel, MessageListNotifications = DefaultMessageListNotifications, MessageNotification = DefaultMessageNotification, TypingIndicator = DefaultTypingIndicator, UnreadMessagesNotification = DefaultUnreadMessagesNotification, } = useComponentContext('MessageList'); const { hasNewMessages, isMessageListScrolledToBottom, onScroll, scrollToBottom, wrapperRect, } = useScrollLocationLogic({ hasMoreNewer, listElement, loadMoreScrollThreshold, messages, // todo: is it correct to base the scroll logic on an array that does not contain date separators or intro? scrolledUpThreshold: props.scrolledUpThreshold, suppressAutoscroll, }); const { show: showUnreadMessagesNotification } = useUnreadMessagesNotification({ isMessageListScrolledToBottom, showAlways: !!showUnreadNotificationAlways, unreadCount: channelUnreadUiState?.unread_messages, }); useMarkRead({ isMessageListScrolledToBottom, messageListIsThread: threadList, wasMarkedUnread: !!channelUnreadUiState?.first_unread_message_id, }); const { messageGroupStyles, messages: enrichedMessages } = useEnrichedMessages({ channel, disableDateSeparator, groupStyles, headerPosition, hideDeletedMessages, hideNewMessageSeparator, maxTimeBetweenGroupedMessages, messages, noGroupByUser, reviewProcessedMessage, }); const elements = useMessageListElements({ channelUnreadUiState, enrichedMessages, internalMessageProps: { additionalMessageInputProps: props.additionalMessageInputProps, closeReactionSelectorOnClick: props.closeReactionSelectorOnClick, customMessageActions: props.customMessageActions, disableQuotedMessages: props.disableQuotedMessages, formatDate: props.formatDate, getDeleteMessageErrorNotification: props.getDeleteMessageErrorNotification, getFlagMessageErrorNotification: props.getFlagMessageErrorNotification, getFlagMessageSuccessNotification: props.getFlagMessageSuccessNotification, getMarkMessageUnreadErrorNotification: props.getMarkMessageUnreadErrorNotification, getMarkMessageUnreadSuccessNotification: props.getMarkMessageUnreadSuccessNotification, getMuteUserErrorNotification: props.getMuteUserErrorNotification, getMuteUserSuccessNotification: props.getMuteUserSuccessNotification, getPinMessageErrorNotification: props.getPinMessageErrorNotification, Message: props.Message, messageActions, messageListRect: wrapperRect, onlySenderCanEdit: props.onlySenderCanEdit, onMentionsClick: props.onMentionsClick, onMentionsHover: props.onMentionsHover, onUserClick: props.onUserClick, onUserHover: props.onUserHover, openThread: props.openThread, pinPermissions, reactionDetailsSort, renderText: props.renderText, retrySendMessage: props.retrySendMessage, sortReactionDetails, sortReactions, unsafeHTML, }, messageGroupStyles, read, renderMessages, returnAllReadData, threadList, }); const messageListClass = customClasses?.messageList || 'str-chat__list'; const loadMore = React.useCallback(() => { if (loadMoreCallback) { loadMoreCallback(messageLimit); } }, [loadMoreCallback, messageLimit]); const loadMoreNewer = React.useCallback(() => { if (loadMoreNewerCallback) { loadMoreNewerCallback(messageLimit); } }, [loadMoreNewerCallback, messageLimit]); const scrollToBottomFromNotification = React.useCallback(async () => { if (hasMoreNewer) { await jumpToLatestMessage(); } else { scrollToBottom(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [scrollToBottom, hasMoreNewer]); React.useLayoutEffect(() => { if (highlightedMessageId) { const element = ulElement?.querySelector(`[data-message-id='${highlightedMessageId}']`); element?.scrollIntoView({ block: 'center' }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [highlightedMessageId]); const id = useStableId(); const showEmptyStateIndicator = elements.length === 0 && !threadList; const dialogManagerId = threadList ? `message-list-dialog-manager-thread-${id}` : `message-list-dialog-manager-${id}`; return (React.createElement(MessageListContextProvider, { value: { listElement, scrollToBottom } }, React.createElement(MessageListMainPanel, null, React.createElement(DialogManagerProvider, { id: dialogManagerId }, !threadList && showUnreadMessagesNotification && (React.createElement(UnreadMessagesNotification, { unreadCount: channelUnreadUiState?.unread_messages })), React.createElement("div", { className: clsx(messageListClass, customClasses?.threadList), onScroll: onScroll, ref: setListElement, tabIndex: 0 }, showEmptyStateIndicator ? (React.createElement(EmptyStateIndicator, { listType: threadList ? 'thread' : 'message' })) : (React.createElement(InfiniteScroll, { className: 'str-chat__message-list-scroll', "data-testid": 'reverse-infinite-scroll', hasNextPage: props.hasMoreNewer, hasPreviousPage: props.hasMore, head: props.head, isLoading: props.loadingMore, loader: React.createElement("div", { className: 'str-chat__list__loading', key: 'loading-indicator' }, props.loadingMore && React.createElement(LoadingIndicator, { size: 20 })), loadNextPage: loadMoreNewer, loadPreviousPage: loadMore, threshold: loadMoreScrollThreshold, ...restInternalInfiniteScrollProps }, React.createElement("ul", { className: 'str-chat__ul', ref: setUlElement }, elements), React.createElement(TypingIndicator, { threadList: threadList }), React.createElement("div", { key: 'bottom' })))))), React.createElement(MessageListNotifications, { hasNewMessages: hasNewMessages, isMessageListScrolledToBottom: isMessageListScrolledToBottom, isNotAtLatestMessageSet: hasMoreNewer, MessageNotification: MessageNotification, notifications: notifications, scrollToBottom: scrollToBottomFromNotification, threadList: threadList, unreadCount: threadList ? undefined : channelUnreadUiState?.unread_messages }))); }; /** * The MessageList component renders a list of Messages. * It is a consumer of 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 MessageList = (props) => { const { jumpToLatestMessage, loadMore, loadMoreNewer } = useChannelActionContext('MessageList'); const { members: membersPropToNotPass, // eslint-disable-line @typescript-eslint/no-unused-vars mutes: mutesPropToNotPass, // eslint-disable-line @typescript-eslint/no-unused-vars watchers: watchersPropToNotPass, // eslint-disable-line @typescript-eslint/no-unused-vars ...restChannelStateContext } = useChannelStateContext('MessageList'); return (React.createElement(MessageListWithContext, { jumpToLatestMessage: jumpToLatestMessage, loadMore: loadMore, loadMoreNewer: loadMoreNewer, ...restChannelStateContext, ...props })); };