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"