Skip to content

Commit d7eb30f

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

File tree

1 file changed

+100
-39
lines changed

1 file changed

+100
-39
lines changed

src/components/MessageList/VirtualizedMessageList.tsx

+100-39
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';
@@ -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,70 @@ 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 && atBottomRef.current)
496+
) {
497+
return;
498+
}
499+
500+
const timeout = setTimeout(() => {
501+
userInterractedWithScrollableViewRef.current = true;
502+
virtuoso.current?.autoscrollToBottom();
503+
}, 0);
504+
505+
return () => {
506+
clearTimeout(timeout);
507+
};
508+
}, [highlightedMessageId, processedMessages]);
509+
459510
if (!processedMessages) return null;
460511

461512
const dialogManagerId = threadList
462513
? 'virtualized-message-list-dialog-manager-thread'
463514
: 'virtualized-message-list-dialog-manager';
464515

516+
const extra = {
517+
...overridingVirtuosoProps,
518+
...(scrollSeekPlaceHolder ? { scrollSeek: scrollSeekPlaceHolder } : {}),
519+
...(defaultItemHeight ? { defaultItemHeight } : {}),
520+
};
521+
465522
return (
466523
<VirtualizedMessageListContextProvider value={{ scrollToBottom }}>
467524
<MessageListMainPanel>
@@ -477,8 +534,8 @@ const VirtualizedMessageListWithContext = <
477534
<Virtuoso<UnknownType, VirtuosoContext<StreamChatGenerics>>
478535
atBottomStateChange={atBottomStateChange}
479536
atBottomThreshold={100}
480-
atTopStateChange={atTopStateChange}
481-
atTopThreshold={100}
537+
// atTopStateChange={atTopStateChange}
538+
startReached={atTopStateChange}
482539
className='str-chat__message-list-scroll'
483540
components={{
484541
EmptyPlaceholder,
@@ -529,13 +586,17 @@ const VirtualizedMessageListWithContext = <
529586
itemSize={fractionalItemSize}
530587
itemsRendered={handleItemsRendered}
531588
key={messageSetKey}
589+
onTouchMove={() => {
590+
userInterractedWithScrollableViewRef.current = true;
591+
}}
592+
onWheel={() => {
593+
userInterractedWithScrollableViewRef.current = true;
594+
}}
532595
overscan={overscan}
533596
ref={virtuoso}
534-
style={{ overflowX: 'hidden' }}
597+
style={{ overflowX: 'hidden', overscrollBehavior: 'none' }}
535598
totalCount={processedMessages.length}
536-
{...overridingVirtuosoProps}
537-
{...(scrollSeekPlaceHolder ? { scrollSeek: scrollSeekPlaceHolder } : {})}
538-
{...(defaultItemHeight ? { defaultItemHeight } : {})}
599+
{...extra}
539600
/>
540601
</div>
541602
</DialogManagerProvider>

0 commit comments

Comments
 (0)