import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import { useMediaQuery } from '@librechat/client'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; import type { ConversationListResponse } from 'librechat-data-provider'; import type { InfiniteQueryObserverResult } from '@tanstack/react-query'; import { useLocalize, useHasAccess, useAuthContext, useLocalStorage, useNavScrolling, } from '~/hooks'; import { useConversationsInfiniteQuery } from '~/data-provider'; import { Conversations } from '~/components/Conversations'; import SearchBar from './SearchBar'; import NewChat from './NewChat'; import { cn } from '~/utils'; import store from '~/store'; const BookmarkNav = lazy(() => import('./Bookmarks/BookmarkNav')); const AccountSettings = lazy(() => import('./AccountSettings')); const NAV_WIDTH_DESKTOP = '260px'; const NAV_WIDTH_MOBILE = '320px'; const NavMask = memo( ({ navVisible, toggleNavVisible }: { navVisible: boolean; toggleNavVisible: () => void }) => (
{ if (e.key === 'Enter' || e.key === ' ') { toggleNavVisible(); } }} aria-label="Toggle navigation" /> ), ); const MemoNewChat = memo(NewChat); const Nav = memo( ({ navVisible, setNavVisible, }: { navVisible: boolean; setNavVisible: React.Dispatch>; }) => { const localize = useLocalize(); const { isAuthenticated } = useAuthContext(); const [navWidth, setNavWidth] = useState(NAV_WIDTH_DESKTOP); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const [newUser, setNewUser] = useLocalStorage('newUser', true); const [showLoading, setShowLoading] = useState(false); const [tags, setTags] = useState([]); const hasAccessToBookmarks = useHasAccess({ permissionType: PermissionTypes.BOOKMARKS, permission: Permissions.USE, }); const search = useRecoilValue(store.search); const { data, fetchNextPage, isFetchingNextPage, isLoading, isFetching, refetch } = useConversationsInfiniteQuery( { tags: tags.length === 0 ? undefined : tags, search: search.debouncedQuery || undefined, }, { enabled: isAuthenticated, staleTime: 30000, cacheTime: 300000, }, ); const computedHasNextPage = useMemo(() => { if (data?.pages && data.pages.length > 0) { const lastPage: ConversationListResponse = data.pages[data.pages.length - 1]; return lastPage.nextCursor !== null; } return false; }, [data?.pages]); const outerContainerRef = useRef(null); const listRef = useRef(null); const { moveToTop } = useNavScrolling({ setShowLoading, fetchNextPage: async (options?) => { if (computedHasNextPage) { return fetchNextPage(options); } return Promise.resolve( {} as InfiniteQueryObserverResult, ); }, isFetchingNext: isFetchingNextPage, }); const conversations = useMemo(() => { return data ? data.pages.flatMap((page) => page.conversations) : []; }, [data]); const toggleNavVisible = useCallback(() => { setNavVisible((prev: boolean) => { localStorage.setItem('navVisible', JSON.stringify(!prev)); return !prev; }); if (newUser) { setNewUser(false); } }, [newUser, setNavVisible, setNewUser]); const itemToggleNav = useCallback(() => { if (isSmallScreen) { toggleNavVisible(); } }, [isSmallScreen, toggleNavVisible]); useEffect(() => { if (isSmallScreen) { const savedNavVisible = localStorage.getItem('navVisible'); if (savedNavVisible === null) { toggleNavVisible(); } setNavWidth(NAV_WIDTH_MOBILE); } else { setNavWidth(NAV_WIDTH_DESKTOP); } }, [isSmallScreen, toggleNavVisible]); useEffect(() => { refetch(); }, [tags, refetch]); const loadMoreConversations = useCallback(() => { if (isFetchingNextPage || !computedHasNextPage) { return; } fetchNextPage(); }, [isFetchingNextPage, computedHasNextPage, fetchNextPage]); const subHeaders = useMemo( () => search.enabled === true && , [search.enabled, isSmallScreen], ); const headerButtons = useMemo( () => hasAccessToBookmarks && ( <>
), [hasAccessToBookmarks, tags, isSmallScreen], ); const [isSearchLoading, setIsSearchLoading] = useState( !!search.query && (search.isTyping || isLoading || isFetching), ); useEffect(() => { if (search.isTyping) { setIsSearchLoading(true); } else if (!isLoading && !isFetching) { setIsSearchLoading(false); } else if (!!search.query && (isLoading || isFetching)) { setIsSearchLoading(true); } }, [search.query, search.isTyping, isLoading, isFetching]); return ( <>
{isSmallScreen && } ); }, ); Nav.displayName = 'Nav'; export default Nav;