Skip to content
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
65 changes: 52 additions & 13 deletions apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<IMessage> = 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;
Expand All @@ -69,6 +75,10 @@ class RoomHistoryManagerClass extends Emitter {
firstUnread: ReactiveVar<IMessage | undefined>;
loaded: number | undefined;
oldestTs?: Date;
scroll?: {
scrollHeight: number;
scrollTop: number;
};
}
> = {};

Expand Down Expand Up @@ -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;
}
Expand All @@ -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<boolean>) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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 });

Expand All @@ -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');
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, MutableRefObject } from 'react';
import type { KeyboardEvent, MouseEvent, RefCallback } from 'react';
import { createContext, useContext } from 'react';

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

export const MessageListContext = createContext<MessageListContextValue>({
Expand Down Expand Up @@ -73,7 +73,7 @@ export const MessageListContext = createContext<MessageListContextValue>({
formatDateAndTime: () => '',
formatTime: () => '',
formatDate: () => '',
messageListRef: { current: undefined },
messageListRef: undefined,
});

export const useShowTranslated: MessageListContextValue['autoTranslate']['showAutoTranslate'] = (...args) =>
Expand Down
6 changes: 2 additions & 4 deletions apps/meteor/client/lib/utils/legacyJumpToMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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',
Expand Down Expand Up @@ -60,7 +63,7 @@ export const useJumpToMessage = (messageId: IMessage['_id']) => {
return () => {
observer.disconnect();
if (listRef) {
listRef.current = undefined;
setRef(listRef, undefined);
}
};
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,7 +18,7 @@ import { useLoadSurroundingMessages } from '../hooks/useLoadSurroundingMessages'

type MessageListProviderProps = {
children: ReactNode;
messageListRef?: MutableRefObject<HTMLElement | undefined>;
messageListRef?: RefCallback<HTMLElement | undefined>;
attachmentDimension?: {
width?: number;
height?: number;
Expand Down
13 changes: 11 additions & 2 deletions apps/meteor/client/views/room/body/RoomBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 11 additions & 2 deletions apps/meteor/client/views/room/body/RoomBodyV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jest.mock('../../../../../app/ui-utils/client', () => ({
hasMoreNext: jest.fn(),
getMore: jest.fn(),
getMoreNext: jest.fn(),
restoreScroll: jest.fn(),
},
}));

Expand Down
36 changes: 28 additions & 8 deletions apps/meteor/client/views/room/body/hooks/useGetMore.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,64 @@
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';

export const useGetMore = (rid: string, atBottomRef: MutableRefObject<boolean>) => {
const ref = useRef<HTMLElement>(null);

const jumpToRef = useRef<HTMLElement>(undefined);

useEffect(() => {
if (!ref.current) {
return;
}

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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import { useCallback } from 'react';
const isRefCallback = <T>(x: unknown): x is RefCallback<T> => typeof x === 'function';
const isMutableRefObject = <T>(x: unknown): x is MutableRefObject<T> => typeof x === 'object';

export const setRef = <T>(ref: Ref<T> | undefined, refValue: T) => {
if (isRefCallback<T>(ref)) {
ref(refValue);
return;
}

if (isMutableRefObject<T>(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
Expand All @@ -13,16 +24,7 @@ const isMutableRefObject = <T>(x: unknown): x is MutableRefObject<T> => typeof x
*/
export const useMessageComposerMergedRefs = <T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> => {
return useCallback((refValue: T) => {
refs.forEach((ref) => {
if (isRefCallback<T>(ref)) {
ref(refValue);
return;
}

if (isMutableRefObject<T>(ref)) {
ref.current = refValue;
}
});
refs.forEach((ref) => setRef(ref, refValue));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, refs);
};
Loading
Loading