diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index db11d3ece34fc..9b8c29b086ea1 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -14,6 +14,8 @@ import { waitForElement } from '../../../../client/lib/utils/waitForElement'; import { Messages, Subscriptions } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; +const waitAfterFlush = () => new Promise((resolve) => Tracker.afterFlush(() => resolve(void 0))); + export async function upsertMessage( { msg, @@ -40,18 +42,22 @@ export async function upsertMessage( return collection.upsert({ _id }, msg); } -export function upsertMessageBulk( +export async function upsertMessageBulk( { msgs, subscription }: { msgs: IMessage[]; subscription?: ISubscription }, collection: MinimongoCollection = Messages, ) { const { queries } = collection; collection.queries = []; - msgs.forEach((msg, index) => { - if (index === msgs.length - 1) { - collection.queries = queries; - } - void upsertMessage({ msg, subscription }, collection); - }); + const lastMessage = msgs.pop(); + + for await (const msg of msgs) { + await upsertMessage({ msg, subscription }, collection); + } + + if (lastMessage) { + collection.queries = queries; + await upsertMessage({ msg: lastMessage, subscription }, collection); + } } const defaultLimit = parseInt(getConfig('roomListLimit') ?? '50') || 50; @@ -69,6 +75,10 @@ class RoomHistoryManagerClass extends Emitter { firstUnread: ReactiveVar; loaded: number | undefined; oldestTs?: Date; + scroll?: { + scrollHeight: number; + scrollTop: number; + }; } > = {}; @@ -162,13 +172,20 @@ class RoomHistoryManagerClass extends Emitter { room.oldestTs = messages[messages.length - 1].ts; } - await waitForElement('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + const wrapper = await waitForElement('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + + room.scroll = { + scrollHeight: wrapper.scrollHeight, + scrollTop: wrapper.scrollTop, + }; - upsertMessageBulk({ + await upsertMessageBulk({ msgs: messages.filter((msg) => msg.t !== 'command'), subscription, }); + this.emit('loaded-messages'); + if (!room.loaded) { room.loaded = 0; } @@ -185,7 +202,27 @@ class RoomHistoryManagerClass extends Emitter { return this.getMore(rid); } + this.emit('loaded-messages'); + room.isLoading.set(false); + await waitAfterFlush(); + } + + public restoreScroll(rid: IRoom['_id']) { + const room = this.getRoom(rid); + const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + + if (room.scroll === undefined) { + return; + } + + if (!wrapper) { + return; + } + + const heightDiff = wrapper.scrollHeight - (room.scroll.scrollHeight ?? NaN); + wrapper.scrollTop = room.scroll.scrollTop + heightDiff; + room.scroll = undefined; } public async getMoreNext(rid: IRoom['_id'], atBottomRef: MutableRefObject) { @@ -206,11 +243,13 @@ class RoomHistoryManagerClass extends Emitter { if (lastMessage?.ts) { const { ts } = lastMessage; const result = await callWithErrorHandling('loadNextMessages', rid, ts, defaultLimit); - upsertMessageBulk({ + await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription, }); + this.emit('loaded-messages'); + room.isLoading.set(false); if (!room.loaded) { room.loaded = 0; @@ -247,7 +286,7 @@ class RoomHistoryManagerClass extends Emitter { return room.isLoading.get(); } - public async clear(rid: IRoom['_id']) { + public clear(rid: IRoom['_id']) { const room = this.getRoom(rid); Messages.remove({ rid }); room.isLoading.set(true); @@ -269,7 +308,7 @@ class RoomHistoryManagerClass extends Emitter { } const room = this.getRoom(message.rid); - void this.clear(message.rid); + this.clear(message.rid); const subscription = Subscriptions.findOne({ rid: message.rid }); @@ -279,7 +318,7 @@ class RoomHistoryManagerClass extends Emitter { return; } - upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); + await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); Tracker.afterFlush(async () => { this.emit('loaded-messages'); diff --git a/apps/meteor/client/components/message/list/MessageListContext.tsx b/apps/meteor/client/components/message/list/MessageListContext.tsx index 6ce88e9b3033a..45a1e4239ff0c 100644 --- a/apps/meteor/client/components/message/list/MessageListContext.tsx +++ b/apps/meteor/client/components/message/list/MessageListContext.tsx @@ -1,5 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import type { KeyboardEvent, MouseEvent, MutableRefObject } from 'react'; +import type { KeyboardEvent, MouseEvent, RefCallback } from 'react'; import { createContext, useContext } from 'react'; import type { useFormatDate } from '../../../hooks/useFormatDate'; @@ -42,7 +42,7 @@ export type MessageListContextValue = { formatDateAndTime: ReturnType; formatTime: ReturnType; formatDate: ReturnType; - messageListRef?: MutableRefObject; + messageListRef?: RefCallback; }; export const MessageListContext = createContext({ @@ -73,7 +73,7 @@ export const MessageListContext = createContext({ formatDateAndTime: () => '', formatTime: () => '', formatDate: () => '', - messageListRef: { current: undefined }, + messageListRef: undefined, }); export const useShowTranslated: MessageListContextValue['autoTranslate']['showAutoTranslate'] = (...args) => diff --git a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts index 9ae917e8cae9a..b972f93bbbd52 100644 --- a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts +++ b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts @@ -36,13 +36,11 @@ export const legacyJumpToMessage = async (message: IMessage) => { } if (RoomManager.opened === message.rid) { - RoomHistoryManager.getSurroundingMessages(message); + await RoomHistoryManager.getSurroundingMessages(message); return; } await goToRoomById(message.rid); - setTimeout(() => { - RoomHistoryManager.getSurroundingMessages(message); - }, 400); + await RoomHistoryManager.getSurroundingMessages(message); }; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts index 83f5c8230b25f..7ba684ea8eeea 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts @@ -4,6 +4,7 @@ import { useRouter } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; import { useMessageListJumpToMessageParam, useMessageListRef } from '../../../../components/message/list/MessageListContext'; +import { setRef } from '../../composer/hooks/useMessageComposerMergedRefs'; import { setHighlightMessage, clearHighlightMessage } from '../providers/messageHighlightSubscription'; // this is an arbitrary value so that there's a gap between the header and the message; @@ -20,10 +21,12 @@ export const useJumpToMessage = (messageId: IMessage['_id']) => { return; } - if (listRef) { - listRef.current = node; + if (!listRef) { + return; } + setRef(listRef, node); + node.scrollIntoView({ behavior: 'smooth', block: 'center', @@ -60,7 +63,7 @@ export const useJumpToMessage = (messageId: IMessage['_id']) => { return () => { observer.disconnect(); if (listRef) { - listRef.current = undefined; + setRef(listRef, undefined); } }; }, diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index e67bd3a48405c..2a242a5e65084 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -1,6 +1,6 @@ import { isThreadMainMessage } from '@rocket.chat/core-typings'; import { useLayout, useUser, useUserPreference, useSetting, useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts'; -import type { MutableRefObject, ReactNode } from 'react'; +import type { ReactNode, RefCallback } from 'react'; import { useMemo, memo } from 'react'; import { getRegexHighlight, getRegexHighlightUrl } from '../../../../../app/highlight-words/client/helper'; @@ -18,7 +18,7 @@ import { useLoadSurroundingMessages } from '../hooks/useLoadSurroundingMessages' type MessageListProviderProps = { children: ReactNode; - messageListRef?: MutableRefObject; + messageListRef?: RefCallback; attachmentDimension?: { width?: number; height?: number; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index e4b64f35c5864..6be98ae7592f4 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -91,9 +91,18 @@ const RoomBody = (): ReactElement => { const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); - const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom, jumpToRef } = useListIsAtBottom(); + const { + innerRef: isAtBottomInnerRef, + atBottomRef, + sendToBottom, + sendToBottomIfNecessary, + isAtBottom, + jumpToRef: jumpToRefIsAtBottom, + } = useListIsAtBottom(); + + const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + const jumpToRef = useMergedRefs(jumpToRefGetMore, jumpToRefIsAtBottom); const { uploads, diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 2038163f82637..1b3c8cfb72300 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -91,9 +91,18 @@ const RoomBody = (): ReactElement => { const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); - const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom, jumpToRef } = useListIsAtBottom(); + const { + innerRef: isAtBottomInnerRef, + atBottomRef, + sendToBottom, + sendToBottomIfNecessary, + isAtBottom, + jumpToRef: jumpToRefIsAtBottom, + } = useListIsAtBottom(); + + const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + const jumpToRef = useMergedRefs(jumpToRefIsAtBottom, jumpToRefGetMore); const { uploads, diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts index ca289f7f5772b..e9e36dcd2d1da 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts @@ -11,6 +11,7 @@ jest.mock('../../../../../app/ui-utils/client', () => ({ hasMoreNext: jest.fn(), getMore: jest.fn(), getMoreNext: jest.fn(), + restoreScroll: jest.fn(), }, })); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index 32a6b1fb78e72..5c57bf12358df 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,5 +1,6 @@ import type { MutableRefObject } from 'react'; import { useEffect, useRef } from 'react'; +import { flushSync } from 'react-dom'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; @@ -7,6 +8,8 @@ import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { const ref = useRef(null); + const jumpToRef = useRef(undefined); + useEffect(() => { if (!ref.current) { return; @@ -14,31 +17,48 @@ export const useGetMore = (rid: string, atBottomRef: MutableRefObject) const refValue = ref.current; - const handleScroll = withThrottling({ wait: 100 })((event) => { + const handleScroll = withThrottling({ wait: 300 })(async (event) => { const lastScrollTopRef = event.target.scrollTop; const height = event.target.clientHeight; const isLoading = RoomHistoryManager.isLoading(rid); const hasMore = RoomHistoryManager.hasMore(rid); const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); - if ((isLoading === false && hasMore === true) || hasMoreNext === true) { - if (hasMore === true && lastScrollTopRef <= height / 3) { - RoomHistoryManager.getMore(rid); - } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { - RoomHistoryManager.getMoreNext(rid, atBottomRef); - atBottomRef.current = false; + if (!((isLoading === false && hasMore === true) || hasMoreNext === true)) { + return; + } + + if (jumpToRef.current) { + return; + } + + if (hasMore === true && lastScrollTopRef <= height / 3) { + await RoomHistoryManager.getMore(rid); + + if (jumpToRef.current) { + return; } + flushSync(() => { + RoomHistoryManager.restoreScroll(rid); + }); + } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { + RoomHistoryManager.getMoreNext(rid, atBottomRef); + atBottomRef.current = false; } }); - refValue.addEventListener('scroll', handleScroll); + refValue.addEventListener('scroll', handleScroll, { + passive: true, + }); return () => { + handleScroll.cancel(); refValue.removeEventListener('scroll', handleScroll); }; }, [rid, atBottomRef]); return { innerRef: ref, + jumpToRef, }; }; diff --git a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts index 02f129df3b6db..aea0318ce831d 100644 --- a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts +++ b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts @@ -4,6 +4,17 @@ import { useCallback } from 'react'; const isRefCallback = (x: unknown): x is RefCallback => typeof x === 'function'; const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x === 'object'; +export const setRef = (ref: Ref | undefined, refValue: T) => { + if (isRefCallback(ref)) { + ref(refValue); + return; + } + + if (isMutableRefObject(ref)) { + ref.current = refValue; + } +}; + /** * Merges multiple refs into a single ref callback. * it was not meant to be used with in any different place than MessageBox @@ -13,16 +24,7 @@ const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x */ export const useMessageComposerMergedRefs = (...refs: (Ref | undefined)[]): RefCallback => { return useCallback((refValue: T) => { - refs.forEach((ref) => { - if (isRefCallback(ref)) { - ref(refValue); - return; - } - - if (isMutableRefObject(ref)) { - ref.current = refValue; - } - }); + refs.forEach((ref) => setRef(ref, refValue)); // eslint-disable-next-line react-hooks/exhaustive-deps }, refs); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx index b52e8c7b0cc57..208065e4beab8 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -2,7 +2,7 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; import { differenceInSeconds } from 'date-fns'; -import type { ReactElement } from 'react'; +import type { ReactElement, RefCallback } from 'react'; import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; @@ -79,7 +79,7 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen ) : ( - + }> {[mainMessage, ...messages].map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); const newDay = isMessageNewDay(message, previous);