Skip to content

Commit fb70268

Browse files
authored
feat: Improvements for virtual rendering (#19448)
* feat: Improvements for virutal rendering * improvements
1 parent 431ec0d commit fb70268

File tree

6 files changed

+164
-145
lines changed

6 files changed

+164
-145
lines changed

src/script/components/MessagesList/VirtualizedMessageListWrapper.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {VirtualizedMessagesList} from 'Components/MessagesList/VirtualizedMessag
2828
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
2929

3030
import {MessagesListParams} from './MessageList.types';
31-
import {useLoadConversation} from './utils/useLoadConversation';
3231

3332
export const VirtualizedMessageListWrapper = ({
3433
assetRepository,
@@ -59,14 +58,6 @@ export const VirtualizedMessageListWrapper = ({
5958
const parentRef = useRef<HTMLDivElement | null>(null);
6059
const conversationLastReadTimestamp = useRef<number>(lastReadTimestamp);
6160

62-
// Hook for load current conversation
63-
const {loadConversation} = useLoadConversation({
64-
conversation,
65-
conversationRepository,
66-
conversationLastReadTimestamp,
67-
onLoading,
68-
});
69-
7061
return (
7162
<FadingScrollbar
7263
ref={parentRef}
@@ -81,7 +72,7 @@ export const VirtualizedMessageListWrapper = ({
8172
</div>
8273
)}
8374

84-
{isConversationLoaded && parentRef.current && (
75+
{parentRef.current && (
8576
<VirtualizedMessagesList
8677
parentElement={parentRef.current}
8778
conversationLastReadTimestamp={conversationLastReadTimestamp}
@@ -104,7 +95,8 @@ export const VirtualizedMessageListWrapper = ({
10495
isMsgElementsFocusable={isMsgElementsFocusable}
10596
setMsgElementsFocusable={setMsgElementsFocusable}
10697
updateConversationLastRead={updateConversationLastRead}
107-
loadConversation={loadConversation}
98+
onLoading={onLoading}
99+
isConversationLoaded={isConversationLoaded}
108100
/>
109101
)}
110102
</FadingScrollbar>

src/script/components/MessagesList/VirtualizedMessagesList/VirtualizedMessagesList.tsx

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
*/
1919

20-
import {MutableRefObject, useEffect, useMemo, useState} from 'react';
20+
import {MutableRefObject, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
2121

2222
import {useVirtualizer} from '@tanstack/react-virtual';
2323
import cx from 'classnames';
@@ -28,22 +28,22 @@ import {MessagesListParams} from 'Components/MessagesList/MessageList.types';
2828
import {UploadAssets} from 'Components/MessagesList/UploadAssets';
2929
import {verticallyCenterMessage} from 'Components/MessagesList/utils/helpers';
3030
import {filterMessages} from 'Components/MessagesList/utils/messagesFilter';
31+
import {useLoadConversation} from 'Components/MessagesList/utils/useLoadConversation';
32+
import {useLoadInitialMessage} from 'Components/MessagesList/utils/useLoadInitialMessage';
3133
import {groupMessagesBySenderAndTime, isMarker} from 'Components/MessagesList/utils/virtualizedMessagesGroup';
3234
import {useLoadMessages} from 'Components/MessagesList/VirtualizedMessagesList/useLoadMessages';
3335
import {useScrollMessages} from 'Components/MessagesList/VirtualizedMessagesList/useScrollMessages';
3436
import {useRoveFocus} from 'Hooks/useRoveFocus';
35-
import {Conversation} from 'Repositories/entity/Conversation';
36-
import {Message as MessageEntity} from 'Repositories/entity/message/Message';
3737
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
3838

3939
import {VirtualizedJumpToLastMessageButton} from '../VirtualizedJumpToLastMessageButton';
4040

4141
const ESTIMATED_ELEMENT_SIZE = 48;
4242

43-
interface Props extends Omit<MessagesListParams, 'isRightSidebarOpen' | 'onLoading' | 'isConversationLoaded'> {
43+
interface Props extends Omit<MessagesListParams, 'isRightSidebarOpen' | 'onLoading'> {
4444
parentElement: HTMLDivElement;
4545
conversationLastReadTimestamp: MutableRefObject<number>;
46-
loadConversation: (conversation: Conversation) => Promise<MessageEntity[]>;
46+
onLoading: (isLoading: boolean) => void;
4747
}
4848

4949
export const VirtualizedMessagesList = ({
@@ -68,7 +68,8 @@ export const VirtualizedMessagesList = ({
6868
setMsgElementsFocusable,
6969
conversationLastReadTimestamp,
7070
updateConversationLastRead,
71-
loadConversation,
71+
onLoading,
72+
isConversationLoaded,
7273
}: Props) => {
7374
const {
7475
messages: allMessages,
@@ -77,13 +78,17 @@ export const VirtualizedMessagesList = ({
7778
isGuestAndServicesRoom,
7879
isActiveParticipant,
7980
inTeam,
81+
isLoadingMessages,
82+
hasAdditionalMessages,
8083
} = useKoSubscribableChildren(conversation, [
8184
'inTeam',
8285
'isActiveParticipant',
8386
'messages',
8487
'lastDeliveredMessage',
8588
'isGuestRoom',
8689
'isGuestAndServicesRoom',
90+
'isLoadingMessages',
91+
'hasAdditionalMessages',
8792
]);
8893

8994
const filteredMessages = filterMessages(allMessages);
@@ -92,26 +97,8 @@ export const VirtualizedMessagesList = ({
9297
return groupMessagesBySenderAndTime(filteredMessages, conversationLastReadTimestamp.current);
9398
}, [conversationLastReadTimestamp, filteredMessages]);
9499

95-
const initialMessageId = conversation.initialMessage()?.id;
96-
97-
const [highlightedMessage, setHighlightedMessage] = useState<string | undefined>(undefined);
100+
const [highlightedMessage, setHighlightedMessage] = useState<string | undefined>(conversation.initialMessage()?.id);
98101
const [loadingMessages, setLoadingMessages] = useState(false);
99-
const [alreadyScrolledToLastMessage, setAlreadyScrolledToLastMessage] = useState(false);
100-
101-
useEffect(() => {
102-
if (!initialMessageId) {
103-
return () => undefined;
104-
}
105-
106-
setHighlightedMessage(initialMessageId);
107-
108-
const timeout = setTimeout(() => {
109-
setHighlightedMessage(undefined);
110-
conversation.initialMessage(undefined);
111-
}, 3000);
112-
113-
return () => clearTimeout(timeout);
114-
}, [conversation, initialMessageId]);
115102

116103
const {focusedId, handleKeyDown, setFocusedId} = useRoveFocus(filteredMessages.map(message => message.id));
117104

@@ -122,24 +109,39 @@ export const VirtualizedMessagesList = ({
122109
getScrollElement: () => parentElement,
123110
estimateSize: () => ESTIMATED_ELEMENT_SIZE,
124111
measureElement: element => element?.getBoundingClientRect().height || ESTIMATED_ELEMENT_SIZE,
125-
overscan: 2,
126112
});
127113

114+
// Hook for load current conversation
115+
const {loadConversation} = useLoadConversation({
116+
conversation,
117+
conversationRepository,
118+
conversationLastReadTimestamp,
119+
onLoading,
120+
});
121+
122+
// Hook for loading the initial message ( it takes initial message from search, last unread message or last message )
123+
useLoadInitialMessage(virtualizer, {
124+
conversation,
125+
isConversationLoaded,
126+
allMessages,
127+
conversationLastReadTimestamp,
128+
});
129+
130+
// Hook for loading preceding / following messages when user scrolls to top / bottom
128131
useLoadMessages(virtualizer, {
129132
conversation,
130133
conversationRepository,
131134
loadingMessages,
132135
onLoadingMessages: setLoadingMessages,
133136
itemsLength: groupedMessages.length,
134-
initialMessageId,
137+
shouldPullMessages: !isLoadingMessages && hasAdditionalMessages,
135138
});
136139

140+
// Hook for scrolling messages when a new message is sent or received
137141
useScrollMessages(virtualizer, {
138142
messages: groupedMessages,
139-
highlightedMessage,
140143
userId: selfUser.id,
141-
conversationLastReadTimestamp,
142-
setAlreadyScrolledToLastMessage,
144+
isConversationLoaded,
143145
});
144146

145147
// When a new conversation is opened using keyboard(enter), focus on the last message
@@ -155,24 +157,41 @@ export const VirtualizedMessagesList = ({
155157
}
156158
}, []);
157159

160+
const scrolledToHighlightedMessage = useRef(false);
161+
158162
const onTimestampClick = async (messageId: string) => {
163+
scrolledToHighlightedMessage.current = false;
159164
setHighlightedMessage(messageId);
160165

161-
const highlightMessageTimeout = setTimeout(() => setHighlightedMessage(undefined), 3000);
162-
clearTimeout(highlightMessageTimeout);
166+
const clearHighlightedMessage = setTimeout(() => {
167+
setHighlightedMessage(undefined);
168+
scrolledToHighlightedMessage.current = false;
169+
clearTimeout(clearHighlightedMessage);
170+
}, 5000);
163171

164172
const messageIsLoaded = conversation.getMessage(messageId);
165173

166174
if (!messageIsLoaded) {
167175
const messageEntity = await messageRepository.getMessageInConversationById(conversation, messageId);
168176
conversation.removeMessages();
169-
conversationRepository.getMessagesWithOffset(conversation, messageEntity);
177+
void conversationRepository.getMessagesWithOffset(conversation, messageEntity);
170178
}
171179
};
172180

173-
const virtualItems = virtualizer.getVirtualItems();
174-
const lastIndex = groupedMessages.length - 1;
175-
const isLastMessageVisible = virtualItems.some(item => item.index === lastIndex);
181+
useLayoutEffect(() => {
182+
if (highlightedMessage && !scrolledToHighlightedMessage.current) {
183+
const highlightedMessageIndex = groupedMessages.findIndex(
184+
msg => !isMarker(msg) && msg.message.id === highlightedMessage,
185+
);
186+
187+
virtualizer.scrollToIndex(highlightedMessageIndex, {align: 'center'});
188+
189+
const setScrolledToHighlightedMessageTimeout = setTimeout(() => {
190+
scrolledToHighlightedMessage.current = true;
191+
clearTimeout(setScrolledToHighlightedMessageTimeout);
192+
}, 100);
193+
}
194+
}, [groupedMessages, highlightedMessage, virtualizer]);
176195

177196
const onJumpToLastMessageClick = async () => {
178197
setHighlightedMessage(undefined);
@@ -185,17 +204,22 @@ export const VirtualizedMessagesList = ({
185204
await loadConversation(conversation);
186205
}
187206

188-
conversationLastReadTimestamp.current = groupedMessages[groupedMessages.length - 1].timestamp;
207+
conversationLastReadTimestamp.current = allMessages[allMessages.length - 1].timestamp();
189208

190-
requestAnimationFrame(() => {
191-
virtualizer.scrollToIndex(groupedMessages.length - 1, {align: 'end'});
192-
});
209+
const scrollTimeout = setTimeout(() => {
210+
virtualizer.scrollToIndex(allMessages.length - 1, {align: 'end'});
211+
clearTimeout(scrollTimeout);
212+
}, 100);
193213
};
194214

215+
const virtualItems = virtualizer.getVirtualItems();
216+
const lastIndex = groupedMessages.length - 1;
217+
const isLastMessageVisible = virtualItems.some(item => item.index === lastIndex);
218+
195219
useEffect(() => {
196220
// Timeout to ensure that the messages are rendered before calling getVisibleCallback
197221
const timeout = setTimeout(() => {
198-
if (!alreadyScrolledToLastMessage) {
222+
if (!isConversationLoaded) {
199223
return;
200224
}
201225

@@ -208,7 +232,11 @@ export const VirtualizedMessagesList = ({
208232
}, 100);
209233

210234
return () => clearTimeout(timeout);
211-
}, [alreadyScrolledToLastMessage, virtualItems]);
235+
}, [isConversationLoaded, virtualItems]);
236+
237+
if (!isConversationLoaded) {
238+
return null;
239+
}
212240

213241
return (
214242
<>

src/script/components/MessagesList/VirtualizedMessagesList/useLoadMessages.ts

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,58 +23,40 @@ import {Virtualizer} from '@tanstack/react-virtual';
2323

2424
import {ConversationRepository} from 'Repositories/conversation/ConversationRepository';
2525
import {Conversation} from 'Repositories/entity/Conversation';
26-
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
2726
import {isLastReceivedMessage} from 'Util/conversationMessages';
2827

29-
const SCROLL_THRESHOLD = 10;
28+
const SCROLL_THRESHOLD = 100;
3029

3130
interface Props {
3231
conversation: Conversation;
3332
conversationRepository: ConversationRepository;
3433
loadingMessages: boolean;
3534
onLoadingMessages: (isLoading: boolean) => void;
3635
itemsLength: number;
37-
initialMessageId?: string;
36+
shouldPullMessages: boolean;
3837
}
3938

4039
export const useLoadMessages = (
4140
virtualizer: Virtualizer<HTMLDivElement, Element>,
42-
{conversation, conversationRepository, loadingMessages, onLoadingMessages, itemsLength, initialMessageId}: Props,
41+
{conversation, conversationRepository, loadingMessages, onLoadingMessages, itemsLength, shouldPullMessages}: Props,
4342
) => {
4443
const fillContainerByMessagesRef = useRef(false);
4544

46-
const {isLoadingMessages, hasAdditionalMessages} = useKoSubscribableChildren(conversation, [
47-
'isLoadingMessages',
48-
'hasAdditionalMessages',
49-
]);
50-
5145
const loadPrecedingMessages = useCallback(async () => {
52-
const shouldPullMessages = !isLoadingMessages && hasAdditionalMessages;
53-
5446
if (!shouldPullMessages) {
5547
return;
5648
}
5749

5850
try {
5951
onLoadingMessages(true);
6052
const newMessages = await conversationRepository.getPrecedingMessages(conversation);
61-
62-
if (!initialMessageId) {
63-
virtualizer.scrollToIndex(newMessages.length, {align: 'start'});
64-
}
53+
virtualizer.scrollToIndex(newMessages.length, {align: 'start'});
6554
} catch (error) {
6655
console.error('Error loading preceding messages:', error);
6756
} finally {
6857
onLoadingMessages(false);
6958
}
70-
}, [
71-
conversation,
72-
conversationRepository,
73-
hasAdditionalMessages,
74-
isLoadingMessages,
75-
onLoadingMessages,
76-
initialMessageId,
77-
]);
59+
}, [conversation, conversationRepository, shouldPullMessages, onLoadingMessages]);
7860

7961
const loadFollowingMessages = useCallback(async () => {
8062
const lastMessage = conversation.getNewestMessage();
@@ -92,16 +74,14 @@ export const useLoadMessages = (
9274
// if the last loaded message is not the last of the conversation, we load the subsequent messages
9375
const newMessages = await conversationRepository.getSubsequentMessages(conversation, lastMessage);
9476

95-
if (!initialMessageId) {
96-
const newIndex = itemsLength + newMessages.length;
97-
virtualizer.scrollToIndex(newIndex, {align: 'end'});
98-
}
77+
const newIndex = itemsLength + newMessages.length;
78+
virtualizer.scrollToIndex(newIndex, {align: 'end'});
9979
} catch (error) {
10080
console.error('Error loading following messages:', error);
10181
} finally {
10282
onLoadingMessages(false);
10383
}
104-
}, [itemsLength, conversation, conversationRepository, onLoadingMessages, initialMessageId]);
84+
}, [itemsLength, conversation, conversationRepository, onLoadingMessages]);
10585

10686
// This function ensures that after user scroll to top or bottom content,
10787
// the preceding / following messages will be loaded.

0 commit comments

Comments
 (0)