diff --git a/packages/react/package.json b/packages/react/package.json index 82cc80268..6f85d0a38 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -99,6 +99,7 @@ "json5": "^2.2.3", "normalize.css": "^8.0.1", "prop-types": "^15.8.1", + "react-virtuoso": "^4.18.1", "swiper": "^11.1.0", "zustand": "^4.3.8" } diff --git a/packages/react/src/store/messageStore.js b/packages/react/src/store/messageStore.js index 4f84f8c1f..3ffa04150 100644 --- a/packages/react/src/store/messageStore.js +++ b/packages/react/src/store/messageStore.js @@ -29,8 +29,16 @@ const useMessageStore = create((set, get) => ({ const uniqueMessages = Array.from( new Map(allMessages.map((msg) => [msg._id, msg])).values() ); + + uniqueMessages.sort((a, b) => new Date(a.ts) - new Date(b.ts)); + + const cappedMessages = + uniqueMessages.length > 500 + ? uniqueMessages.slice(-500) + : uniqueMessages; + return { - messages: uniqueMessages, + messages: cappedMessages, isMessageLoaded: true, }; }), @@ -42,9 +50,12 @@ const useMessageStore = create((set, get) => ({ })); } } else { - set((state) => ({ - messages: upsertMessage(state.messages, message), - })); + set((state) => { + const updated = upsertMessage(state.messages, message); + return { + messages: updated.length > 500 ? updated.slice(-500) : updated, + }; + }); } }, removeMessage: (messageId) => { diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index 34f5c8bf4..ec3d8fac1 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -45,9 +45,8 @@ const ChatBody = ({ const { classNames, styleOverrides } = useComponentOverrides('ChatBody'); const { theme, mode } = useTheme(); const styles = getChatbodyStyles(theme, mode); - const [scrollPosition, setScrollPosition] = useState(0); const [popupVisible, setPopupVisible] = useState(false); - const [, setIsUserScrolledUp] = useState(false); + const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); const [otherUserMessage, setOtherUserMessage] = useState(false); const [isOverflowing, setIsOverflowing] = useState(false); const [firstUnreadMessageId, setFirstUnreadMessageId] = useState(null); @@ -206,89 +205,67 @@ const ChatBody = ({ setPopupVisible(false); }; - const handleScroll = useCallback(async () => { - if (messageListRef && messageListRef.current) { - setScrollPosition(messageListRef.current.scrollTop); - setIsUserScrolledUp( - messageListRef.current.scrollTop + messageListRef.current.clientHeight < - messageListRef.current.scrollHeight - ); - - if ( - messageListRef.current.scrollTop === 0 && - !loadingOlderMessages && - hasMoreMessages - ) { - setLoadingOlderMessages(true); - - try { - const olderMessages = await RCInstance.getOlderMessages( - anonymousMode, - ECOptions?.enableThreads - ? { - query: { - tmid: { - $exists: false, - }, + const loadMoreMessages = useCallback(async () => { + if (!loadingOlderMessages && hasMoreMessages) { + setLoadingOlderMessages(true); + try { + const olderMessages = await RCInstance.getOlderMessages( + anonymousMode, + ECOptions?.enableThreads + ? { + query: { + tmid: { + $exists: false, }, - offset, - } - : undefined, - anonymousMode ? false : isChannelPrivate - ); - const messageList = messageListRef.current; - if (olderMessages?.messages?.length) { - const previousScrollHeight = messageList.scrollHeight; - - setMessages(olderMessages.messages, true); - setMessagesOffset(offset + olderMessages.messages.length); - - requestAnimationFrame(() => { - const newScrollHeight = messageList.scrollHeight; - messageList.scrollTop = newScrollHeight - previousScrollHeight; - }); - } else { - setHasMoreMessages(false); - } - } catch (error) { - console.error('Error fetching older messages:', error); + }, + offset, + } + : undefined, + anonymousMode ? false : isChannelPrivate + ); + if (olderMessages?.messages?.length) { + setMessages(olderMessages.messages, true); + setMessagesOffset(offset + olderMessages.messages.length); + } else { setHasMoreMessages(false); - } finally { - setLoadingOlderMessages(false); } + } catch (error) { + console.error('Error fetching older messages:', error); + setHasMoreMessages(false); + } finally { + setLoadingOlderMessages(false); } } - - const isAtBottom = messageListRef?.current?.scrollTop === 0; - if (isAtBottom) { - setPopupVisible(false); - setIsUserScrolledUp(false); - setOtherUserMessage(false); - // Clear unread divider when scrolled to bottom - if (firstUnreadMessageId) { - setFirstUnreadMessageId(null); - } - // Also clear pending unread ref - pendingFirstUnreadRef.current = null; - } }, [ - messageListRef, - offset, - setMessagesOffset, - setMessages, - anonymousMode, + loadingOlderMessages, hasMoreMessages, RCInstance, - isChannelPrivate, + anonymousMode, ECOptions?.enableThreads, - loadingOlderMessages, - setScrollPosition, - setIsUserScrolledUp, - setPopupVisible, - setOtherUserMessage, - firstUnreadMessageId, + offset, + isChannelPrivate, + setMessages, + setMessagesOffset, ]); + const handleAtBottom = useCallback( + (atBottom) => { + setIsUserScrolledUp(!atBottom); + if (atBottom) { + setPopupVisible(false); + setOtherUserMessage(false); + if (firstUnreadMessageId) setFirstUnreadMessageId(null); + pendingFirstUnreadRef.current = null; + } + }, + [ + setPopupVisible, + setOtherUserMessage, + firstUnreadMessageId, + setFirstUnreadMessageId, + ] + ); + const showNewMessagesPopup = () => { setPopupVisible(true); }; @@ -307,33 +284,15 @@ const ChatBody = ({ } }; - useEffect(() => { - if (messageListRef.current) { - messageListRef.current.scrollTop = messageListRef.current.scrollHeight; - } - }, [messages]); - useEffect(() => { checkOverflow(); }, [channelInfo.announcement, showAnnouncement]); - useEffect(() => { - const currentRef = messageListRef.current; - currentRef.addEventListener('scroll', handleScroll); - - return () => { - currentRef.removeEventListener('scroll', handleScroll); - }; - }, [handleScroll, messageListRef]); useEffect(() => { - const isScrolledUp = - scrollPosition + messageListRef.current.clientHeight < - messageListRef.current.scrollHeight; - - if (isScrolledUp && otherUserMessage) { + if (isUserScrolledUp && otherUserMessage) { showNewMessagesPopup(); } - }, [scrollPosition, otherUserMessage, messageListRef]); + }, [isUserScrolledUp, otherUserMessage]); return ( <> @@ -398,6 +357,7 @@ const ChatBody = ({ css={styles.chatbodyContainer} style={{ ...styleOverrides, + overflow: 'hidden', }} className={`ec-chat-body ${classNames}`} > @@ -422,6 +382,11 @@ const ChatBody = ({ isUserAuthenticated={isUserAuthenticated} hasMoreMessages={hasMoreMessages} firstUnreadMessageId={firstUnreadMessageId} + loadMoreMessages={loadMoreMessages} + scrollerRef={(ref) => { + if (messageListRef) messageListRef.current = ref; + }} + onAtBottomStateChange={handleAtBottom} /> )} diff --git a/packages/react/src/views/MessageList/MessageList.js b/packages/react/src/views/MessageList/MessageList.js index 31dd291b7..48a6d7134 100644 --- a/packages/react/src/views/MessageList/MessageList.js +++ b/packages/react/src/views/MessageList/MessageList.js @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useMemo } from 'react'; +import { Virtuoso } from 'react-virtuoso'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import { isSameDay } from 'date-fns'; -import { Box, Icon, Throbber, useTheme } from '@embeddedchat/ui-elements'; +import { Box, Icon, Throbber } from '@embeddedchat/ui-elements'; import { useMessageStore } from '../../store'; import MessageReportWindow from '../ReportMessage/MessageReportWindow'; import isMessageSequential from '../../lib/isMessageSequential'; @@ -11,109 +12,133 @@ import isMessageLastSequential from '../../lib/isMessageLastSequential'; import { MessageBody } from '../Message/MessageBody'; import { MessageDivider } from '../Message/MessageDivider'; +const MessageListHeader = ({ + context: { loadingOlderMessages, isUserAuthenticated, hasMoreMessages }, +}) => { + if (loadingOlderMessages && isUserAuthenticated) { + return ( + + + + ); + } + if (!hasMoreMessages && isUserAuthenticated) { + return ( + + Start of conversation + + ); + } + return null; +}; + +const MessageListPlaceholder = ({ context: { isMessageLoaded } }) => ( + + + + {isMessageLoaded + ? 'No messages' + : 'Ready to chat? Login now to join the fun.'} + + +); + +const renderMessageItem = ( + index, + msg, + { filteredMessages, firstUnreadMessageId } +) => { + const prev = filteredMessages[index - 1]; + const next = filteredMessages[index + 1]; + + if (!msg) return null; + + const newDay = !prev || !isSameDay(new Date(msg.ts), new Date(prev.ts)); + const sequential = isMessageSequential(msg, prev, 300); + const lastSequential = sequential && isMessageLastSequential(msg, next); + const showUnreadDivider = + firstUnreadMessageId && msg._id === firstUnreadMessageId; + + return ( + + {showUnreadDivider && ( + Unread Messages + )} + + + ); +}; + const MessageList = ({ messages, loadingOlderMessages, isUserAuthenticated, hasMoreMessages, firstUnreadMessageId, + loadMoreMessages, + scrollerRef, + onAtBottomStateChange, }) => { const showReportMessage = useMessageStore((state) => state.showReportMessage); const messageToReport = useMessageStore((state) => state.messageToReport); const isMessageLoaded = useMessageStore((state) => state.isMessageLoaded); - const { theme } = useTheme(); - - const isMessageNewDay = (current, previous) => - !previous || !isSameDay(new Date(current.ts), new Date(previous.ts)); const filteredMessages = messages.filter((msg) => !msg.tmid); const reportedMessage = messages.find((msg) => msg._id === messageToReport); + const virtuosoComponents = useMemo( + () => ({ + Header: MessageListHeader, + EmptyPlaceholder: MessageListPlaceholder, + }), + [] + ); + return ( <> - {filteredMessages.length === 0 ? ( - - - - {isMessageLoaded - ? 'No messages' - : 'Ready to chat? Login now to join the fun.'} - - - ) : ( - <> - {!hasMoreMessages && isUserAuthenticated && ( - - Start of conversation - - )} - {loadingOlderMessages && isUserAuthenticated && ( - - - - )} - {filteredMessages - .slice() - .reverse() - .map((msg, index, arr) => { - const prev = arr[index - 1]; - const next = arr[index + 1]; - - if (!msg) return null; - const newDay = isMessageNewDay(msg, prev); - const sequential = isMessageSequential(msg, prev, 300); - const lastSequential = - sequential && isMessageLastSequential(msg, next); - const showUnreadDivider = - firstUnreadMessageId && msg._id === firstUnreadMessageId; - - return ( - - {showUnreadDivider && ( - Unread Messages - )} - - - ); - })} - {showReportMessage && ( - - )} - + + {showReportMessage && ( + )} ); diff --git a/yarn.lock b/yarn.lock index 23b67cfd1..745cb472c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2501,6 +2501,7 @@ __metadata: prop-types: ^15.8.1 react: ^17.0.2 react-dom: ^17.0.2 + react-virtuoso: ^4.18.1 rimraf: ^5.0.1 rollup: ^2.70.1 rollup-plugin-analyzer: ^4.0.0 @@ -27240,6 +27241,16 @@ __metadata: languageName: node linkType: hard +"react-virtuoso@npm:^4.18.1": + version: 4.18.1 + resolution: "react-virtuoso@npm:4.18.1" + peerDependencies: + react: ">=16 || >=17 || >= 18 || >= 19" + react-dom: ">=16 || >=17 || >= 18 || >=19" + checksum: 613261e9555a4d6fbb17ffc3443bc2d0b877c2f8ec52b5cd45ab40c91183e0e14d74151d1cf16195b0df91e41017d733d78aaf28b26987e55642d2b13aa733ed + languageName: node + linkType: hard + "react@npm:18.2.0, react@npm:^18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0"