-
Notifications
You must be signed in to change notification settings - Fork 13.7k
chore: paginated thread message list #40998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import type { IMessage, IThreadMessage, MessageAttachment } from '@rocket.chat/core-typings'; | ||||||||||||||||||||||||||||||||||||||||||
| import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter'; | ||||||||||||||||||||||||||||||||||||||||||
| import type { QueryClient } from '@tanstack/react-query'; | ||||||||||||||||||||||||||||||||||||||||||
| import type { InfiniteData, QueryClient } from '@tanstack/react-query'; | ||||||||||||||||||||||||||||||||||||||||||
| import type { Condition, Filter } from 'mongodb'; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import { queryClient as defaultQueryClient } from '../queryClient'; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,24 +47,76 @@ export const createDeleteCriteria = (params: NotifyRoomRidDeleteBulkEvent): ((me | |||||||||||||||||||||||||||||||||||||||||
| return createPredicateFromFilter(query); | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export type ThreadMessagesPage = { | ||||||||||||||||||||||||||||||||||||||||||
| items: IThreadMessage[]; | ||||||||||||||||||||||||||||||||||||||||||
| itemCount: number; | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export type ThreadMessagesInfiniteData = InfiniteData<ThreadMessagesPage, number>; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export const mutateThreadMessagesInfiniteData = ( | ||||||||||||||||||||||||||||||||||||||||||
| client: QueryClient, | ||||||||||||||||||||||||||||||||||||||||||
| queryKey: readonly unknown[], | ||||||||||||||||||||||||||||||||||||||||||
| mutation: (messages: IThreadMessage[]) => void, | ||||||||||||||||||||||||||||||||||||||||||
| ): void => { | ||||||||||||||||||||||||||||||||||||||||||
| client.setQueryData<ThreadMessagesInfiniteData>(queryKey, (old) => { | ||||||||||||||||||||||||||||||||||||||||||
| if (!old?.pages.length) { | ||||||||||||||||||||||||||||||||||||||||||
| return old; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const items = old.pages.flatMap((page) => page.items); | ||||||||||||||||||||||||||||||||||||||||||
| const originalPageLengths = old.pages.map((page) => page.items.length); | ||||||||||||||||||||||||||||||||||||||||||
| const oldTotal = old.pages.at(-1)?.itemCount ?? 0; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const beforeMutationItemsLength = items.length; | ||||||||||||||||||||||||||||||||||||||||||
| mutation(items); | ||||||||||||||||||||||||||||||||||||||||||
| const afterMutationItemsLength = items.length; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const itemCountDelta = beforeMutationItemsLength - afterMutationItemsLength; | ||||||||||||||||||||||||||||||||||||||||||
| const newTotal = Math.max(0, oldTotal - itemCountDelta); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const pages: ThreadMessagesPage[] = []; | ||||||||||||||||||||||||||||||||||||||||||
| let cursor = 0; | ||||||||||||||||||||||||||||||||||||||||||
| for (let pageIndex = 0; pageIndex < old.pages.length; pageIndex++) { | ||||||||||||||||||||||||||||||||||||||||||
| const isLastPage = pageIndex === old.pages.length - 1; | ||||||||||||||||||||||||||||||||||||||||||
| const take = isLastPage ? items.length - cursor : Math.min(originalPageLengths[pageIndex], items.length - cursor); | ||||||||||||||||||||||||||||||||||||||||||
| const slice = items.slice(cursor, cursor + Math.max(0, take)); | ||||||||||||||||||||||||||||||||||||||||||
| cursor += slice.length; | ||||||||||||||||||||||||||||||||||||||||||
| pages.push({ items: slice, itemCount: newTotal }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||
| pages, | ||||||||||||||||||||||||||||||||||||||||||
| pageParams: old.pageParams, | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export const upsertThreadMessageInCache = ( | ||||||||||||||||||||||||||||||||||||||||||
| message: IMessage, | ||||||||||||||||||||||||||||||||||||||||||
| rid: IMessage['rid'], | ||||||||||||||||||||||||||||||||||||||||||
| tmid: IMessage['_id'], | ||||||||||||||||||||||||||||||||||||||||||
| client: QueryClient = defaultQueryClient, | ||||||||||||||||||||||||||||||||||||||||||
| ): void => { | ||||||||||||||||||||||||||||||||||||||||||
| const queryKey = roomsQueryKeys.threadMessages(rid, tmid); | ||||||||||||||||||||||||||||||||||||||||||
| client.setQueryData<IMessage[]>(queryKey, (old) => { | ||||||||||||||||||||||||||||||||||||||||||
| if (!old) { | ||||||||||||||||||||||||||||||||||||||||||
| return [message]; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| const idx = old.findIndex((m) => m._id === message._id); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (!client.getQueryData<ThreadMessagesInfiniteData>(queryKey)) { | ||||||||||||||||||||||||||||||||||||||||||
| client.setQueryData<ThreadMessagesInfiniteData>(queryKey, { | ||||||||||||||||||||||||||||||||||||||||||
| pages: [{ items: [message as IThreadMessage], itemCount: 1 }], | ||||||||||||||||||||||||||||||||||||||||||
| pageParams: [0], | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| mutateThreadMessagesInfiniteData(client, queryKey, (messages) => { | ||||||||||||||||||||||||||||||||||||||||||
| const idx = messages.findIndex((m) => m._id === message._id); | ||||||||||||||||||||||||||||||||||||||||||
| if (idx >= 0) { | ||||||||||||||||||||||||||||||||||||||||||
| const updated = [...old]; | ||||||||||||||||||||||||||||||||||||||||||
| updated[idx] = message; | ||||||||||||||||||||||||||||||||||||||||||
| return updated; | ||||||||||||||||||||||||||||||||||||||||||
| messages[idx] = message as IThreadMessage; | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return [...old, message].sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+112
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Update every cached copy of the message, not just the first match. Infinite pages can overlap, and a later stale duplicate can overwrite the updated one in the selected thread messages. Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| messages.push(message as IThreadMessage); | ||||||||||||||||||||||||||||||||||||||||||
| messages.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime()); | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,18 @@ | ||||||||||||||||||||||||||||||||||
| import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; | ||||||||||||||||||||||||||||||||||
| import { isEditedMessage } from '@rocket.chat/core-typings'; | ||||||||||||||||||||||||||||||||||
| import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; | ||||||||||||||||||||||||||||||||||
| import { MessageTypes } from '@rocket.chat/message-types'; | ||||||||||||||||||||||||||||||||||
| import { isTruthy } from '@rocket.chat/tools'; | ||||||||||||||||||||||||||||||||||
| import { clientCallbacks, CustomVirtuaScrollbars } from '@rocket.chat/ui-client'; | ||||||||||||||||||||||||||||||||||
| import { useSearchParameter, useSetting, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; | ||||||||||||||||||||||||||||||||||
| import { differenceInSeconds } from 'date-fns'; | ||||||||||||||||||||||||||||||||||
| import { Fragment, useEffect, useMemo, useRef } from 'react'; | ||||||||||||||||||||||||||||||||||
| import { Fragment, useEffect, useLayoutEffect, useMemo, useRef } from 'react'; | ||||||||||||||||||||||||||||||||||
| import { useTranslation } from 'react-i18next'; | ||||||||||||||||||||||||||||||||||
| import type { VirtualizerHandle } from 'virtua'; | ||||||||||||||||||||||||||||||||||
| import { VList } from 'virtua'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { ThreadMessageItem } from './ThreadMessageItem'; | ||||||||||||||||||||||||||||||||||
| import InfiniteListAnchor from '../../../../../components/InfiniteListAnchor'; | ||||||||||||||||||||||||||||||||||
| import { useMergedRefsV2 } from '../../../../../hooks/useMergedRefsV2'; | ||||||||||||||||||||||||||||||||||
| import { setMessageJumpQueryStringParameter } from '../../../../../lib/utils/setMessageJumpQueryStringParameter'; | ||||||||||||||||||||||||||||||||||
| import { BubbleDate } from '../../../BubbleDate'; | ||||||||||||||||||||||||||||||||||
|
|
@@ -61,7 +63,40 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| const msgJumpParam = useSearchParameter('msg'); | ||||||||||||||||||||||||||||||||||
| const { bubbleRef, handleDateScroll, ...bubbleDate } = useDateScroll(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const { data: messages = [], isLoading: loading } = useThreadMessagesQuery(mainMessage._id); | ||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||
| data, | ||||||||||||||||||||||||||||||||||
| isLoading: loading, | ||||||||||||||||||||||||||||||||||
| fetchNextPage, | ||||||||||||||||||||||||||||||||||
| hasNextPage, | ||||||||||||||||||||||||||||||||||
| isFetchingNextPage, | ||||||||||||||||||||||||||||||||||
| fetchPreviousPage, | ||||||||||||||||||||||||||||||||||
| hasPreviousPage, | ||||||||||||||||||||||||||||||||||
| isFetchingPreviousPage, | ||||||||||||||||||||||||||||||||||
| loadMessageAround, | ||||||||||||||||||||||||||||||||||
| } = useThreadMessagesQuery(mainMessage._id); | ||||||||||||||||||||||||||||||||||
| const messages = useMemo(() => data?.messages ?? [], [data?.messages]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const loadMoreMessages = useDebouncedCallback( | ||||||||||||||||||||||||||||||||||
| () => { | ||||||||||||||||||||||||||||||||||
| if (hasNextPage && !isFetchingNextPage) { | ||||||||||||||||||||||||||||||||||
| void fetchNextPage(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| 100, | ||||||||||||||||||||||||||||||||||
| [hasNextPage, isFetchingNextPage, fetchNextPage], | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const initialScrollDoneRef = useRef(false); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const loadPreviousMessages = useDebouncedCallback( | ||||||||||||||||||||||||||||||||||
| () => { | ||||||||||||||||||||||||||||||||||
| if (initialScrollDoneRef.current && isAtBottom.current !== true && hasPreviousPage && !isFetchingPreviousPage) { | ||||||||||||||||||||||||||||||||||
| void fetchPreviousPage(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| 100, | ||||||||||||||||||||||||||||||||||
| [hasPreviousPage, isFetchingPreviousPage, fetchPreviousPage], | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const room = useRoom(); | ||||||||||||||||||||||||||||||||||
| const uid = useUserId(); | ||||||||||||||||||||||||||||||||||
|
|
@@ -88,12 +123,50 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }, [messagesLength, setKeepAtBottom, msgJumpParam]); | ||||||||||||||||||||||||||||||||||
| const loadingWindowKeyRef = useRef<string | undefined>(undefined); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||
| if (loading || !msgJumpParam || isFetchingNextPage || isFetchingPreviousPage) { | ||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (msgJumpParam === mainMessage._id) { | ||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (messages.some((message) => message._id === msgJumpParam)) { | ||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| const windowKey = `${mainMessage._id}:${msgJumpParam}`; | ||||||||||||||||||||||||||||||||||
| if (loadingWindowKeyRef.current === windowKey) { | ||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| loadingWindowKeyRef.current = windowKey; | ||||||||||||||||||||||||||||||||||
| void loadMessageAround(msgJumpParam); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+138
to
+143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Handle failed deep-link loads and allow retry.
Proposed fix if (loadingWindowKeyRef.current === windowKey) {
return;
}
loadingWindowKeyRef.current = windowKey;
- void loadMessageAround(msgJumpParam);
+ void loadMessageAround(msgJumpParam).catch(() => {
+ if (loadingWindowKeyRef.current === windowKey) {
+ loadingWindowKeyRef.current = undefined;
+ }
+ });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| }, [loading, isFetchingNextPage, isFetchingPreviousPage, msgJumpParam, messages, mainMessage._id, loadMessageAround]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const mergedRefs = useMergedRefsV2(messageListRef, keepAtBottomRef); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const lastScrollSizeRef = useRef(0); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const items = loading ? [] : [mainMessage, ...messages]; | ||||||||||||||||||||||||||||||||||
| const showMainMessage = !hasPreviousPage; | ||||||||||||||||||||||||||||||||||
| const items = useMemo(() => { | ||||||||||||||||||||||||||||||||||
| if (loading) { | ||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return showMainMessage ? [mainMessage, ...messages] : messages; | ||||||||||||||||||||||||||||||||||
| }, [loading, showMainMessage, mainMessage, messages]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const firstItemId = items[0]?._id; | ||||||||||||||||||||||||||||||||||
| const prevFirstItemIdRef = useRef<string | undefined>(undefined); | ||||||||||||||||||||||||||||||||||
| const prevItemsCountRef = useRef(0); | ||||||||||||||||||||||||||||||||||
| const isPrepend = | ||||||||||||||||||||||||||||||||||
| prevItemsCountRef.current > 0 && | ||||||||||||||||||||||||||||||||||
| items.length > prevItemsCountRef.current && | ||||||||||||||||||||||||||||||||||
| firstItemId !== undefined && | ||||||||||||||||||||||||||||||||||
| firstItemId !== prevFirstItemIdRef.current; | ||||||||||||||||||||||||||||||||||
| useLayoutEffect(() => { | ||||||||||||||||||||||||||||||||||
| prevFirstItemIdRef.current = firstItemId; | ||||||||||||||||||||||||||||||||||
| prevItemsCountRef.current = items.length; | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const threadMsgTargetIndex = useMemo(() => { | ||||||||||||||||||||||||||||||||||
| if (!msgJumpParam || loading) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -103,14 +176,19 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| const replyIndex = messages.findIndex((m) => m._id === msgJumpParam); | ||||||||||||||||||||||||||||||||||
| return replyIndex >= 0 ? 1 + replyIndex : -1; | ||||||||||||||||||||||||||||||||||
| }, [msgJumpParam, loading, mainMessage._id, messages]); | ||||||||||||||||||||||||||||||||||
| if (replyIndex < 0) { | ||||||||||||||||||||||||||||||||||
| return -1; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return showMainMessage ? 1 + replyIndex : replyIndex; | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Jump-to-message is off by one when older thread pages exist. Account for the top loader child before calling Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||
| }, [msgJumpParam, loading, mainMessage._id, messages, showMainMessage]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const lastThreadJumpKeyRef = useRef<string | undefined>(undefined); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||
| lastThreadJumpKeyRef.current = undefined; | ||||||||||||||||||||||||||||||||||
| loadingWindowKeyRef.current = undefined; | ||||||||||||||||||||||||||||||||||
| prevItemsLengthRef.current = 0; | ||||||||||||||||||||||||||||||||||
| initialScrollDoneRef.current = false; | ||||||||||||||||||||||||||||||||||
| }, [mainMessage._id]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||
|
|
@@ -145,6 +223,7 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| isAtBottom.current = true; | ||||||||||||||||||||||||||||||||||
| handle.scrollToIndex(items.length, { align: 'end' }); | ||||||||||||||||||||||||||||||||||
| setShouldJumpToBottom(false); | ||||||||||||||||||||||||||||||||||
| initialScrollDoneRef.current = true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }, [items, loading, msgJumpParam, threadMsgTargetIndex, shouldJumpToBottom, setShouldJumpToBottom, uid]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -163,6 +242,7 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| lastThreadJumpKeyRef.current = jumpKey; | ||||||||||||||||||||||||||||||||||
| setShouldJumpToBottom(false); | ||||||||||||||||||||||||||||||||||
| handle.scrollToIndex(threadMsgTargetIndex, { align: 'center' }); | ||||||||||||||||||||||||||||||||||
| initialScrollDoneRef.current = true; | ||||||||||||||||||||||||||||||||||
| setHighlightMessage(msgJumpParam); | ||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||
| clearHighlightMessage(); | ||||||||||||||||||||||||||||||||||
|
|
@@ -213,15 +293,20 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| <MessageListProvider> | ||||||||||||||||||||||||||||||||||
| <VList | ||||||||||||||||||||||||||||||||||
| ref={virtualizerRef} | ||||||||||||||||||||||||||||||||||
| shift={isPrepend} | ||||||||||||||||||||||||||||||||||
| style={{ height: '100%' }} | ||||||||||||||||||||||||||||||||||
| aria-label={t('Thread_message_list')} | ||||||||||||||||||||||||||||||||||
| aria-busy={loading} | ||||||||||||||||||||||||||||||||||
| aria-busy={loading || isFetchingNextPage || isFetchingPreviousPage} | ||||||||||||||||||||||||||||||||||
| role='list' | ||||||||||||||||||||||||||||||||||
| keepMounted={keepMountedMessages} | ||||||||||||||||||||||||||||||||||
| onScroll={(offset) => { | ||||||||||||||||||||||||||||||||||
| const handle = virtualizerRef.current; | ||||||||||||||||||||||||||||||||||
| if (!handle) return; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (offset < 200 && hasPreviousPage) { | ||||||||||||||||||||||||||||||||||
| loadPreviousMessages(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Copied from messageList, I'm unsure why this is necessary, but it seems to be needed to properly set the isAtBottom state | ||||||||||||||||||||||||||||||||||
| if (handle.scrollSize >= handle.viewportSize) { | ||||||||||||||||||||||||||||||||||
| isAtBottom.current = true; | ||||||||||||||||||||||||||||||||||
|
|
@@ -236,7 +321,11 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| <li className='load-more'> | ||||||||||||||||||||||||||||||||||
| <LoadingMessagesIndicator /> | ||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||
| {!loading && hasPreviousPage ? ( | ||||||||||||||||||||||||||||||||||
| <li className='load-more'>{isFetchingPreviousPage ? <LoadingMessagesIndicator /> : null}</li> | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Top date tracking becomes off by one while previous pages exist because the loader occupies VList index 0. Subtract the top-loader offset before indexing Prompt for AI agents |
||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||
| {!loading && | ||||||||||||||||||||||||||||||||||
| items.map((message, index, { [index - 1]: previous }) => { | ||||||||||||||||||||||||||||||||||
| const sequential = isMessageSequential(message, previous, messageGroupingPeriod); | ||||||||||||||||||||||||||||||||||
| const newDay = isMessageNewDay(message, previous); | ||||||||||||||||||||||||||||||||||
|
|
@@ -257,8 +346,12 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot | |||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
| </Fragment> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||
| {!loading && hasNextPage ? ( | ||||||||||||||||||||||||||||||||||
| <li className='load-more'> | ||||||||||||||||||||||||||||||||||
| {isFetchingNextPage ? <LoadingMessagesIndicator /> : <InfiniteListAnchor loadMore={loadMoreMessages} />} | ||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||
| </VList> | ||||||||||||||||||||||||||||||||||
| </MessageListProvider> | ||||||||||||||||||||||||||||||||||
| </CustomVirtuaScrollbars> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Include the same query filter when calculating the
aroundIdoffset. Otherwise filtered thread-message requests can return a page that is not centered around the requested message.Prompt for AI agents