Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement loading of older messages functionality #810

Merged
merged 7 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading