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' ;
@@ -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