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' ;
@@ -231,7 +234,7 @@ const VirtualizedMessageListWithContext = <
231
234
showUnreadNotificationAlways,
232
235
sortReactionDetails,
233
236
sortReactions,
234
- stickToBottomScrollBehavior = 'smooth ' ,
237
+ stickToBottomScrollBehavior = 'auto ' ,
235
238
suppressAutoscroll,
236
239
threadList,
237
240
} = props ;
@@ -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,71 @@ 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 ||
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
+
459
511
if ( ! processedMessages ) return null ;
460
512
461
513
const dialogManagerId = threadList
462
514
? 'virtualized-message-list-dialog-manager-thread'
463
515
: 'virtualized-message-list-dialog-manager' ;
464
516
517
+ const extra = {
518
+ ...overridingVirtuosoProps ,
519
+ ...( scrollSeekPlaceHolder ? { scrollSeek : scrollSeekPlaceHolder } : { } ) ,
520
+ ...( defaultItemHeight ? { defaultItemHeight } : { } ) ,
521
+ } ;
522
+
465
523
return (
466
524
< VirtualizedMessageListContextProvider value = { { scrollToBottom } } >
467
525
< MessageListMainPanel >
@@ -477,8 +535,8 @@ const VirtualizedMessageListWithContext = <
477
535
< Virtuoso < UnknownType , VirtuosoContext < StreamChatGenerics > >
478
536
atBottomStateChange = { atBottomStateChange }
479
537
atBottomThreshold = { 100 }
480
- atTopStateChange = { atTopStateChange }
481
- atTopThreshold = { 100 }
538
+ // atTopStateChange={atTopStateChange}
539
+ startReached = { atTopStateChange }
482
540
className = 'str-chat__message-list-scroll'
483
541
components = { {
484
542
EmptyPlaceholder,
@@ -529,13 +587,17 @@ const VirtualizedMessageListWithContext = <
529
587
itemSize = { fractionalItemSize }
530
588
itemsRendered = { handleItemsRendered }
531
589
key = { messageSetKey }
590
+ onTouchMove = { ( ) => {
591
+ userInterractedWithScrollableViewRef . current = true ;
592
+ } }
593
+ onWheel = { ( ) => {
594
+ userInterractedWithScrollableViewRef . current = true ;
595
+ } }
532
596
overscan = { overscan }
533
597
ref = { virtuoso }
534
- style = { { overflowX : 'hidden' } }
598
+ style = { { overflowX : 'hidden' , overscrollBehavior : 'none' } }
535
599
totalCount = { processedMessages . length }
536
- { ...overridingVirtuosoProps }
537
- { ...( scrollSeekPlaceHolder ? { scrollSeek : scrollSeekPlaceHolder } : { } ) }
538
- { ...( defaultItemHeight ? { defaultItemHeight } : { } ) }
600
+ { ...extra }
539
601
/>
540
602
</ div >
541
603
</ DialogManagerProvider >
0 commit comments