Skip to content

Commit

Permalink
feat: implement loading of older messages functionality (#810)
Browse files Browse the repository at this point in the history
* feat: implement loading of older messages functionality

* Fix lint error

* Globalize messages offset

* ensure loader appears far from date header
  • Loading branch information
SinghaAnirban005 authored Jan 30, 2025
1 parent 9d84f49 commit bde1a16
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 36 deletions.
41 changes: 41 additions & 0 deletions packages/api/src/EmbeddedChatApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,47 @@ export default class EmbeddedChatApi {
}
}

async getOlderMessages(
anonymousMode = false,
options: {
query?: object | undefined;
field?: object | undefined;
offset?: number;
} = {
query: undefined,
field: undefined,
offset: 50,
},
isChannelPrivate = false
) {
const roomType = isChannelPrivate ? "groups" : "channels";
const endp = anonymousMode ? "anonymousread" : "messages";
const query = options?.query
? `&query=${JSON.stringify(options.query)}`
: "";
const field = options?.field
? `&field=${JSON.stringify(options.field)}`
: "";
const offset = options?.offset ? options.offset : 0;
try {
const { userId, authToken } = (await this.auth.getCurrentUser()) || {};
const messages = await fetch(
`${this.host}/api/v1/${roomType}.${endp}?roomId=${this.rid}${query}${field}&offset=${offset}`,
{
headers: {
"Content-Type": "application/json",
"X-Auth-Token": authToken,
"X-User-Id": userId,
},
method: "GET",
}
);
return await messages.json();
} catch (err) {
console.log(err);
}
}

async getThreadMessages(tmid: string, isChannelPrivate = false) {
try {
const { userId, authToken } = (await this.auth.getCurrentUser()) || {};
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/hooks/useFetchChatData.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const useFetchChatData = (showRoles) => {
const setMemberRoles = useMemberStore((state) => state.setMemberRoles);
const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate);
const setMessages = useMessageStore((state) => state.setMessages);
const setMessagesOffset = useMessageStore((state) => state.setMessagesOffset);
const setAdmins = useMemberStore((state) => state.setAdmins);
const setStarredMessages = useStarredMessageStore(
(state) => state.setStarredMessages
Expand All @@ -31,7 +32,7 @@ const useFetchChatData = (showRoles) => {
return;
}

const { messages } = await RCInstance.getMessages(
const { messages, count } = await RCInstance.getMessages(
anonymousMode,
ECOptions?.enableThreads
? {
Expand All @@ -47,6 +48,7 @@ const useFetchChatData = (showRoles) => {

if (messages) {
setMessages(messages.filter((message) => message._hidden !== true));
setMessagesOffset(count);
}

if (!isUserAuthenticated) {
Expand Down
20 changes: 15 additions & 5 deletions packages/react/src/store/messageStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const useMessageStore = create((set, get) => ({
threadMessages: [],
filtered: false,
editMessage: {},
messagesOffset: 0,
quoteMessage: [],
messageToReport: NaN,
showReportMessage: false,
Expand All @@ -16,11 +17,19 @@ const useMessageStore = create((set, get) => ({
threadMainMessage: null,
headerTitle: null,
setFilter: (filter) => set(() => ({ filtered: filter })),
setMessages: (messages) =>
set(() => ({
messages,
isMessageLoaded: true,
})),
setMessages: (newMessages, append = false) =>
set((state) => {
const allMessages = append
? [...state.messages, ...newMessages]
: newMessages;
const uniqueMessages = Array.from(
new Map(allMessages.map((msg) => [msg._id, msg])).values()
);
return {
messages: uniqueMessages,
isMessageLoaded: true,
};
}),
upsertMessage: (message, enableThreads = false) => {
if (message.tmid && enableThreads) {
if (get().threadMainMessage?._id === message.tmid) {
Expand Down Expand Up @@ -71,6 +80,7 @@ const useMessageStore = create((set, get) => ({
}
},
setEditMessage: (editMessage) => set(() => ({ editMessage })),
setMessagesOffset: (newOffset) => set(() => ({ messagesOffset: newOffset })),
editMessagePermissions: {},
setEditMessagePermissions: (editMessagePermissions) =>
set((state) => ({ ...state, editMessagePermissions })),
Expand Down
74 changes: 72 additions & 2 deletions packages/react/src/views/ChatBody/ChatBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,19 @@ const ChatBody = ({
const { RCInstance, ECOptions } = useContext(RCContext);
const showAnnouncement = ECOptions?.showAnnouncement;
const messages = useMessageStore((state) => state.messages);
const offset = useMessageStore((state) => state.messagesOffset);
const setMessagesOffset = useMessageStore((state) => state.setMessagesOffset);
const threadMessages = useMessageStore((state) => state.threadMessages);
const [isModalOpen, setModalOpen] = useState(false);
const setThreadMessages = useMessageStore((state) => state.setThreadMessages);
const upsertMessage = useMessageStore((state) => state.upsertMessage);
const [loadingOlderMessages, setLoadingOlderMessages] = useState(false);
const [hasMoreMessages, setHasMoreMessages] = useState(true);
const removeMessage = useMessageStore((state) => state.removeMessage);
const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate);
const channelInfo = useChannelStore((state) => state.channelInfo);
const isLoginIn = useLoginStore((state) => state.isLoginIn);
const setMessages = useMessageStore((state) => state.setMessages);

const [isThreadOpen, threadMainMessage] = useMessageStore((state) => [
state.isThreadOpen,
Expand Down Expand Up @@ -153,6 +158,7 @@ const ChatBody = ({
RCInstance.auth.onAuthChange((user) => {
if (user) {
getMessagesAndRoles();
setHasMoreMessages(true);
} else {
getMessagesAndRoles(anonymousMode);
}
Expand All @@ -166,13 +172,57 @@ const ChatBody = ({
setPopupVisible(false);
};

const handleScroll = useCallback(() => {
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,
},
},
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);
setHasMoreMessages(false);
} finally {
setLoadingOlderMessages(false);
}
}
}

const isAtBottom = messageListRef?.current?.scrollTop === 0;
Expand All @@ -183,6 +233,15 @@ const ChatBody = ({
}
}, [
messageListRef,
offset,
setMessagesOffset,
setMessages,
anonymousMode,
hasMoreMessages,
RCInstance,
isChannelPrivate,
ECOptions?.enableThreads,
loadingOlderMessages,
setScrollPosition,
setIsUserScrolledUp,
setPopupVisible,
Expand All @@ -207,6 +266,12 @@ const ChatBody = ({
}
};

useEffect(() => {
if (messageListRef.current) {
messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
}
}, [messages]);

useEffect(() => {
checkOverflow();
}, [channelInfo.announcement, showAnnouncement]);
Expand Down Expand Up @@ -292,7 +357,12 @@ const ChatBody = ({
threadMessages={threadMessages}
/>
) : (
<MessageList messages={messages} />
<MessageList
messages={messages}
loadingOlderMessages={loadingOlderMessages}
isUserAuthenticated={isUserAuthenticated}
hasMoreMessages={hasMoreMessages}
/>
)}

<TotpModal handleLogin={handleLogin} />
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/views/ChatBody/ChatBody.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const getChatbodyStyles = (theme, mode) => {
overflow: auto;
overflow-x: hidden;
display: flex;
flex-direction: column-reverse;
flex-direction: column;
max-height: 100%;
position: relative;
padding-top: 70px;
Expand Down
10 changes: 5 additions & 5 deletions packages/react/src/views/Message/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ const Message = ({

return (
<>
{newDay && (
<MessageDivider>
{format(new Date(message.ts), 'MMMM d, yyyy')}
</MessageDivider>
)}
<Box
className={appendClassNames('ec-message', classNames)}
css={[
Expand Down Expand Up @@ -355,11 +360,6 @@ const Message = ({
) : null}
</MessageBodyContainer>
</Box>
{newDay && (
<MessageDivider>
{format(new Date(message.ts), 'MMMM d, yyyy')}
</MessageDivider>
)}
</>
);
};
Expand Down
82 changes: 60 additions & 22 deletions packages/react/src/views/MessageList/MessageList.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ import React from 'react';
import PropTypes from 'prop-types';
import { css } from '@emotion/react';
import { isSameDay } from 'date-fns';
import { Box, Icon } from '@embeddedchat/ui-elements';
import { Box, Icon, Throbber, useTheme } from '@embeddedchat/ui-elements';
import { useMessageStore } from '../../store';
import MessageReportWindow from '../ReportMessage/MessageReportWindow';
import isMessageSequential from '../../lib/isMessageSequential';
import { Message } from '../Message';
import isMessageLastSequential from '../../lib/isMessageLastSequential';
import { MessageBody } from '../Message/MessageBody';

const MessageList = ({ messages }) => {
const MessageList = ({
messages,
loadingOlderMessages,
isUserAuthenticated,
hasMoreMessages,
}) => {
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));
Expand All @@ -37,28 +44,59 @@ const MessageList = ({ messages }) => {
</Box>
) : (
<>
{filteredMessages.map((msg, index, arr) => {
const prev = arr[index + 1];
const next = arr[index - 1];
{!hasMoreMessages && isUserAuthenticated && (
<MessageBody
style={{
position: 'absolute',
top: 0,
left: '50%',
transform: 'translateX(-50%)',
padding: '8px 16px',
zIndex: 10,
}}
>
Start of conversation
</MessageBody>
)}
{loadingOlderMessages && isUserAuthenticated && (
<Box
css={css`
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
padding: 8px 16px;
z-index: 10;
`}
>
<Throbber />
</Box>
)}
{messages
.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);
if (!msg) return null;
const newDay = isMessageNewDay(msg, prev);
const sequential = isMessageSequential(msg, prev, 300);
const lastSequential =
sequential && isMessageLastSequential(msg, next);

return (
<Message
key={msg._id}
message={msg}
newDay={newDay}
sequential={sequential}
lastSequential={lastSequential}
type="default"
showAvatar
/>
);
})}
return (
<Message
key={msg._id}
message={msg}
newDay={newDay}
sequential={sequential}
lastSequential={lastSequential}
type="default"
showAvatar
/>
);
})}
{showReportMessage && (
<MessageReportWindow messageId={messageToReport} />
)}
Expand Down

0 comments on commit bde1a16

Please sign in to comment.