Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/ui-kit": "^0.31.25",
"@rollup/plugin-json": "^6.0.0",
"@tanstack/react-virtual": "^3.13.18",
"date-fns": "^2.30.0",
"emoji-picker-react": "^4.4.9",
"emoji-toolkit": "^7.0.1",
Expand Down
21 changes: 21 additions & 0 deletions packages/react/src/context/MessageNavigationContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { createContext, useContext } from 'react';

export const MessageNavigationContext = createContext(null);

export const MessageNavigationProvider = ({ value, children }) => {
return (
<MessageNavigationContext.Provider value={value}>
{children}
</MessageNavigationContext.Provider>
);
};

export const useMessageNavigation = () => {
const ctx = useContext(MessageNavigationContext);
if (!ctx) {
throw new Error(
'useMessageNavigation must be used within MessageNavigationProvider'
);
}
return ctx;
};
2 changes: 2 additions & 0 deletions packages/react/src/store/pinnedMessageStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { create } from 'zustand';
const usePinnedMessageStore = create((set) => ({
showPinned: false,
setShowPinned: (showPinned) => set(() => ({ showPinned })),
pinnedMessages: [],
setPinnedMessages: (messages) => set(() => ({ pinnedMessages: messages })),
}));

export default usePinnedMessageStore;
105 changes: 104 additions & 1 deletion packages/react/src/views/ChatBody/ChatBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const ChatBody = ({
showRoles,
messageListRef,
scrollToBottom,
onRegisterJump,
clearUnreadDividerRef,
}) => {
const { classNames, styleOverrides } = useComponentOverrides('ChatBody');
Expand All @@ -63,11 +64,15 @@ const ChatBody = ({
const upsertMessage = useMessageStore((state) => state.upsertMessage);
const [loadingOlderMessages, setLoadingOlderMessages] = useState(false);
const [hasMoreMessages, setHasMoreMessages] = useState(true);
const hasMoreMessagesRef = useRef(hasMoreMessages);
const loadingOlderMessagesRef = useRef(loadingOlderMessages);
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 offsetRef = useRef(offset);
const jumpFnRef = useRef(null);

const [isThreadOpen, threadMainMessage] = useMessageStore((state) => [
state.isThreadOpen,
Expand Down Expand Up @@ -174,6 +179,18 @@ const ChatBody = ({
});
}, [RCInstance, anonymousMode, getMessagesAndRoles]);

useEffect(() => {
hasMoreMessagesRef.current = hasMoreMessages;
}, [hasMoreMessages]);

useEffect(() => {
loadingOlderMessagesRef.current = loadingOlderMessages;
}, [loadingOlderMessages]);

useEffect(() => {
offsetRef.current = offset;
}, [offset]);

useEffect(() => {
RCInstance.auth.onAuthChange((user) => {
if (user) {
Expand Down Expand Up @@ -289,6 +306,89 @@ const ChatBody = ({
firstUnreadMessageId,
]);

const loadOlderMessagesUntil = useCallback(
async (messageId, maxPages = 10) => {
if (!messageId) return false;
const hasMessage = () =>
useMessageStore
.getState()
.messages.some((msg) => msg._id === messageId);

if (hasMessage()) return true;

for (let attempt = 0; attempt < maxPages; attempt += 1) {
if (!hasMoreMessagesRef.current || loadingOlderMessagesRef.current) {
return false;
}

loadingOlderMessagesRef.current = true;
setLoadingOlderMessages(true);

try {
const olderMessages = await RCInstance.getOlderMessages(
anonymousMode,
ECOptions?.enableThreads
? {
query: {
tmid: {
$exists: false,
},
},
offset: offsetRef.current,
}
: undefined,
anonymousMode ? false : isChannelPrivate
);

if (olderMessages?.messages?.length) {
setMessages(olderMessages.messages, true);
const newOffset = offsetRef.current + olderMessages.messages.length;
setMessagesOffset(newOffset);
offsetRef.current = newOffset;
} else {
setHasMoreMessages(false);
hasMoreMessagesRef.current = false;
return false;
}
} catch (error) {
console.error('Error fetching older messages:', error);
setHasMoreMessages(false);
hasMoreMessagesRef.current = false;
return false;
} finally {
setLoadingOlderMessages(false);
loadingOlderMessagesRef.current = false;
}

if (hasMessage()) return true;
}

return hasMessage();
},
[
RCInstance,
anonymousMode,
ECOptions?.enableThreads,
isChannelPrivate,
setMessages,
setMessagesOffset,
]
);

const handleRegisterJump = useCallback(
(jumpFn) => {
jumpFnRef.current = jumpFn;
onRegisterJump?.(async (messageId) => {
const found = await loadOlderMessagesUntil(messageId);
if (!found) return;
requestAnimationFrame(() => {
jumpFnRef.current?.(messageId);
});
});
},
[loadOlderMessagesUntil, onRegisterJump]
);

const showNewMessagesPopup = () => {
setPopupVisible(true);
};
Expand Down Expand Up @@ -417,10 +517,12 @@ const ChatBody = ({
/>
) : (
<MessageList
messages={messages}
messagesS={messages}
loadingOlderMessages={loadingOlderMessages}
isUserAuthenticated={isUserAuthenticated}
hasMoreMessages={hasMoreMessages}
messageContainerRef={messageListRef}
onRegisterJump={handleRegisterJump}
firstUnreadMessageId={firstUnreadMessageId}
/>
)}
Expand Down Expand Up @@ -449,4 +551,5 @@ export default ChatBody;
ChatBody.propTypes = {
anonymousMode: PropTypes.bool,
showRoles: PropTypes.bool,
onRegisterJump: PropTypes.func,
};
82 changes: 64 additions & 18 deletions packages/react/src/views/ChatLayout/ChatLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import CheckPreviewType from '../AttachmentPreview/CheckPreviewType';
import { useRCContext } from '../../context/RCInstance';
import UiKitContextualBar from '../ContextualBarBlock/uiKit/UiKitContextualBar';
import useUiKitStore from '../../store/uiKitStore';
import { MessageNavigationProvider } from '../../context/MessageNavigationContext';

const ChatLayout = () => {
const messageListRef = useRef(null);
const jumpToMessageRef = useRef(null);
const clearUnreadDividerRef = useRef(null);
const { classNames, styleOverrides } = useComponentOverrides('ChatBody');
const { RCInstance, ECOptions } = useRCContext();
Expand Down Expand Up @@ -98,16 +100,40 @@ const ChatLayout = () => {
useEffect(() => {
getStarredMessages();
}, [showSidebar]);
const registerJumpToMessage = useCallback((fn) => {
jumpToMessageRef.current = fn;
}, []);

return (
<Box
css={styles.layout}
style={{
...styleOverrides,
<MessageNavigationProvider
value={{
jumpToMessage: (messageOrId) => {
const messageId =
typeof messageOrId === 'string' ? messageOrId : messageOrId?._id;
if (!messageId) return;
return jumpToMessageRef.current?.(messageId);
},
}}
className={`ec-chat-layout ${classNames}`}
onDragOver={(e) => handleDrag(e)}
onDrop={(e) => handleDragDrop(e)}
>
<Box
css={styles.layout}
style={{
...styleOverrides,
}}
className={`ec-chat-layout ${classNames}`}
onDragOver={(e) => handleDrag(e)}
onDrop={(e) => handleDragDrop(e)}
>
<Box css={styles.chatMain}>
<ChatBody
anonymousMode={anonymousMode}
showRoles={showRoles}
messageListRef={messageListRef}
scrollToBottom={scrollToBottom}
onRegisterJump={registerJumpToMessage}
/>
<ChatInput scrollToBottom={scrollToBottom} />
<div id="emoji-popup" />
<Box css={styles.chatMain}>
<ChatBody
anonymousMode={anonymousMode}
Expand Down Expand Up @@ -141,18 +167,38 @@ const ChatLayout = () => {
/>
)}
</Box>
)}

{attachmentWindowOpen ? (
data ? (
<>
<AttachmentPreview />
</>
) : (
<CheckPreviewType data={data} />
)
) : null}
</Box>
{showSidebar && (
<Box className="ec-sidebar-view">
{showMembers && <RoomMembers members={members} />}
{showSearch && <SearchMessages />}
{showChannelinfo && <Roominfo />}
{showAllThreads && <ThreadedMessages />}
{showAllFiles && <FileGallery />}
{showMentions && <MentionedMessages />}
{showPinned && <PinnedMessages />}
{showStarred && <StarredMessages />}
{showCurrentUserInfo && <UserInformation />}
{uiKitContextualBarOpen && (
<UiKitContextualBar
key={Math.random()}
initialView={uiKitContextualBarData}
/>
)}
</Box>
)}

{attachmentWindowOpen ? (
data ? (
<>
<AttachmentPreview />
</>
) : (
<CheckPreviewType data={data} />
)
) : null}
</Box>
</MessageNavigationProvider>
);
};

Expand Down
17 changes: 16 additions & 1 deletion packages/react/src/views/FileMessage/FileMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
memo,
useContext,
useEffect,
useMemo,
} from 'react';
import PropTypes from 'prop-types';
import {
Expand All @@ -29,12 +30,19 @@ import { useRCContext } from '../../context/RCInstance';
import { useChannelStore, useMessageStore } from '../../store';
import { fileDisplayStyles as styles } from './Files.styles';

const FileMessage = ({ fileMessage, onDeleteFile }) => {
const FileMessage = ({ fileMessage, onDeleteFile, onClick }) => {
const { classNames, styleOverrides } = useComponentOverrides('FileMessage');
const [fileId, setFileId] = useState(null);
const dispatchToastMessage = useToastBarDispatch();
const { RCInstance } = useRCContext();
const messages = useMessageStore((state) => state.messages);
const threadMessages = useMessageStore((state) => state.threadMessages) || [];
const allMessages = useMemo(
() => [...messages, ...[...threadMessages].reverse()],
[messages, threadMessages]
);
const [files, setFiles] = useState([]);
const [fileMessageId, setFileMessageId] = useState(null);
const theme = useTheme();
const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate);
const [isFetching, setIsFetching] = useState(true);
Expand Down Expand Up @@ -84,6 +92,12 @@ const FileMessage = ({ fileMessage, onDeleteFile }) => {
[messages, RCInstance, dispatchToastMessage]
);
useEffect(() => {
const targetFileId = fileMessage._id;
const matchedMessages = allMessages.filter((message) =>
message.file ? message.file._id === targetFileId : false
);
const messageObject = matchedMessages[0];
setFileMessageId(messageObject);
const fetchAllFiles = async () => {
const res = await RCInstance.getAllFiles(isChannelPrivate, '');
if (res?.files) {
Expand All @@ -107,6 +121,7 @@ const FileMessage = ({ fileMessage, onDeleteFile }) => {
className={appendClassNames('ec-file', classNames)}
style={styleOverrides}
css={[messageStyles, hoverStyle]}
onClick={() => onClick(fileMessageId)}
>
<FilePreviewContainer file={fileMessage} />
<FileBodyContainer style={{ width: '75%' }}>
Expand Down
13 changes: 13 additions & 0 deletions packages/react/src/views/MessageAggregators/FileGallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const FileGallery = () => {
const [files, setFiles] = useState([]);
const [selectedFilter, setSelectedFilter] = useState('all');

const [fileMessageIds, setFileMessageIds] = useState([]);
const threadMessages = useMessageStore((state) => state.threadMessages) || [];
const allMessages = useMemo(
() => [...messages, ...[...threadMessages].reverse()],
[messages, threadMessages]
);

const options = [
{ value: 'all', label: 'All' },
{ value: 'application', label: 'Files' },
Expand Down Expand Up @@ -45,7 +52,12 @@ const FileGallery = () => {
const sortedFiles = res.files.sort(
(a, b) => new Date(b.uploadedAt) - new Date(a.uploadedAt)
);
const fileIdSet = new Set(res.files.map((file) => file._id));
const matchedMessages = allMessages
.filter((message) => fileIdSet.has(message.file?._id))
.map((message) => message._id);
setFiles(sortedFiles);
setFileMessageIds(matchedMessages);
setIsFetching(false);
}
};
Expand Down Expand Up @@ -86,6 +98,7 @@ const FileGallery = () => {
}}
fetching={isFetching}
shouldRender={(file) => file.path}
fileMessageIds={fileMessageIds}
type="file"
searchFiltered={filteredFiles}
viewType={viewType}
Expand Down
Loading