stream-chat-react
Version:
React components to create chat conversations or livestream style chat
158 lines (157 loc) • 10.9 kB
JavaScript
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 }));
};