'use client' import { Message } from '@/components/message/Message' import { Loader } from '@/components/ui/Loader' import { AdminOnlyUi } from '@/components/util/AdminOnlyUi' import { useMessageFeedQuery } from '@/lib/api/messages' import { twx } from '@/lib/utils' import type { Message as MessageType } from '@corale/backend' import { memo, useCallback, useRef, useState } from 'react' import { Components, ListItem, Virtuoso, VirtuosoHandle } from 'react-virtuoso' export type MessageFeedContext = { status: 'LoadingFirstPage' | 'CanLoadMore' | 'LoadingMore' | 'Exhausted' } export const VirtualizedMessageFeed = ({ threadId }: { threadId: string }) => { const virtuosoRef = useRef(null) const isScrollingRef = useRef(false) const lastMessageData = useRef({ _id: '', size: 0 }) const [isAtTop, setIsAtTop] = useState(true) const [isAtBottom, setIsAtBottom] = useState(false) const { results, loadMore, status, prependedCount } = useMessageFeedQuery(threadId, 25) const scrollToEnd = useCallback(() => { if (isScrollingRef.current) return console.debug('scroll skipped') virtuosoRef.current?.scrollToIndex({ index: results.length - 1, align: 'end', behavior: 'smooth', }) console.debug('scrolled to end') }, [results.length]) // * track last item's size, scroll to end if increased const handleItemsRendered = useCallback( (items: ListItem[]) => { const lastItemRendered = items.at(-1) const isLastMessage = lastItemRendered && lastItemRendered.originalIndex === results.length - 1 if (!isLastMessage || !lastItemRendered.data) return const isSameId = lastItemRendered.data._id === lastMessageData.current._id const isLarger = lastItemRendered.size > lastMessageData.current.size if (isSameId && isLarger && isAtBottom) scrollToEnd() lastMessageData.current = { _id: lastItemRendered.data._id, size: lastItemRendered.size } }, [isAtBottom, results.length, scrollToEnd], ) const handleAtTopStateChange = useCallback( (atTop: boolean) => { setIsAtTop(atTop) if (atTop && status === 'CanLoadMore') { console.debug('load', 40) loadMore(40) } }, [loadMore, status], ) const handleAtBottomStateChange = useCallback((atBottom: boolean) => setIsAtBottom(atBottom), []) return ( <> ref={virtuosoRef} context={{ status }} components={{ Header, Footer, List, EmptyPlaceholder, }} data={results} alignToBottom followOutput={true} firstItemIndex={1_000_000 - prependedCount} atTopStateChange={handleAtTopStateChange} atTopThreshold={1200} atBottomStateChange={handleAtBottomStateChange} atBottomThreshold={200} increaseViewportBy={1200} defaultItemHeight={900} computeItemKey={(_, item) => item._id} itemContent={(_, data) => } isScrolling={(isScrolling) => (isScrollingRef.current = isScrolling)} itemsRendered={handleItemsRendered} skipAnimationFrameInResizeObserver /> {status === 'LoadingMore' && (
)}
{isAtTop ? 'atTop' : ''} {isAtBottom ? 'atBottom' : ''} {-prependedCount} {results.length}
) } const MemoizedMessage = memo(({ message }: { message: MessageType }) => { return (
) }) MemoizedMessage.displayName = 'MMessage' const EmptyPlaceholder: Components['EmptyPlaceholder'] = ({ context }) => (
{context.status === 'LoadingFirstPage' && }
) const Header: Components['Header'] = ({ context }) => { return (
{context.status === 'Exhausted' && (
This is the start of the chat.
)}
) } const Footer: Components['Footer'] = () => { return
} const List = twx.div`px-1 lg:max-w-[80%] w-full mx-auto`