import { useMemo, memo, type FC, useCallback } from 'react'; import throttle from 'lodash/throttle'; import { parseISO, isToday } from 'date-fns'; import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized'; import { useLocalize, TranslationKeys, useMediaQuery } from '~/hooks'; import { TConversation } from 'librechat-data-provider'; import { groupConversationsByDate } from '~/utils'; import { Spinner } from '~/components/svg'; import Convo from './Convo'; interface ConversationsProps { conversations: Array; moveToTop: () => void; toggleNav: () => void; containerRef: React.RefObject; loadMoreConversations: () => void; isFetchingNextPage: boolean; isSearchLoading: boolean; } const LoadingSpinner = memo(() => ( )); const DateLabel: FC<{ groupName: string }> = memo(({ groupName }) => { const localize = useLocalize(); return (
{localize(groupName as TranslationKeys) || groupName}
); }); DateLabel.displayName = 'DateLabel'; type FlattenedItem = | { type: 'header'; groupName: string } | { type: 'convo'; convo: TConversation }; const MemoizedConvo = memo( ({ conversation, retainView, toggleNav, isLatestConvo, }: { conversation: TConversation; retainView: () => void; toggleNav: () => void; isLatestConvo: boolean; }) => { return ( ); }, (prevProps, nextProps) => { return ( prevProps.conversation.conversationId === nextProps.conversation.conversationId && prevProps.conversation.title === nextProps.conversation.title && prevProps.isLatestConvo === nextProps.isLatestConvo && prevProps.conversation.endpoint === nextProps.conversation.endpoint ); }, ); const Conversations: FC = ({ conversations: rawConversations, moveToTop, toggleNav, containerRef, loadMoreConversations, isFetchingNextPage, isSearchLoading, }) => { const isSmallScreen = useMediaQuery('(max-width: 768px)'); const convoHeight = isSmallScreen ? 44 : 34; const filteredConversations = useMemo( () => rawConversations.filter(Boolean) as TConversation[], [rawConversations], ); const groupedConversations = useMemo( () => groupConversationsByDate(filteredConversations), [filteredConversations], ); const firstTodayConvoId = useMemo( () => filteredConversations.find((convo) => convo.updatedAt && isToday(parseISO(convo.updatedAt))) ?.conversationId ?? undefined, [filteredConversations], ); const flattenedItems = useMemo(() => { const items: FlattenedItem[] = []; groupedConversations.forEach(([groupName, convos]) => { items.push({ type: 'header', groupName }); items.push(...convos.map((convo) => ({ type: 'convo' as const, convo }))); }); return items; }, [groupedConversations]); const cache = useMemo( () => new CellMeasurerCache({ fixedWidth: true, defaultHeight: convoHeight, keyMapper: (index) => { const item = flattenedItems[index]; return item.type === 'header' ? `header-${index}` : `convo-${item.convo.conversationId}`; }, }), [flattenedItems, convoHeight], ); const rowRenderer = useCallback( ({ index, key, parent, style }) => { const item = flattenedItems[index]; return ( {({ registerChild }) => (
{item.type === 'header' ? ( ) : ( )}
)}
); }, [cache, flattenedItems, firstTodayConvoId, moveToTop, toggleNav], ); const getRowHeight = useCallback( ({ index }: { index: number }) => cache.getHeight(index, 0), [cache], ); const throttledLoadMore = useMemo( () => throttle(loadMoreConversations, 300), [loadMoreConversations], ); const handleRowsRendered = useCallback( ({ stopIndex }: { stopIndex: number }) => { if (stopIndex >= flattenedItems.length - 2) { throttledLoadMore(); } }, [flattenedItems.length, throttledLoadMore], ); return (
{isSearchLoading ? (
Loading...
) : (
{({ width, height }) => ( } width={width} height={height} deferredMeasurementCache={cache} rowCount={flattenedItems.length} rowHeight={getRowHeight} rowRenderer={rowRenderer} overscanRowCount={10} className="outline-none" style={{ outline: 'none' }} role="list" aria-label="Conversations" onRowsRendered={handleRowsRendered} /> )}
)} {isFetchingNextPage && !isSearchLoading && (
)}
); }; export default memo(Conversations);