diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 3a1c3b5f9e..952d26c993 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -572,6 +572,28 @@ export default class EmbeddedChatApi { } } + async getAllThreadMessages(type?: string, text?: string, offset?:number, count?:number) { + try { + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + console.log("offset",offset); + console.log("count",count); + const allthreads = await fetch( + `${this.host}/api/v1/chat.getThreadsList?rid=${this.rid}&type=${type}&text=${text}&offset=${offset}&count=${count}`, + { + headers: { + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, + }, + method: "GET", + } + ); + return await allthreads.json(); + } catch (err) { + console.log(err); + } + } + async getChannelRoles(isChannelPrivate = false) { const roomType = isChannelPrivate ? "groups" : "channels"; try { diff --git a/packages/docs/blog/EmbeddedChat-2023.md b/packages/docs/blog/EmbeddedChat-2023.md index 80e5f66a08..0b59e863b5 100644 --- a/packages/docs/blog/EmbeddedChat-2023.md +++ b/packages/docs/blog/EmbeddedChat-2023.md @@ -132,6 +132,7 @@ class EmbeddedChatApi { field?: object | undefined; }): Promise; getThreadMessages(tmid: string): Promise; + getAllThreadMessages( type?: string, text?: string, offset?:number, count?:number): Promise; getChannelRoles(): Promise; sendTypingStatus(username: string, typing: boolean): Promise; sendMessage(message: any, threadId: string): Promise; diff --git a/packages/react/src/hooks/useFetchChatData.js b/packages/react/src/hooks/useFetchChatData.js index 583ad431e5..866c6f7ee0 100644 --- a/packages/react/src/hooks/useFetchChatData.js +++ b/packages/react/src/hooks/useFetchChatData.js @@ -1,4 +1,4 @@ -import { useCallback, useContext } from 'react'; +import { useCallback, useContext,useState } from 'react'; import RCContext from '../context/RCInstance'; import { useUserStore, @@ -17,12 +17,19 @@ const useFetchChatData = (showRoles) => { const setStarredMessages = useStarredMessageStore( (state) => state.setStarredMessages ); + const [loading, setLoading] = useState(false); const isUserAuthenticated = useUserStore( (state) => state.isUserAuthenticated ); const setViewUserInfoPermissions = useUserStore( (state) => state.setViewUserInfoPermissions ); + const AllThreadMessages = useMessageStore((state) => state.allThreadMessages); + const setAllThreadMessages = useMessageStore( + (state) => state.setAllThreadMessages + ); + const setOffset=useMessageStore((state)=>state.setOffset); + const threadOffset= useMessageStore((state)=>state.threadOffset); const getMessagesAndRoles = useCallback( async (anonymousMode) => { @@ -90,6 +97,27 @@ const useFetchChatData = (showRoles) => { ] ); + const getAllThreadMessages = useCallback( + async (anonymousMode) => { + if (isUserAuthenticated) { + try { + if (!isUserAuthenticated && !anonymousMode) { + return; + } + setLoading(true); + const { threads: allThreadMessages } = await RCInstance.getAllThreadMessages('', '', threadOffset, 30); + setAllThreadMessages(allThreadMessages,true); + setOffset((prevOffset) => prevOffset + 30); + setLoading(false); + } catch (e) { + console.log(e); + setLoading(false); + } + } + }, + [isUserAuthenticated, RCInstance, setAllThreadMessages] + ); + const getStarredMessages = useCallback( async (anonymousMode) => { if (isUserAuthenticated) { @@ -107,7 +135,7 @@ const useFetchChatData = (showRoles) => { [isUserAuthenticated, RCInstance, setStarredMessages] ); - return { getMessagesAndRoles, getStarredMessages }; + return { getMessagesAndRoles, getStarredMessages, getAllThreadMessages }; }; export default useFetchChatData; diff --git a/packages/react/src/store/messageStore.js b/packages/react/src/store/messageStore.js index 69d1d2b73f..0af453f066 100644 --- a/packages/react/src/store/messageStore.js +++ b/packages/react/src/store/messageStore.js @@ -6,6 +6,9 @@ const useMessageStore = create((set, get) => ({ messages: [], isMessageLoaded: false, threadMessages: [], + allThreadMessages: [], + threadOffset:0, + filtered: false, editMessage: {}, quoteMessage: [], @@ -113,6 +116,19 @@ const useMessageStore = create((set, get) => ({ }, setThreadMessages: (messages) => set(() => ({ threadMessages: messages })), setHeaderTitle: (title) => set(() => ({ headerTitle: title })), + setAllThreadMessages: (newMessages, append = false) => + set((state) => { + const allMessages = append + ? [...state.allThreadMessages, ...newMessages] + : newMessages; + const uniqueMessages = Array.from( + new Map(allMessages.map((msg) => [msg._id, msg])).values() + ); + return { + allThreadMessages: uniqueMessages, + }; + }), + setOffset: (offset) => set(() => ({ threadOffset: offset })), })); export default useMessageStore; diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index 940b63e9bb..df583e3c80 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -59,6 +59,12 @@ const ChatBody = ({ const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate); const channelInfo = useChannelStore((state) => state.channelInfo); const isLoginIn = useLoginStore((state) => state.isLoginIn); + const setOffset = useMessageStore((state) => state.setOffset); + const threadOffset = useMessageStore((state) => state.threadOffset); + const setAllThreadMessages = useMessageStore( + (state) => state.setAllThreadMessages + ); + const { getAllThreadMessages } = useFetchChatData(); const [isThreadOpen, threadMainMessage] = useMessageStore((state) => [ state.isThreadOpen, @@ -80,6 +86,30 @@ const ChatBody = ({ const username = useUserStore((state) => state.username); const { getMessagesAndRoles } = useFetchChatData(showRoles); + const loadMoreThreadMessages = useCallback(async (currentLength) => { + if (isUserAuthenticated && threadMainMessage?._id) { + try { + if (!isUserAuthenticated && !anonymousMode) { + return; + } + const { threads: moreThreadMessages } = await RCInstance.getAllThreadMessages( + '', '', threadOffset, 30 + ); + setThreadMessages((prevMessages) => [...prevMessages, ...moreThreadMessages]); + setOffset(threadOffset + 30); + } catch (e) { + console.error(e); + } + } + }, [ + isUserAuthenticated, + anonymousMode, + RCInstance, + threadMainMessage?._id, + setThreadMessages, + threadOffset, + setOffset, + ]); const getThreadMessages = useCallback(async () => { if (isUserAuthenticated && threadMainMessage?._id) { @@ -92,6 +122,8 @@ const ChatBody = ({ isChannelPrivate ); setThreadMessages(messages.reverse()); + + // getAllThreadMessages('','',10,30); } catch (e) { console.error(e); } @@ -181,12 +213,15 @@ const ChatBody = ({ setIsUserScrolledUp(false); setOtherUserMessage(false); } + }, [ messageListRef, setScrollPosition, setIsUserScrolledUp, setPopupVisible, setOtherUserMessage, + + ]); const showNewMessagesPopup = () => { diff --git a/packages/react/src/views/ChatLayout/ChatLayout.js b/packages/react/src/views/ChatLayout/ChatLayout.js index 60c935a360..999e6a6be4 100644 --- a/packages/react/src/views/ChatLayout/ChatLayout.js +++ b/packages/react/src/views/ChatLayout/ChatLayout.js @@ -12,6 +12,7 @@ import { useThreadsMessageStore, useMemberStore, useSidebarStore, + useMessageStore, } from '../../store'; import RoomMembers from '../RoomMembers/RoomMember'; @@ -35,6 +36,8 @@ import useUiKitStore from '../../store/uiKitStore'; const ChatLayout = () => { const messageListRef = useRef(null); + const threadListRef = useRef(null); + const [isFetching, setIsFetching]=useState(false); const { classNames, styleOverrides } = useComponentOverrides('ChatBody'); const { RCInstance, ECOptions } = useRCContext(); const anonymousMode = ECOptions?.anonymousMode; @@ -45,6 +48,9 @@ const ChatLayout = () => { const starredMessages = useStarredMessageStore( (state) => state.starredMessages ); + const setAllThreadMessages = useMessageStore( + (state) => state.setAllThreadMessages + ); const showSidebar = useSidebarStore((state) => state.showSidebar); const showMentions = useMentionsStore((state) => state.showMentions); const showAllFiles = useFileStore((state) => state.showAllFiles); @@ -97,6 +103,11 @@ const ChatLayout = () => { useEffect(() => { getStarredMessages(); }, [showSidebar]); + + + + + return ( { {showMembers && } {showSearch && } {showChannelinfo && } - {showAllThreads && } + {showAllThreads && } {showAllFiles && } {showMentions && } {showPinned && } diff --git a/packages/react/src/views/MessageAggregators/ThreadedMessages.js b/packages/react/src/views/MessageAggregators/ThreadedMessages.js index bf9aefb3c1..2245398c39 100644 --- a/packages/react/src/views/MessageAggregators/ThreadedMessages.js +++ b/packages/react/src/views/MessageAggregators/ThreadedMessages.js @@ -1,16 +1,14 @@ -import React, { useState, useMemo } from 'react'; - +import React, { useState, useMemo, forwardRef } from 'react'; import { useComponentOverrides } from '@embeddedchat/ui-elements'; import { useMessageStore } from '../../store'; - import { MessageAggregator } from './common/MessageAggregator'; -const ThreadedMessages = () => { +const ThreadedMessages = ({threadListRef}) => { const messages = useMessageStore((state) => state.messages); const { variantOverrides } = useComponentOverrides('ThreadedMessages'); const viewType = variantOverrides.viewType || 'Sidebar'; const [text, setText] = useState(''); - + const allThreadMessages = useMessageStore((state) => state.allThreadMessages); const handleInputChange = (e) => { setText(e.target.value); }; @@ -24,19 +22,23 @@ const ThreadedMessages = () => { ); return ( - !msg.t && msg.tcount} - viewType={viewType} - /> + + !msg.t && msg.tcount} + viewType={viewType} + /> + ); }; diff --git a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js index 44461790a3..a985a74507 100644 --- a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js +++ b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js @@ -1,5 +1,12 @@ -import React, { useState, useMemo } from 'react'; -import { isSameDay, format, set } from 'date-fns'; +import React, { + useState, + useMemo, + forwardRef, + useCallback, + useEffect, +} from 'react'; + +import { isSameDay, format } from 'date-fns'; import { Box, Sidebar, @@ -18,130 +25,227 @@ import NoMessagesIndicator from './NoMessageIndicator'; import FileDisplay from '../../FileMessage/FileMessage'; import useSetExclusiveState from '../../../hooks/useSetExclusiveState'; import { useRCContext } from '../../../context/RCInstance'; +import { useUserStore } from '../../../store'; -export const MessageAggregator = ({ - title, - iconName, - noMessageInfo, - shouldRender, - fetchedMessageList, - filterProps, - searchProps, - searchFiltered, - fetching, - type = 'message', - viewType = 'Sidebar', -}) => { - const { theme } = useTheme(); - const styles = getMessageAggregatorStyles(theme); - const setExclusiveState = useSetExclusiveState(); - const { ECOptions } = useRCContext(); - const showRoles = ECOptions?.showRoles; - const messages = useMessageStore((state) => state.messages); - const threadMessages = useMessageStore((state) => state.threadMessages) || []; - const allMessages = useMemo( - () => [...messages, ...[...threadMessages].reverse()], - [messages, threadMessages] - ); - - const [messageRendered, setMessageRendered] = useState(false); - const { loading, messageList } = useSetMessageList( - fetchedMessageList || searchFiltered || allMessages, - shouldRender - ); - - const setShowSidebar = useSidebarStore((state) => state.setShowSidebar); - const openThread = useMessageStore((state) => state.openThread); - const closeThread = useMessageStore((state) => state.closeThread); - - const setJumpToMessage = (msg) => { - if (!msg || !msg._id) { - console.error('Invalid message object:', msg); - return; - } - const { _id: msgId, tmid: threadId } = msg; - if (msgId) { - let element; - if (threadId) { - const parentMessage = messages.find((m) => m._id === threadId); - if (parentMessage) { - closeThread(); - setTimeout(() => { - openThread(parentMessage); - setShowSidebar(false); +export const MessageAggregator = ( + ( + { + title, + iconName, + noMessageInfo, + shouldRender, + fetchedMessageList, + filterProps, + searchProps, + searchFiltered, + fetching, + type = 'message', + viewType = 'Sidebar', + threadListRef={threadListRef} + }, + + ) => { + const { theme } = useTheme(); + const styles = getMessageAggregatorStyles(theme); + const setExclusiveState = useSetExclusiveState(); + const { ECOptions } = useRCContext(); + const showRoles = ECOptions?.showRoles; + const messages = useMessageStore((state) => state.messages); + const isUserAuthenticated = useUserStore( + (state) => state.isUserAuthenticated + ); + const threadOffset = useMessageStore((state) => state.threadOffset); + const setOffset = useMessageStore((state) => state.setOffset); + const [, setIsUserScrolledUp] = useState(false); + const setAllThreadMessages = useMessageStore( + (state) => state.setAllThreadMessages + ); + const allThreadMessages = useMessageStore( + (state) => state.allThreadMessages + ); + + const threadMessages = + useMessageStore((state) => state.threadMessages) || []; + const allMessages = useMemo( + () => [...messages, ...[...threadMessages].reverse()], + [messages, threadMessages] + ); + const [scrollPosition, setScrollPosition] = useState(0); + const [popupVisible, setPopupVisible] = useState(false); + const [isFetching, setIsFetching] = useState(false); + + const [messageRendered, setMessageRendered] = useState(false); + const { loading, messageList } = useSetMessageList( + fetchedMessageList || searchFiltered || allMessages, + shouldRender + ); + const { RCInstance } = useRCContext(); + const showSidebar = useSidebarStore((state) => state.showSidebar); + + const setShowSidebar = useSidebarStore((state) => state.setShowSidebar); + const openThread = useMessageStore((state) => state.openThread); + const closeThread = useMessageStore((state) => state.closeThread); + + const setJumpToMessage = (msg) => { + if (!msg || !msg._id) { + console.error('Invalid message object:', msg); + return; + } + const { _id: msgId, tmid: threadId } = msg; + if (msgId) { + let element; + if (threadId) { + const parentMessage = messages.find((m) => m._id === threadId); + if (parentMessage) { + closeThread(); setTimeout(() => { - element = document.getElementById(`ec-message-body-${msgId}`); - if (element) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }); - element.style.backgroundColor = theme.colors.warning; - setTimeout(() => { - element.style.backgroundColor = ''; - }, 1000); - } + openThread(parentMessage); + setShowSidebar(false); + setTimeout(() => { + element = document.getElementById(`ec-message-body-${msgId}`); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + element.style.backgroundColor = theme.colors.warning; + setTimeout(() => { + element.style.backgroundColor = ''; + }, 1000); + } + }, 300); }, 300); + } + } else { + closeThread(); + setTimeout(() => { + element = document.getElementById(`ec-message-body-${msgId}`); + if (element) { + setShowSidebar(false); + element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + element.style.backgroundColor = theme.colors.warning; + setTimeout(() => { + element.style.backgroundColor = ''; + }, 1000); + } }, 300); } - } else { - closeThread(); - setTimeout(() => { - element = document.getElementById(`ec-message-body-${msgId}`); - if (element) { - setShowSidebar(false); - element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - element.style.backgroundColor = theme.colors.warning; - setTimeout(() => { - element.style.backgroundColor = ''; - }, 1000); - } - }, 300); } - } - }; - - const isMessageNewDay = (current, previous) => - !previous || - shouldRender(previous) || - !isSameDay(new Date(current.ts), new Date(previous.ts)); - - const noMessages = messageList?.length === 0 || !messageRendered; - const ViewComponent = viewType === 'Popup' ? Popup : Sidebar; - - return ( - setExclusiveState(null)} - style={{ - width: '400px', - padding: 0, - zIndex: window.innerWidth <= 780 ? 1 : null, - }} - {...(viewType === 'Popup' - ? { - isPopupHeader: true, + }; + + const isMessageNewDay = (current, previous) => + !previous || + shouldRender(previous) || + !isSameDay(new Date(current.ts), new Date(previous.ts)); + + const noMessages = messageList?.length === 0 || !messageRendered; + const ViewComponent = viewType === 'Popup' ? Popup : Sidebar; + + const getAllThreads = useCallback( + async (offset = 0) => { + if (isUserAuthenticated && !isFetching) { + setIsFetching(true); + try { + const { threads: fetchedThreadMessages } = + await RCInstance.getAllThreadMessages('', '', offset, 30); + + setAllThreadMessages(fetchedThreadMessages, true); + + setOffset(offset + 30); + } catch (e) { + console.error(e); + } finally { + setIsFetching(false); } - : {})} - > - {fetching || loading ? ( - - ) : ( - - {noMessages && ( - - )} - - {[...new Map(messageList.map((msg) => [msg._id, msg])).values()].map( - (msg, index, arr) => { + } + }, + [ + isUserAuthenticated, + isFetching, + RCInstance, + setAllThreadMessages, + setOffset, + ] + ); + + const handleThreadScroll = useCallback(() => { + console.log('heerrrthreadsfdsf'); + if (threadListRef && threadListRef.current) { + setScrollPosition(threadListRef.current.scrollTop); + setIsUserScrolledUp( + threadListRef.current.scrollTop + threadListRef.current.clientHeight < + threadListRef.current.scrollHeight + ); + } + + const isAtBottom = + threadListRef?.current?.scrollTop + + threadListRef.current.clientHeight >= + threadListRef.current.scrollHeight - 100; + console.log('bootom', isAtBottom); + if (isAtBottom) { + console.log('here'); + getAllThreads(threadListRef.current.children.length); + } + }, [threadListRef, setScrollPosition, setIsUserScrolledUp, getAllThreads]); + useEffect(() => { + console.log('threadListRef', threadListRef); + if (threadListRef.current) { + console.log('Adding scroll event listener'); + threadListRef.current.addEventListener('scroll', handleThreadScroll); + } + return () => { + if (threadListRef.current) { + threadListRef.current.removeEventListener( + 'scroll', + handleThreadScroll + ); + } + }; + }, [handleThreadScroll, threadListRef]); + + useEffect(() => { + getAllThreads(); + }, [showSidebar]); + + return ( + setExclusiveState(null)} + style={{ + width: '400px', + padding: 0, + zIndex: window.innerWidth <= 780 ? 1 : null, + }} + {...(viewType === 'Popup' + ? { + isPopupHeader: true, + } + : {})} + > + {fetching || loading ? ( + + ) : ( + + {noMessages && ( + + )} + + {[ + ...new Map(messageList.map((msg) => [msg._id, msg])).values(), + ].map((msg, index, arr) => { const newDay = isMessageNewDay(msg, arr[index - 1]); if (!messageRendered && shouldRender(msg)) { setMessageRendered(true); @@ -199,10 +303,10 @@ export const MessageAggregator = ({ )} ); - } - )} - - )} - - ); -}; + })} + + )} + + ); + } +);