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' ;
2
4
import {
3
5
ComputeItemKey ,
6
+ FollowOutputScalarType ,
4
7
ScrollSeekConfiguration ,
5
8
ScrollSeekPlaceholderProps ,
6
- Virtuoso ,
7
9
VirtuosoHandle ,
8
10
VirtuosoProps ,
9
11
} from 'react-virtuoso' ;
12
+ import { Virtuoso } from 'react-virtuoso' ;
10
13
11
14
import { GiphyPreviewMessage as DefaultGiphyPreviewMessage } from './GiphyPreviewMessage' ;
12
15
import { useLastReadData } from './hooks' ;
@@ -263,6 +266,13 @@ const VirtualizedMessageListWithContext = <
263
266
264
267
const virtuoso = useRef < VirtuosoHandle > ( null ) ;
265
268
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
+
266
276
const lastRead = useMemo ( ( ) => channel . lastRead ?.( ) , [ channel ] ) ;
267
277
268
278
const { show : showUnreadMessagesNotification , toggleShowUnreadMessagesNotification } =
@@ -361,27 +371,9 @@ const VirtualizedMessageListWithContext = <
361
371
wasMarkedUnread : ! ! channelUnreadUiState ?. first_unread_message_id ,
362
372
} ) ;
363
373
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
+ } , [ ] ) ;
385
377
386
378
useScrollToBottomOnNewMessage ( {
387
379
messages,
@@ -406,7 +398,35 @@ const VirtualizedMessageListWithContext = <
406
398
makeItemsRenderedHandler ( [ toggleShowUnreadMessagesNotification ] , processedMessages ) ,
407
399
[ processedMessages , toggleShowUnreadMessagesNotification ] ,
408
400
) ;
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 ) => {
410
430
if ( hasMoreNewer || suppressAutoscroll ) {
411
431
return false ;
412
432
}
@@ -426,7 +446,7 @@ const VirtualizedMessageListWithContext = <
426
446
[ ] ,
427
447
) ;
428
448
429
- const atBottomStateChange = ( isAtBottom : boolean ) => {
449
+ atBottomRef . current = ( isAtBottom ) => {
430
450
atBottom . current = isAtBottom ;
431
451
setIsMessageListScrolledToBottom ( isAtBottom ) ;
432
452
@@ -435,33 +455,70 @@ const VirtualizedMessageListWithContext = <
435
455
setNewMessagesNotification ?.( false ) ;
436
456
}
437
457
} ;
438
- const atTopStateChange = ( isAtTop : boolean ) => {
458
+
459
+ atTopRef . current = ( isAtTop : boolean ) => {
439
460
if ( isAtTop ) {
440
461
loadMore ?.( messageLimit ) ;
441
462
}
442
463
} ;
443
464
444
465
useEffect ( ( ) => {
466
+ if ( ! highlightedMessageId ) return ;
467
+
445
468
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 ) ;
453
474
}
475
+
454
476
return ( ) => {
455
477
clearTimeout ( scrollTimeout ) ;
456
478
} ;
457
479
} , [ highlightedMessageId , processedMessages ] ) ;
458
480
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
+
459
510
if ( ! processedMessages ) return null ;
460
511
461
512
const dialogManagerId = threadList
462
513
? 'virtualized-message-list-dialog-manager-thread'
463
514
: 'virtualized-message-list-dialog-manager' ;
464
515
516
+ const extra = {
517
+ ...overridingVirtuosoProps ,
518
+ ...( scrollSeekPlaceHolder ? { scrollSeek : scrollSeekPlaceHolder } : { } ) ,
519
+ ...( defaultItemHeight ? { defaultItemHeight } : { } ) ,
520
+ } ;
521
+
465
522
return (
466
523
< VirtualizedMessageListContextProvider value = { { scrollToBottom } } >
467
524
< MessageListMainPanel >
@@ -477,8 +534,8 @@ const VirtualizedMessageListWithContext = <
477
534
< Virtuoso < UnknownType , VirtuosoContext < StreamChatGenerics > >
478
535
atBottomStateChange = { atBottomStateChange }
479
536
atBottomThreshold = { 100 }
480
- atTopStateChange = { atTopStateChange }
481
- atTopThreshold = { 100 }
537
+ // atTopStateChange={atTopStateChange}
538
+ startReached = { atTopStateChange }
482
539
className = 'str-chat__message-list-scroll'
483
540
components = { {
484
541
EmptyPlaceholder,
@@ -529,13 +586,17 @@ const VirtualizedMessageListWithContext = <
529
586
itemSize = { fractionalItemSize }
530
587
itemsRendered = { handleItemsRendered }
531
588
key = { messageSetKey }
589
+ onTouchMove = { ( ) => {
590
+ userInterractedWithScrollableViewRef . current = true ;
591
+ } }
592
+ onWheel = { ( ) => {
593
+ userInterractedWithScrollableViewRef . current = true ;
594
+ } }
532
595
overscan = { overscan }
533
596
ref = { virtuoso }
534
- style = { { overflowX : 'hidden' } }
597
+ style = { { overflowX : 'hidden' , overscrollBehavior : 'none' } }
535
598
totalCount = { processedMessages . length }
536
- { ...overridingVirtuosoProps }
537
- { ...( scrollSeekPlaceHolder ? { scrollSeek : scrollSeekPlaceHolder } : { } ) }
538
- { ...( defaultItemHeight ? { defaultItemHeight } : { } ) }
599
+ { ...extra }
539
600
/>
540
601
</ div >
541
602
</ DialogManagerProvider >
0 commit comments