Skip to content

Commit fae5658

Browse files
Add "interracted with" patch
1 parent 50ee29c commit fae5658

File tree

1 file changed

+102
-40
lines changed

1 file changed

+102
-40
lines changed

src/components/MessageList/VirtualizedMessageList.tsx

+102-40
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import React, { RefObject, useCallback, useEffect, useMemo, useRef } from 'react';
1+
/* eslint-disable react/jsx-sort-props */
2+
import type { RefObject } from 'react';
3+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
24
import {
35
ComputeItemKey,
6+
FollowOutputScalarType,
47
ScrollSeekConfiguration,
58
ScrollSeekPlaceholderProps,
6-
Virtuoso,
79
VirtuosoHandle,
810
VirtuosoProps,
911
} from 'react-virtuoso';
12+
import { Virtuoso } from 'react-virtuoso';
1013

1114
import { GiphyPreviewMessage as DefaultGiphyPreviewMessage } from './GiphyPreviewMessage';
1215
import { useLastReadData } from './hooks';
@@ -231,7 +234,7 @@ const VirtualizedMessageListWithContext = <
231234
showUnreadNotificationAlways,
232235
sortReactionDetails,
233236
sortReactions,
234-
stickToBottomScrollBehavior = 'smooth',
237+
stickToBottomScrollBehavior = 'auto',
235238
suppressAutoscroll,
236239
threadList,
237240
} = props;
@@ -263,6 +266,13 @@ const VirtualizedMessageListWithContext = <
263266

264267
const virtuoso = useRef<VirtuosoHandle>(null);
265268

269+
const userInterractedWithScrollableViewRef = useRef<boolean>(false);
270+
const atBottomRef = useRef<(t: boolean) => void | undefined>(undefined);
271+
const atTopRef = useRef<(t: boolean) => void | undefined>(undefined);
272+
const followOutputRef =
273+
useRef<(t: boolean) => FollowOutputScalarType | undefined>(undefined);
274+
const scrollToBottomRef = useRef<() => void>(undefined);
275+
266276
const lastRead = useMemo(() => channel.lastRead?.(), [channel]);
267277

268278
const { show: showUnreadMessagesNotification, toggleShowUnreadMessagesNotification } =
@@ -361,27 +371,9 @@ const VirtualizedMessageListWithContext = <
361371
wasMarkedUnread: !!channelUnreadUiState?.first_unread_message_id,
362372
});
363373

364-
const scrollToBottom = useCallback(async () => {
365-
if (hasMoreNewer) {
366-
await jumpToLatestMessage();
367-
return;
368-
}
369-
370-
if (virtuoso.current) {
371-
virtuoso.current.scrollToIndex(processedMessages.length - 1);
372-
}
373-
374-
setNewMessagesNotification(false);
375-
// eslint-disable-next-line react-hooks/exhaustive-deps
376-
}, [
377-
virtuoso,
378-
processedMessages,
379-
setNewMessagesNotification,
380-
// processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
381-
processedMessages.length,
382-
hasMoreNewer,
383-
jumpToLatestMessage,
384-
]);
374+
const scrollToBottom = useCallback(() => {
375+
scrollToBottomRef.current?.();
376+
}, []);
385377

386378
useScrollToBottomOnNewMessage({
387379
messages,
@@ -406,7 +398,35 @@ const VirtualizedMessageListWithContext = <
406398
makeItemsRenderedHandler([toggleShowUnreadMessagesNotification], processedMessages),
407399
[processedMessages, toggleShowUnreadMessagesNotification],
408400
);
409-
const followOutput = (isAtBottom: boolean) => {
401+
402+
const followOutput = useCallback(
403+
(isAtBottom: boolean) =>
404+
followOutputRef.current?.(isAtBottom) as FollowOutputScalarType,
405+
[],
406+
);
407+
408+
const atBottomStateChange = useCallback((isAtBottom: boolean) => {
409+
atBottomRef.current?.(isAtBottom);
410+
}, []);
411+
412+
const atTopStateChange = useCallback(() => {
413+
atTopRef.current?.(true);
414+
}, []);
415+
416+
scrollToBottomRef.current = async () => {
417+
if (hasMoreNewer) {
418+
await jumpToLatestMessage();
419+
return;
420+
}
421+
422+
if (virtuoso.current) {
423+
virtuoso.current.scrollToIndex(processedMessages.length - 1);
424+
}
425+
426+
setNewMessagesNotification(false);
427+
};
428+
429+
followOutputRef.current = (isAtBottom: boolean) => {
410430
if (hasMoreNewer || suppressAutoscroll) {
411431
return false;
412432
}
@@ -426,7 +446,7 @@ const VirtualizedMessageListWithContext = <
426446
[],
427447
);
428448

429-
const atBottomStateChange = (isAtBottom: boolean) => {
449+
atBottomRef.current = (isAtBottom) => {
430450
atBottom.current = isAtBottom;
431451
setIsMessageListScrolledToBottom(isAtBottom);
432452

@@ -435,33 +455,71 @@ const VirtualizedMessageListWithContext = <
435455
setNewMessagesNotification?.(false);
436456
}
437457
};
438-
const atTopStateChange = (isAtTop: boolean) => {
458+
459+
atTopRef.current = (isAtTop: boolean) => {
439460
if (isAtTop) {
440461
loadMore?.(messageLimit);
441462
}
442463
};
443464

444465
useEffect(() => {
466+
if (!highlightedMessageId) return;
467+
445468
let scrollTimeout: ReturnType<typeof setTimeout>;
446-
if (highlightedMessageId) {
447-
const index = findMessageIndex(processedMessages, highlightedMessageId);
448-
if (index !== -1) {
449-
scrollTimeout = setTimeout(() => {
450-
virtuoso.current?.scrollToIndex({ align: 'center', index });
451-
}, 0);
452-
}
469+
const index = findMessageIndex(processedMessages, highlightedMessageId);
470+
if (index !== -1) {
471+
scrollTimeout = setTimeout(() => {
472+
virtuoso.current?.scrollToIndex({ align: 'center', index });
473+
}, 0);
453474
}
475+
454476
return () => {
455477
clearTimeout(scrollTimeout);
456478
};
457479
}, [highlightedMessageId, processedMessages]);
458480

481+
// force autoscrollToBottom if user hasn't interracted yet
482+
useEffect(() => {
483+
/**
484+
* a combination of parameters paired with extra data load on Virtuoso render causes
485+
* a message list to render a set of items not at the bottom of the list as expected
486+
* but rather either in the middle or a few hundredth pixels from the bottom
487+
*
488+
* `atTopStateChange` - if at top, load previous page, changing this to `startReached` reduces the amount of errors as it is not
489+
* being triggered at Virtuoso render but does not solve the core issue
490+
* `followOutput` - function which returns "smooth" value which is somehow more error-prone for Firefox and Safari
491+
*/
492+
493+
if (
494+
highlightedMessageId ||
495+
userInterractedWithScrollableViewRef.current ||
496+
atBottom.current
497+
) {
498+
return;
499+
}
500+
501+
const timeout = setTimeout(() => {
502+
userInterractedWithScrollableViewRef.current = true;
503+
virtuoso.current?.autoscrollToBottom();
504+
}, 0);
505+
506+
return () => {
507+
clearTimeout(timeout);
508+
};
509+
}, [atBottom, highlightedMessageId, processedMessages]);
510+
459511
if (!processedMessages) return null;
460512

461513
const dialogManagerId = threadList
462514
? 'virtualized-message-list-dialog-manager-thread'
463515
: 'virtualized-message-list-dialog-manager';
464516

517+
const extra = {
518+
...overridingVirtuosoProps,
519+
...(scrollSeekPlaceHolder ? { scrollSeek: scrollSeekPlaceHolder } : {}),
520+
...(defaultItemHeight ? { defaultItemHeight } : {}),
521+
};
522+
465523
return (
466524
<VirtualizedMessageListContextProvider value={{ scrollToBottom }}>
467525
<MessageListMainPanel>
@@ -477,8 +535,8 @@ const VirtualizedMessageListWithContext = <
477535
<Virtuoso<UnknownType, VirtuosoContext<StreamChatGenerics>>
478536
atBottomStateChange={atBottomStateChange}
479537
atBottomThreshold={100}
480-
atTopStateChange={atTopStateChange}
481-
atTopThreshold={100}
538+
// atTopStateChange={atTopStateChange}
539+
startReached={atTopStateChange}
482540
className='str-chat__message-list-scroll'
483541
components={{
484542
EmptyPlaceholder,
@@ -529,13 +587,17 @@ const VirtualizedMessageListWithContext = <
529587
itemSize={fractionalItemSize}
530588
itemsRendered={handleItemsRendered}
531589
key={messageSetKey}
590+
onTouchMove={() => {
591+
userInterractedWithScrollableViewRef.current = true;
592+
}}
593+
onWheel={() => {
594+
userInterractedWithScrollableViewRef.current = true;
595+
}}
532596
overscan={overscan}
533597
ref={virtuoso}
534-
style={{ overflowX: 'hidden' }}
598+
style={{ overflowX: 'hidden', overscrollBehavior: 'none' }}
535599
totalCount={processedMessages.length}
536-
{...overridingVirtuosoProps}
537-
{...(scrollSeekPlaceHolder ? { scrollSeek: scrollSeekPlaceHolder } : {})}
538-
{...(defaultItemHeight ? { defaultItemHeight } : {})}
600+
{...extra}
539601
/>
540602
</div>
541603
</DialogManagerProvider>

0 commit comments

Comments
 (0)