Skip to content
Draft
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
5 changes: 2 additions & 3 deletions apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Emitter } from '@rocket.chat/emitter';
import { differenceInMilliseconds } from 'date-fns';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import type { MutableRefObject } from 'react';

import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived';
import { getUserId } from '../../../../client/lib/user';
Expand Down Expand Up @@ -216,14 +215,13 @@ class RoomHistoryManagerClass extends Emitter {
room.scroll = undefined;
}

public async getMoreNext(rid: IRoom['_id'], atBottomRef: MutableRefObject<boolean>) {
public async getMoreNext(rid: IRoom['_id']) {
const room = this.getRoom(rid);
if (Tracker.nonreactive(() => room.hasMoreNext.get()) !== true) {
return;
}

await this.queue();
atBottomRef.current = false;

room.isLoading.set(true);

Expand Down Expand Up @@ -315,6 +313,7 @@ class RoomHistoryManagerClass extends Emitter {
}

const room = this.getRoom(message.rid);
room.isLoading.set(true);

const subscription = Subscriptions.state.find((record) => record.rid === message.rid);
const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit, showThreadMessages);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IMessage } from '@rocket.chat/core-typings';
import type { KeyboardEvent, MouseEvent, RefCallback } from 'react';
import type { KeyboardEvent, MouseEvent } from 'react';
import { createContext, useContext } from 'react';

import type { useFormatDate } from '../../../hooks/useFormatDate';
Expand Down Expand Up @@ -42,7 +42,6 @@ export type MessageListContextValue = {
formatDateAndTime: ReturnType<typeof useFormatDateAndTime>;
formatTime: ReturnType<typeof useFormatTime>;
formatDate: ReturnType<typeof useFormatDate>;
messageListRef?: RefCallback<HTMLElement | undefined>;
};

export const messageListContextDefaultValue: MessageListContextValue = {
Expand Down Expand Up @@ -73,7 +72,6 @@ export const messageListContextDefaultValue: MessageListContextValue = {
formatDateAndTime: () => '',
formatTime: () => '',
formatDate: () => '',
messageListRef: undefined,
};

export const MessageListContext = createContext<MessageListContextValue>(messageListContextDefaultValue);
Expand All @@ -98,8 +96,6 @@ export const useUserHasReacted: MessageListContextValue['useUserHasReacted'] = (
export const useOpenEmojiPicker: MessageListContextValue['useOpenEmojiPicker'] = (...args) =>
useContext(MessageListContext).useOpenEmojiPicker(...args);

export const useMessageListRef = (): MessageListContextValue['messageListRef'] => useContext(MessageListContext).messageListRef;

export const useMessageListShowColors = (): MessageListContextValue['showColors'] => useContext(MessageListContext).showColors;

export const useMessageListKatex = (): MessageListContextValue['katex'] => useContext(MessageListContext).katex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
useIsSelectedMessage,
useCountSelected,
} from '../../../views/room/MessageList/contexts/SelectedMessagesContext';
import { useJumpToMessage } from '../../../views/room/MessageList/hooks/useJumpToMessage';
import Emoji from '../../Emoji';
import IgnoredContent from '../IgnoredContent';
import MessageHeader from '../MessageHeader';
Expand Down Expand Up @@ -86,11 +85,9 @@ const RoomMessage = ({
const { enabled: readReceiptEnabled } = useMessageListReadReceipts();

useCountSelected();
const messageRef = useJumpToMessage(message._id);

return (
<Message
ref={messageRef}
id={message._id}
role='listitem'
aria-roledescription={t('message')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { memo } from 'react';

import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction';
import { useIsMessageHighlight } from '../../../views/room/MessageList/contexts/MessageHighlightContext';
import { useJumpToMessage } from '../../../views/room/MessageList/hooks/useJumpToMessage';
import Emoji from '../../Emoji';
import IgnoredContent from '../IgnoredContent';
import MessageHeader from '../MessageHeader';
Expand All @@ -33,15 +32,12 @@ const ThreadMessage = ({ message, sequential, unread, showUserAvatar }: ThreadMe
// Checks if is videoconf message to limit toolbox actions
const messageContext: MessageActionContext = isVideoConfMessage(message) ? 'videoconf-threads' : 'threads';

const messageRef = useJumpToMessage(message._id);

return (
<Message
role='listitem'
aria-roledescription={t('thread_message')}
tabIndex={0}
id={message._id}
ref={messageRef}
isEditing={editing}
isPending={message.temp}
sequential={sequential}
Expand Down
185 changes: 156 additions & 29 deletions apps/meteor/client/views/room/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,181 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { isThreadMessage } from '@rocket.chat/core-typings';
import { MessageTypes } from '@rocket.chat/message-types';
import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts';
import type { ComponentProps } from 'react';
import { Fragment } from 'react';
import type { MutableRefObject } from 'react';
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { VirtualizerHandle } from 'virtua';
import { VList } from 'virtua';

import { MessageListItem } from './MessageListItem';
import { useRoomSubscription } from '../contexts/RoomContext';
import { useFirstUnreadMessageId } from '../hooks/useFirstUnreadMessageId';
import { SelectedMessagesProvider } from '../providers/SelectedMessagesProvider';
import { useMessages } from './hooks/useMessages';
import { useMessages } from './hooks/useMessages';import useTryToJumpToMessage from './hooks/useTryToJumpToMessage';

Check failure on line 15 in apps/meteor/client/views/room/MessageList/MessageList.tsx

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Insert `⏎`
import { isMessageSequential } from './lib/isMessageSequential';
import MessageListProvider from './providers/MessageListProvider';
import { RoomManager } from '../../../lib/RoomManager';
import LoadingMessagesIndicator from '../body/LoadingMessagesIndicator';
import RetentionPolicyWarning from '../body/RetentionPolicyWarning';
import RoomForeword from '../body/RoomForeword/RoomForeword';
import { useStoreScrollPosition } from '../body/hooks/useStoreScrollPosition';

type MessageListProps = {
rid: IRoom['_id'];
messageListRef: ComponentProps<typeof MessageListProvider>['messageListRef'];
canPreview: boolean;
hasMorePreviousMessages: boolean;
isLoadingMoreMessages: boolean;
user: IUser | null;
room: IRoom;
retentionPolicy: RetentionPolicy | undefined;
hasMoreNextMessages: boolean;
shouldJumpToBottom: MutableRefObject<boolean>;
isAtBottom: MutableRefObject<boolean>;
isJumpingToMessage: MutableRefObject<boolean>;
};

export const MessageList = function MessageList({ rid, messageListRef }: MessageListProps) {
const lastViewportSize = 0;

export const MessageList = function MessageList({
rid,
canPreview,
hasMorePreviousMessages,
isLoadingMoreMessages,
user,
room,
retentionPolicy,
hasMoreNextMessages,
shouldJumpToBottom,
isAtBottom,
isJumpingToMessage,
}: MessageListProps) {
// Prepend ref needed for adjusting the message list shift
// https://inokawa.github.io/virtua/?path=/story/advanced-chat--default
const isPrepend = useRef<boolean>(false);
useLayoutEffect(() => {
isPrepend.current = false;
});

const virtualizerRef = useRef<VirtualizerHandle | null>(null);

const messages = useMessages({ rid });

useTryToJumpToMessage({ rid, virtualizerRef, isJumpingToMessage, messages });

const handlePrepend = useCallback(
(offset: number) => {
// If the offset is less than 200, it means the user is reaching the top of the list,
// so the prepend need to be enabled for smooth scrolling,
// if the prepend is enabled when a new message is added, the list will misalign.
if (offset < 200) {
isPrepend.current = true;
}

isAtBottom.current = offset - (virtualizerRef.current?.scrollSize ?? 0) + (virtualizerRef.current?.viewportSize ?? 0) >= -20;

if (shouldJumpToBottom.current && isAtBottom.current) {
shouldJumpToBottom.current = false;
}
},
[isAtBottom, shouldJumpToBottom],
);

const isRoomInitialized = useRef<boolean>(false);

// Scroll to bottom
useEffect(() => {
if (isJumpingToMessage.current) {
return;
}

if (!isRoomInitialized.current) {
const store = RoomManager.getStore(rid);
if (!store?.atBottom && store?.scroll) {
shouldJumpToBottom.current = false;
virtualizerRef.current?.scrollTo(store?.scroll);
console.log('room initialized');
isRoomInitialized.current = true;
return;
}
isRoomInitialized.current = true;
}

const handle = virtualizerRef.current;
const lastItemIndex = messages.length - 1;
if (shouldJumpToBottom.current === true) {
console.log('scroll to bottom, 1');
// When new messages arrive, this effect is triggered, but the latest message is not on the index, so it scrolls to the previous index
// TODO: Find if there is a better way to scroll to the latest message
handle?.scrollToIndex(lastItemIndex + 1, {
align: 'end',
});
}
// If new messages arrive and is at bottom, scroll to keep at bottom
if (isAtBottom.current && lastViewportSize !== handle?.viewportSize) {
console.log('scroll to bottom, 2');
handle?.scrollToIndex(lastItemIndex + 1, {
align: 'end',
});
}
}, [isAtBottom, messages, shouldJumpToBottom, isJumpingToMessage, rid]);

const storeScrollPosition = useStoreScrollPosition({ rid, isAtBottom, virtualizerRef });

const subscription = useRoomSubscription();
const showUserAvatar = !!useUserPreference<boolean>('displayAvatars');
const messageGroupingPeriod = useSetting('Message_GroupingPeriod', 300);
const firstUnreadMessageId = useFirstUnreadMessageId();

const { t } = useTranslation();
return (
<MessageListProvider messageListRef={messageListRef}>
<MessageListProvider>
<SelectedMessagesProvider>
{messages.map((message, index, { [index - 1]: previous }) => {
const sequential = isMessageSequential(message, previous, messageGroupingPeriod);
const showUnreadDivider = firstUnreadMessageId === message._id;
const system = MessageTypes.isSystemMessage(message);
const visible = !isThreadMessage(message) && !system;

return (
<Fragment key={message._id}>
<MessageListItem
message={message}
previous={previous}
showUnreadDivider={showUnreadDivider}
showUserAvatar={showUserAvatar}
sequential={sequential}
visible={visible}
subscription={subscription}
system={system}
/>
</Fragment>
);
})}
<VList
ref={virtualizerRef}
shift={isPrepend.current === true}
style={{ height: '100%' }}
aria-label={t('Message_list')}
aria-busy={isLoadingMoreMessages}
onScroll={(offset: number) => {
handlePrepend(offset);
storeScrollPosition();
}}
>
{canPreview ? (
<>
{hasMorePreviousMessages ? (
<li className='load-more'>{isLoadingMoreMessages ? <LoadingMessagesIndicator /> : null}</li>
) : (
<li>
<RoomForeword user={user} room={room} />
{retentionPolicy?.isActive ? <RetentionPolicyWarning room={room} /> : null}
</li>
)}
</>
) : null}
{messages.map((message, index, { [index - 1]: previous }) => {
const sequential = isMessageSequential(message, previous, messageGroupingPeriod);
const showUnreadDivider = firstUnreadMessageId === message._id;
const system = MessageTypes.isSystemMessage(message);
const visible = !isThreadMessage(message) && !system;

return (
<Fragment key={message._id}>
<MessageListItem
message={message}
previous={previous}
showUnreadDivider={showUnreadDivider}
showUserAvatar={showUserAvatar}
sequential={sequential}
visible={visible}
subscription={subscription}
system={system}
/>
</Fragment>
);
})}
{hasMoreNextMessages ? <li className='load-more'>{isLoadingMoreMessages ? <LoadingMessagesIndicator /> : null}</li> : null}
</VList>
</SelectedMessagesProvider>
</MessageListProvider>
);
Expand Down
Loading
Loading