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' ;
24import {
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
1114import { GiphyPreviewMessage as DefaultGiphyPreviewMessage } from './GiphyPreviewMessage' ;
1215import { 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