diff --git a/package-lock.json b/package-lock.json index 00acb5df9..4d813dade 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@prisma/client": "~5.22.0", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^5.63.0", + "@tanstack/react-virtual": "^3.11.2", "@trpc/client": "11.0.0-rc.688", "@trpc/next": "11.0.0-rc.688", "@trpc/react-query": "11.0.0-rc.688", @@ -2024,6 +2025,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz", + "integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.11.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz", + "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", diff --git a/package.json b/package.json index 91317ccb7..225d36ffd 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@prisma/client": "~5.22.0", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^5.63.0", + "@tanstack/react-virtual": "^3.11.2", "@trpc/client": "11.0.0-rc.688", "@trpc/next": "11.0.0-rc.688", "@trpc/react-query": "11.0.0-rc.688", diff --git a/src/apps/chat/components/layout-drawer/ChatDrawer.tsx b/src/apps/chat/components/layout-drawer/ChatDrawer.tsx index 7213969c1..284a15c7b 100644 --- a/src/apps/chat/components/layout-drawer/ChatDrawer.tsx +++ b/src/apps/chat/components/layout-drawer/ChatDrawer.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { useShallow } from 'zustand/react/shallow'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { Box, Button, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy'; import AddIcon from '@mui/icons-material/Add'; @@ -250,6 +251,44 @@ function ChatDrawer(props: { ]); + // Virtualize the list + + const parentRef = React.useRef(null); + + const virtEstimateSize = React.useCallback((index: number) => { + const item = renderNavItems[index]; + switch (item.type) { + case 'nav-item-group': + return 34; + case 'nav-item-chat-data': + return item.isActive ? 80 : 36; + case 'nav-item-info-message': + return 34; + } + }, [renderNavItems]); + + const virtUniqueKeys = React.useMemo(() => renderNavItems.map((item, idx) => { + switch (item.type) { + case 'nav-item-group': + return `g-${item.title}`; + case 'nav-item-chat-data': + return `c-${item.conversationId}${item.isActive ? '-active' : ''}`; + case 'nav-item-info-message': + return `i-${idx}`; + } + }), [renderNavItems]); + + const virtUniqueKey = React.useCallback((index: number) => virtUniqueKeys[index], [virtUniqueKeys]); + + const rowVirtualizer = useVirtualizer({ + count: renderNavItems.length, + getScrollElement: () => parentRef.current, + estimateSize: virtEstimateSize, + getItemKey: virtUniqueKey, + overscan: 0, + }); + + return <> {/* Drawer Header */} @@ -328,55 +367,94 @@ function ChatDrawer(props: { // transition: 'box-shadow 0.2s', }} > - + New chat {/* Chat Titles List (shrink as half the rate as the Folders List) */} - - {renderNavItems.map((item, idx) => item.type === 'nav-item-chat-data' ? ( - - ) : item.type === 'nav-item-group' ? ( - - {item.title} - - ) : item.type === 'nav-item-info-message' ? ( - - - {filterHasStars && } - {item.message} - - {(filterHasStars || filterHasImageAssets || filterHasDocFragments) && ( - - - - - - )} - - ) : null, - )} + +
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const item = renderNavItems[virtualRow.index]; + return ( +
+ {item.type === 'nav-item-group' ? ( + + {item.title} + + ) : item.type === 'nav-item-chat-data' ? ( + + ) : item.type === 'nav-item-info-message' ? ( + + + {filterHasStars && ( + + )} + {item.message} + + {(filterHasStars || filterHasImageAssets || filterHasDocFragments) && ( + + + + + + )} + + ) : 'Unknown item type'} +
+ ); + })} +