Skip to content

Commit 341966e

Browse files
committed
fix: refactor MentionsTab, StarredMessagesTab, and PinnedMessagesTab to use useInfiniteQuery
Replaces blocking sequential for-loop fetching with useInfiniteQuery for MentionsTab, StarredMessagesTab, and PinnedMessagesTab components. Before: All pages were fetched sequentially in a for loop inside useQuery, blocking the UI until every page was loaded. After: Only the first page is fetched initially. Additional pages are loaded on-demand as the user scrolls to the bottom of the list using Virtuoso's endReached callback. Changes: - MentionsTab: useQuery -> useInfiniteQuery with page-by-page fetching - StarredMessagesTab: useQuery -> useInfiniteQuery with page-by-page fetching - PinnedMessagesTab: useQuery -> useInfiniteQuery with page-by-page fetching - MessageListTab: Accept individual props instead of UseQueryResult, add endReached prop to Virtuoso, show loading indicator for next page - Use isFetching guard in handleEndReached to prevent concurrent fetches Fixes #39237
1 parent b59da7e commit 341966e

4 files changed

Lines changed: 125 additions & 51 deletions

File tree

apps/meteor/client/views/room/contextualBar/MentionsTab.tsx

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,53 @@
11
import type { IMessage } from '@rocket.chat/core-typings';
22
import { useEndpoint } from '@rocket.chat/ui-contexts';
3-
import { useQuery } from '@tanstack/react-query';
3+
import { useInfiniteQuery } from '@tanstack/react-query';
44
import type { ReactElement } from 'react';
5+
import { useCallback, useMemo } from 'react';
56
import { useTranslation } from 'react-i18next';
67

78
import MessageListTab from './MessageListTab';
89
import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi';
910
import { useRoom } from '../contexts/RoomContext';
1011

12+
const COUNT = 50;
13+
1114
const MentionsTab = (): ReactElement => {
1215
const getMentionedMessages = useEndpoint('GET', '/v1/chat.getMentionedMessages');
1316

1417
const room = useRoom();
1518

16-
const mentionedMessagesQueryResult = useQuery({
19+
const mentionedMessagesQueryResult = useInfiniteQuery({
1720
queryKey: ['rooms', room._id, 'mentioned-messages'] as const,
1821

19-
queryFn: async () => {
20-
const messages: IMessage[] = [];
22+
queryFn: async ({ pageParam }) => {
23+
const result = await getMentionedMessages({ roomId: room._id, offset: pageParam, count: COUNT });
24+
return {
25+
messages: result.messages.map(mapMessageFromApi),
26+
total: result.total,
27+
count: result.count,
28+
offset: pageParam,
29+
};
30+
},
2131

22-
for (
23-
let offset = 0, result = await getMentionedMessages({ roomId: room._id, offset: 0 });
24-
result.count > 0;
25-
offset += result.count, result = await getMentionedMessages({ roomId: room._id, offset })
26-
) {
27-
messages.push(...result.messages.map(mapMessageFromApi));
28-
}
32+
initialPageParam: 0,
2933

30-
return messages;
34+
getNextPageParam: (lastPage) => {
35+
const nextOffset = lastPage.offset + lastPage.count;
36+
return nextOffset < lastPage.total ? nextOffset : undefined;
3137
},
3238
});
3339

40+
const messages = useMemo(
41+
() => mentionedMessagesQueryResult.data?.pages.flatMap((page) => page.messages) ?? [],
42+
[mentionedMessagesQueryResult.data],
43+
);
44+
45+
const handleEndReached = useCallback(() => {
46+
if (mentionedMessagesQueryResult.hasNextPage && !mentionedMessagesQueryResult.isFetching) {
47+
mentionedMessagesQueryResult.fetchNextPage();
48+
}
49+
}, [mentionedMessagesQueryResult]);
50+
3451
const { t } = useTranslation();
3552

3653
return (
@@ -39,7 +56,11 @@ const MentionsTab = (): ReactElement => {
3956
title={t('Mentions')}
4057
emptyResultMessage={t('No_mentions_found')}
4158
context='mentions'
42-
queryResult={mentionedMessagesQueryResult}
59+
messages={messages}
60+
isLoading={mentionedMessagesQueryResult.isLoading}
61+
isSuccess={mentionedMessagesQueryResult.isSuccess}
62+
isFetchingNextPage={mentionedMessagesQueryResult.isFetchingNextPage}
63+
onEndReached={handleEndReached}
4364
/>
4465
);
4566
};

apps/meteor/client/views/room/contextualBar/MessageListTab.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
ContextualbarDialog,
1414
} from '@rocket.chat/ui-client';
1515
import { useUserPreference, useRoomToolbox } from '@rocket.chat/ui-contexts';
16-
import type { UseQueryResult } from '@tanstack/react-query';
1716
import type { ReactElement, ReactNode } from 'react';
1817
import { useCallback } from 'react';
1918
import { Virtuoso } from 'react-virtuoso';
@@ -32,10 +31,14 @@ type MessageListTabProps = {
3231
title: ReactNode;
3332
emptyResultMessage: string;
3433
context: MessageActionContext;
35-
queryResult: UseQueryResult<IMessage[]>;
34+
messages: IMessage[];
35+
isLoading: boolean;
36+
isSuccess: boolean;
37+
isFetchingNextPage?: boolean;
38+
onEndReached?: () => void;
3639
};
3740

38-
const MessageListTab = ({ iconName, title, emptyResultMessage, context, queryResult }: MessageListTabProps): ReactElement => {
41+
const MessageListTab = ({ iconName, title, emptyResultMessage, context, messages, isLoading, isSuccess, isFetchingNextPage, onEndReached }: MessageListTabProps): ReactElement => {
3942
const formatDate = useFormatDate();
4043
const showUserAvatar = !!useUserPreference<boolean>('displayAvatars');
4144

@@ -54,26 +57,27 @@ const MessageListTab = ({ iconName, title, emptyResultMessage, context, queryRes
5457
<ContextualbarClose onClick={handleTabBarCloseButtonClick} />
5558
</ContextualbarHeader>
5659
<ContextualbarContent flexShrink={1} flexGrow={1} paddingInline={0}>
57-
{queryResult.isLoading && (
60+
{isLoading && (
5861
<Box paddingInline={24} paddingBlock={12}>
5962
<Throbber size='x12' />
6063
</Box>
6164
)}
62-
{queryResult.isSuccess && (
65+
{isSuccess && (
6366
<>
64-
{queryResult.data.length === 0 && <ContextualbarEmptyContent title={emptyResultMessage} />}
67+
{messages.length === 0 && <ContextualbarEmptyContent title={emptyResultMessage} />}
6568

66-
{queryResult.data.length > 0 && (
69+
{messages.length > 0 && (
6770
<MessageListErrorBoundary>
6871
<MessageListProvider>
6972
<Box is='section' display='flex' flexDirection='column' flexGrow={1} flexShrink={1} flexBasis='auto' height='full'>
7073
<VirtualizedScrollbars>
7174
<Virtuoso
72-
totalCount={queryResult.data.length}
75+
totalCount={messages.length}
7376
overscan={25}
74-
data={queryResult.data}
77+
data={messages}
78+
endReached={onEndReached}
7579
itemContent={(index, message) => {
76-
const previous = queryResult.data[index - 1];
80+
const previous = messages[index - 1];
7781

7882
const newDay = isMessageNewDay(message, previous);
7983

@@ -111,6 +115,11 @@ const MessageListTab = ({ iconName, title, emptyResultMessage, context, queryRes
111115
)}
112116
</>
113117
)}
118+
{isFetchingNextPage && (
119+
<Box paddingInline={24} paddingBlock={8}>
120+
<Throbber size='x12' />
121+
</Box>
122+
)}
114123
</ContextualbarContent>
115124
</ContextualbarDialog>
116125
);

apps/meteor/client/views/room/contextualBar/PinnedMessagesTab.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,55 @@
11
import type { IMessage } from '@rocket.chat/core-typings';
22
import { useEndpoint } from '@rocket.chat/ui-contexts';
3-
import { useQuery } from '@tanstack/react-query';
3+
import { useInfiniteQuery } from '@tanstack/react-query';
44
import type { ReactElement } from 'react';
5+
import { useCallback, useMemo } from 'react';
56
import { useTranslation } from 'react-i18next';
67

78
import MessageListTab from './MessageListTab';
89
import { onClientMessageReceived } from '../../../lib/onClientMessageReceived';
910
import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi';
1011
import { useRoom } from '../contexts/RoomContext';
1112

13+
const COUNT = 50;
14+
1215
const PinnedMessagesTab = (): ReactElement => {
1316
const getPinnedMessages = useEndpoint('GET', '/v1/chat.getPinnedMessages');
1417

1518
const room = useRoom();
1619

17-
const pinnedMessagesQueryResult = useQuery({
20+
const pinnedMessagesQueryResult = useInfiniteQuery({
1821
queryKey: ['rooms', room._id, 'pinned-messages'] as const,
1922

20-
queryFn: async () => {
21-
const messages: IMessage[] = [];
23+
queryFn: async ({ pageParam }) => {
24+
const result = await getPinnedMessages({ roomId: room._id, offset: pageParam, count: COUNT });
25+
const processedMessages = await Promise.all(result.messages.map(mapMessageFromApi).map(onClientMessageReceived));
26+
return {
27+
messages: processedMessages,
28+
total: result.total,
29+
count: result.count,
30+
offset: pageParam,
31+
};
32+
},
2233

23-
for (
24-
let offset = 0, result = await getPinnedMessages({ roomId: room._id, offset: 0 });
25-
result.count > 0;
26-
offset += result.count, result = await getPinnedMessages({ roomId: room._id, offset })
27-
) {
28-
messages.push(...result.messages.map(mapMessageFromApi));
29-
}
34+
initialPageParam: 0,
3035

31-
return Promise.all(messages.map(onClientMessageReceived));
36+
getNextPageParam: (lastPage) => {
37+
const nextOffset = lastPage.offset + lastPage.count;
38+
return nextOffset < lastPage.total ? nextOffset : undefined;
3239
},
3340
});
3441

42+
const messages = useMemo(
43+
() => pinnedMessagesQueryResult.data?.pages.flatMap((page) => page.messages) ?? [],
44+
[pinnedMessagesQueryResult.data],
45+
);
46+
47+
const handleEndReached = useCallback(() => {
48+
if (pinnedMessagesQueryResult.hasNextPage && !pinnedMessagesQueryResult.isFetching) {
49+
pinnedMessagesQueryResult.fetchNextPage();
50+
}
51+
}, [pinnedMessagesQueryResult]);
52+
3553
const { t } = useTranslation();
3654

3755
return (
@@ -40,7 +58,11 @@ const PinnedMessagesTab = (): ReactElement => {
4058
title={t('Pinned_Messages')}
4159
emptyResultMessage={t('No_pinned_messages')}
4260
context='pinned'
43-
queryResult={pinnedMessagesQueryResult}
61+
messages={messages}
62+
isLoading={pinnedMessagesQueryResult.isLoading}
63+
isSuccess={pinnedMessagesQueryResult.isSuccess}
64+
isFetchingNextPage={pinnedMessagesQueryResult.isFetchingNextPage}
65+
onEndReached={handleEndReached}
4466
/>
4567
);
4668
};

apps/meteor/client/views/room/contextualBar/StarredMessagesTab.tsx

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IMessage } from '@rocket.chat/core-typings';
22
import { useEndpoint } from '@rocket.chat/ui-contexts';
3-
import { useQuery } from '@tanstack/react-query';
3+
import { useInfiniteQuery } from '@tanstack/react-query';
4+
import { useCallback, useMemo } from 'react';
45
import { useTranslation } from 'react-i18next';
56

67
import MessageListTab from './MessageListTab';
@@ -9,28 +10,45 @@ import { roomsQueryKeys } from '../../../lib/queryKeys';
910
import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi';
1011
import { useRoom } from '../contexts/RoomContext';
1112

13+
const COUNT = 50;
14+
1215
const StarredMessagesTab = () => {
1316
const getStarredMessages = useEndpoint('GET', '/v1/chat.getStarredMessages');
1417

1518
const room = useRoom();
1619

17-
const starredMessagesQueryResult = useQuery({
20+
const starredMessagesQueryResult = useInfiniteQuery({
1821
queryKey: roomsQueryKeys.starredMessages(room._id),
19-
queryFn: async () => {
20-
const messages: IMessage[] = [];
21-
22-
for (
23-
let offset = 0, result = await getStarredMessages({ roomId: room._id, offset: 0 });
24-
result.count > 0;
25-
offset += result.count, result = await getStarredMessages({ roomId: room._id, offset })
26-
) {
27-
messages.push(...result.messages.map(mapMessageFromApi));
28-
}
29-
30-
return Promise.all(messages.map(onClientMessageReceived));
22+
queryFn: async ({ pageParam }) => {
23+
const result = await getStarredMessages({ roomId: room._id, offset: pageParam, count: COUNT });
24+
const processedMessages = await Promise.all(result.messages.map(mapMessageFromApi).map(onClientMessageReceived));
25+
return {
26+
messages: processedMessages,
27+
total: result.total,
28+
count: result.count,
29+
offset: pageParam,
30+
};
31+
},
32+
33+
initialPageParam: 0,
34+
35+
getNextPageParam: (lastPage) => {
36+
const nextOffset = lastPage.offset + lastPage.count;
37+
return nextOffset < lastPage.total ? nextOffset : undefined;
3138
},
3239
});
3340

41+
const messages = useMemo(
42+
() => starredMessagesQueryResult.data?.pages.flatMap((page) => page.messages) ?? [],
43+
[starredMessagesQueryResult.data],
44+
);
45+
46+
const handleEndReached = useCallback(() => {
47+
if (starredMessagesQueryResult.hasNextPage && !starredMessagesQueryResult.isFetching) {
48+
starredMessagesQueryResult.fetchNextPage();
49+
}
50+
}, [starredMessagesQueryResult]);
51+
3452
const { t } = useTranslation();
3553

3654
return (
@@ -39,7 +57,11 @@ const StarredMessagesTab = () => {
3957
title={t('Starred_Messages')}
4058
emptyResultMessage={t('No_starred_messages')}
4159
context='starred'
42-
queryResult={starredMessagesQueryResult}
60+
messages={messages}
61+
isLoading={starredMessagesQueryResult.isLoading}
62+
isSuccess={starredMessagesQueryResult.isSuccess}
63+
isFetchingNextPage={starredMessagesQueryResult.isFetchingNextPage}
64+
onEndReached={handleEndReached}
4365
/>
4466
);
4567
};

0 commit comments

Comments
 (0)