@@ -6,7 +6,7 @@ import { isTruthy } from '@rocket.chat/tools';
66import { clientCallbacks , CustomVirtuaScrollbars } from '@rocket.chat/ui-client' ;
77import { useSearchParameter , useSetting , useUserId , useUserPreference } from '@rocket.chat/ui-contexts' ;
88import { differenceInSeconds } from 'date-fns' ;
9- import { Fragment , useEffect , useMemo , useRef } from 'react' ;
9+ import { Fragment , useEffect , useLayoutEffect , useMemo , useRef } from 'react' ;
1010import { useTranslation } from 'react-i18next' ;
1111import type { VirtualizerHandle } from 'virtua' ;
1212import { VList } from 'virtua' ;
@@ -63,7 +63,17 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
6363 const msgJumpParam = useSearchParameter ( 'msg' ) ;
6464 const { bubbleRef, handleDateScroll, ...bubbleDate } = useDateScroll ( ) ;
6565
66- const { data, isLoading : loading , fetchNextPage, hasNextPage, isFetchingNextPage } = useThreadMessagesQuery ( mainMessage . _id ) ;
66+ const {
67+ data,
68+ isLoading : loading ,
69+ fetchNextPage,
70+ hasNextPage,
71+ isFetchingNextPage,
72+ fetchPreviousPage,
73+ hasPreviousPage,
74+ isFetchingPreviousPage,
75+ loadMessageAround,
76+ } = useThreadMessagesQuery ( mainMessage . _id ) ;
6777 const messages = data ?. messages ?? [ ] ;
6878
6979 const loadMoreMessages = useDebouncedCallback (
@@ -76,6 +86,16 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
7686 [ hasNextPage , isFetchingNextPage , fetchNextPage ] ,
7787 ) ;
7888
89+ const loadPreviousMessages = useDebouncedCallback (
90+ ( ) => {
91+ if ( hasPreviousPage && ! isFetchingPreviousPage ) {
92+ void fetchPreviousPage ( ) ;
93+ }
94+ } ,
95+ 100 ,
96+ [ hasPreviousPage , isFetchingPreviousPage , fetchPreviousPage ] ,
97+ ) ;
98+
7999 const room = useRoom ( ) ;
80100 const uid = useUserId ( ) ;
81101
@@ -90,6 +110,16 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
90110 const isAtBottom = useRef < boolean | null > ( null ) ;
91111 const prevItemsLengthRef = useRef ( 0 ) ;
92112
113+ // When older replies are prepended (loading previous pages), virtua must shift the
114+ // scroll position so the viewport stays anchored on the same message instead of
115+ // jumping. The flag is armed near the top in onScroll and reset after every commit,
116+ // so appends at the bottom (new messages) don't shift.
117+ // https://inokawa.github.io/virtua/?path=/story/advanced-chat--default
118+ const isPrepend = useRef ( false ) ;
119+ useLayoutEffect ( ( ) => {
120+ isPrepend . current = false ;
121+ } ) ;
122+
93123 const { keepAtBottomRef, setKeepAtBottom } = useKeepAtBottom ( isAtBottom ) ;
94124 const messagesLength = messages . length ;
95125 useEffect ( ( ) => {
@@ -101,9 +131,10 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
101131 }
102132 } ) ;
103133 } , [ messagesLength , setKeepAtBottom , msgJumpParam ] ) ;
134+ const loadingWindowKeyRef = useRef < string | undefined > ( undefined ) ;
104135
105136 useEffect ( ( ) => {
106- if ( loading || isFetchingNextPage || ! hasNextPage || ! msgJumpParam ) {
137+ if ( loading || ! msgJumpParam || isFetchingNextPage || isFetchingPreviousPage ) {
107138 return ;
108139 }
109140 if ( msgJumpParam === mainMessage . _id ) {
@@ -112,8 +143,13 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
112143 if ( messages . some ( ( message ) => message . _id === msgJumpParam ) ) {
113144 return ;
114145 }
115- void fetchNextPage ( ) ;
116- } , [ loading , isFetchingNextPage , hasNextPage , msgJumpParam , messages , mainMessage . _id , fetchNextPage ] ) ;
146+ const windowKey = `${ mainMessage . _id } :${ msgJumpParam } ` ;
147+ if ( loadingWindowKeyRef . current === windowKey ) {
148+ return ;
149+ }
150+ loadingWindowKeyRef . current = windowKey ;
151+ void loadMessageAround ( msgJumpParam ) ;
152+ } , [ loading , isFetchingNextPage , isFetchingPreviousPage , msgJumpParam , messages , mainMessage . _id , loadMessageAround ] ) ;
117153
118154 const mergedRefs = useMergedRefsV2 ( messageListRef , keepAtBottomRef ) ;
119155
@@ -136,6 +172,7 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
136172
137173 useEffect ( ( ) => {
138174 lastThreadJumpKeyRef . current = undefined ;
175+ loadingWindowKeyRef . current = undefined ;
139176 prevItemsLengthRef . current = 0 ;
140177 } , [ mainMessage . _id ] ) ;
141178
@@ -239,15 +276,20 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
239276 < MessageListProvider >
240277 < VList
241278 ref = { virtualizerRef }
279+ shift = { isPrepend . current === true }
242280 style = { { height : '100%' } }
243281 aria-label = { t ( 'Thread_message_list' ) }
244- aria-busy = { loading || isFetchingNextPage }
282+ aria-busy = { loading || isFetchingNextPage || isFetchingPreviousPage }
245283 role = 'list'
246284 keepMounted = { keepMountedMessages }
247285 onScroll = { ( offset ) => {
248286 const handle = virtualizerRef . current ;
249287 if ( ! handle ) return ;
250288
289+ if ( offset < 200 && hasPreviousPage ) {
290+ isPrepend . current = true ;
291+ }
292+
251293 // Copied from messageList, I'm unsure why this is necessary, but it seems to be needed to properly set the isAtBottom state
252294 if ( handle . scrollSize >= handle . viewportSize ) {
253295 isAtBottom . current = true ;
@@ -281,6 +323,11 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot
281323 firstUnread = { firstUnread }
282324 system = { system }
283325 />
326+ { index === 0 && hasPreviousPage ? (
327+ < li className = 'load-more' >
328+ { isFetchingPreviousPage ? < LoadingMessagesIndicator /> : < InfiniteListAnchor loadMore = { loadPreviousMessages } /> }
329+ </ li >
330+ ) : null }
284331 </ Fragment >
285332 ) ;
286333 } )
0 commit comments