"use client" import { useRef, useEffect, useMemo } from "react" import { useVirtualizer } from "@tanstack/react-virtual" import { useTimeline } from "@/lib/queries/use-timeline" import { useSavedEntries } from "@/lib/queries/use-saved-entries" import { useCustomFeedTimeline } from "@/lib/queries/use-custom-feed-timeline" import { useUserInterfaceStore } from "@/lib/stores/user-interface-store" import { EntryListItem } from "./entry-list-item" import { usePrefetchEntryDetails } from "@/lib/hooks/use-prefetch-entry-details" interface EntryListProperties { feedFilter: "all" | "saved" folderIdentifier?: string | null feedIdentifier?: string | null customFeedIdentifier?: string | null } function useEntryData( feedFilter: "all" | "saved", folderIdentifier?: string | null, feedIdentifier?: string | null, customFeedIdentifier?: string | null, prioritiseUnread?: boolean ) { const timelineQuery = useTimeline( feedFilter === "all" && !customFeedIdentifier ? folderIdentifier : undefined, feedFilter === "all" && !customFeedIdentifier ? feedIdentifier : undefined, false, prioritiseUnread ) const savedQuery = useSavedEntries() const customFeedQuery = useCustomFeedTimeline( feedFilter === "all" ? (customFeedIdentifier ?? null) : null ) if (feedFilter === "saved") { return savedQuery } if (customFeedIdentifier) { return customFeedQuery } return timelineQuery } export function EntryList({ feedFilter, folderIdentifier, feedIdentifier, customFeedIdentifier, }: EntryListProperties) { const prioritiseUnreadEntries = useUserInterfaceStore( (state) => state.prioritiseUnreadEntries ) const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useEntryData(feedFilter, folderIdentifier, feedIdentifier, customFeedIdentifier, prioritiseUnreadEntries) const entryListViewMode = useUserInterfaceStore( (state) => state.entryListViewMode ) const selectedEntryIdentifier = useUserInterfaceStore( (state) => state.selectedEntryIdentifier ) const setSelectedEntryIdentifier = useUserInterfaceStore( (state) => state.setSelectedEntryIdentifier ) const focusedEntryIdentifier = useUserInterfaceStore( (state) => state.focusedEntryIdentifier ) const setFocusedEntryIdentifier = useUserInterfaceStore( (state) => state.setFocusedEntryIdentifier ) const setNavigableEntryIdentifiers = useUserInterfaceStore( (state) => state.setNavigableEntryIdentifiers ) const allEntries = useMemo(() => { return data?.pages.flatMap((page) => page) ?? [] }, [data]) const scrollContainerReference = useRef(null) const firstEntryIdentifier = allEntries[0]?.entryIdentifier const lastEntryIdentifier = allEntries[allEntries.length - 1]?.entryIdentifier const prefetchIdentifiers = useMemo( () => allEntries.map((entry) => entry.entryIdentifier), // eslint-disable-next-line react-hooks/exhaustive-deps [firstEntryIdentifier, lastEntryIdentifier, allEntries.length] ) usePrefetchEntryDetails(prefetchIdentifiers) useEffect(() => { setNavigableEntryIdentifiers(prefetchIdentifiers) }, [prefetchIdentifiers, setNavigableEntryIdentifiers]) function getEstimatedItemSize() { switch (entryListViewMode) { case "compact": return 40 case "comfortable": return 60 case "expanded": return 108 } } const virtualizer = useVirtualizer({ count: hasNextPage ? allEntries.length + 1 : allEntries.length, getScrollElement: () => scrollContainerReference.current, estimateSize: getEstimatedItemSize, overscan: 10, }) const virtualItems = virtualizer.getVirtualItems() useEffect(() => { const lastItem = virtualItems[virtualItems.length - 1] if (!lastItem) return if ( lastItem.index >= allEntries.length - 1 && hasNextPage && !isFetchingNextPage ) { fetchNextPage() } }, [ virtualItems, allEntries.length, hasNextPage, isFetchingNextPage, fetchNextPage, ]) const setIsEntryListAtTop = useUserInterfaceStore( (state) => state.setIsEntryListAtTop ) useEffect(() => { const scrollElement = scrollContainerReference.current if (!scrollElement) return function handleScroll() { if (!scrollElement) return useUserInterfaceStore.getState().setIsEntryListAtTop(scrollElement.scrollTop < 50) } handleScroll() scrollElement.addEventListener("scroll", handleScroll, { passive: true }) return () => scrollElement.removeEventListener("scroll", handleScroll) }, [setIsEntryListAtTop]) const allEntriesReference = useRef(allEntries) allEntriesReference.current = allEntries useEffect(() => { const activeIdentifier = focusedEntryIdentifier ?? selectedEntryIdentifier if (!activeIdentifier) return const activeIndex = allEntriesReference.current.findIndex( (entry) => entry.entryIdentifier === activeIdentifier ) if (activeIndex !== -1) { virtualizer.scrollToIndex(activeIndex, { align: "auto" }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [focusedEntryIdentifier, allEntries]) if (isLoading) { return (
{Array.from({ length: 8 }).map((_, skeletonIndex) => (
))}
) } if (allEntries.length === 0) { return (

{feedFilter === "saved" ? "no saved entries yet" : "no entries yet \u2014 add a feed to get started"}

) } return (
{virtualItems.map((virtualItem) => { const entry = allEntries[virtualItem.index] if (!entry) { return (

loading ...

) } return ( { setFocusedEntryIdentifier(entry.entryIdentifier) setSelectedEntryIdentifier(entry.entryIdentifier) }} measureReference={virtualizer.measureElement} virtualItem={virtualItem} /> ) })}
) }