/* eslint-disable react-hooks/refs */ import { useWindowVirtualizer } from "@tanstack/react-virtual"; import { useAtom } from "jotai"; import * as React from "react"; import { useEffect, useLayoutEffect } from "react"; //import { useInView } from "react-intersection-observer"; import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { feedHeightsAtom, feedScrollIndexAtom } from "~/utils/atoms"; import { useInfiniteQueryFeedSkeleton } from "~/utils/useQuery"; interface InfiniteCustomFeedProps { feedUri: string; pdsUrl?: string; feedServiceDid?: string; initialScrollIndex?: number; //onVisibleIndexChange?: (index: number) => void; } export function InfiniteCustomFeed({ feedUri, pdsUrl, feedServiceDid, initialScrollIndex, //onVisibleIndexChange, }: InfiniteCustomFeedProps) { const OVERSCAN_COUNT = 10; const ESTIMATE_HEIGHT = 150; const { agent } = useAuth(); const authed = !!agent?.did; const listRef = React.useRef(null); const [offsetTop, setOffsetTop] = React.useState(0); const [scrollIndexes, setScrollIndexes] = useAtom(feedScrollIndexAtom); //const initialScrollIndex = scrollIndexes[feedUri]; // const identityresultmaybe = useQueryIdentity(agent?.did); // const identity = identityresultmaybe?.data; // const feedGenGetRecordQuery = useQueryArbitrary(feedUri); const { data, error, isLoading, isError, hasNextPage, fetchNextPage, isFetchingNextPage, refetch, isRefetching, } = useInfiniteQueryFeedSkeleton({ feedUri: feedUri, agent: agent ?? undefined, isAuthed: authed ?? false, pdsUrl: pdsUrl, feedServiceDid: feedServiceDid, }); const handleRefresh = () => { refetch(); }; //const { ref, inView } = useInView(); // React.useEffect(() => { // if (inView && hasNextPage && !isFetchingNextPage) { // fetchNextPage(); // } // }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); const allPosts = React.useMemo(() => { const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? []; const seenUris = new Set(); return flattenedPosts.filter((item) => { if (!item?.post) return false; if (seenUris.has(item.post)) { return false; } seenUris.add(item.post); return true; }); }, [data]); const [feedHeights, setFeedHeights] = useAtom(feedHeightsAtom); const currentFeedCache = feedHeights[feedUri] ?? {}; const virtualizerRef = React.useRef | null>(null); const virtualizer = useWindowVirtualizer({ count: allPosts.length, // + // (isFetchingNextPage ? 1 : 0) + // (hasNextPage && !isFetchingNextPage ? 1 : 0) + // (!hasNextPage ? 1 : 0) + // 1, estimateSize: (index) => { const post = allPosts[index]; if (!post) return ESTIMATE_HEIGHT; if (currentFeedCache[post.post]) { return currentFeedCache[post.post]; } return ESTIMATE_HEIGHT; }, // measureElement: measureElement, overscan: OVERSCAN_COUNT, scrollMargin: offsetTop, }); // React.useEffect(() => { // virtualizer.measure(); // }, [data]); const measureElement = React.useCallback( (node: HTMLElement | null) => { if (!node) return; virtualizer.measureElement(node); const postUri = node.dataset.postUri; const newHeight = node.offsetHeight; if (postUri && newHeight > 0 && currentFeedCache[postUri] !== newHeight) { setFeedHeights((prev) => ({ ...prev, [feedUri]: { ...prev[feedUri], [postUri]: newHeight, }, })); } }, [virtualizer, setFeedHeights, feedUri, currentFeedCache] ); virtualizerRef.current = virtualizer; useLayoutEffect(() => { const update = () => { if (listRef.current) { setOffsetTop(listRef.current.offsetTop); } //if (virtualizerRef.current) { // virtualizerRef.current.measure(); // } }; update(); let debounceTimeout: NodeJS.Timeout; const debouncedUpdate = () => { clearTimeout(debounceTimeout); debounceTimeout = setTimeout(update, 100); }; window.addEventListener("resize", debouncedUpdate); return () => { window.removeEventListener("resize", debouncedUpdate); clearTimeout(debounceTimeout); }; }, []); const hasRestoredScroll = React.useRef(false); useLayoutEffect(() => { if ( hasRestoredScroll.current || !initialScrollIndex || initialScrollIndex === 0 ) { return; } if (initialScrollIndex < allPosts.length) { console.log(`Restoring scroll to index: ${initialScrollIndex}`); virtualizer.scrollToIndex(initialScrollIndex, { align: "start", behavior: "auto", }); hasRestoredScroll.current = true; } }, [initialScrollIndex, allPosts.length, virtualizer]); // React.useEffect(() => { // const handleScroll = () => { // const topVisibleItem = virtualizer.getVirtualItems()[0]; // if (topVisibleItem && onVisibleIndexChange) { // onVisibleIndexChange(topVisibleItem.index); // } // }; // window.addEventListener('scroll', handleScroll, { passive: true }); // return () => window.removeEventListener('scroll', handleScroll); // }, [virtualizer, onVisibleIndexChange]); useEffect(() => { return () => { const topVisibleItem = virtualizer.getVirtualItems()[OVERSCAN_COUNT]; if (topVisibleItem) { console.log( `Saving final scroll index ${topVisibleItem.index} for feed ${feedUri}` ); setScrollIndexes((prev) => ({ ...prev, [feedUri]: topVisibleItem.index, })); } }; }, [virtualizer, feedUri, setScrollIndexes]); if (isLoading) { return
Loading feed...
; } if (isError) { return (
Error: {error.message}
); } if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { return (
No posts in this feed.
); } //if (offsetTop === 0) { // return
Calculating...
; //} return ( <>
{virtualizer.getVirtualItems().map((virtualItem) => { const item = allPosts[virtualItem.index]; const i = virtualItem.index; if (item) return ( ); })}
{/* allPosts?: {allPosts ? "true" : "false"} hasNextPage?: {hasNextPage ? "true" : "false"} isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */} {isFetchingNextPage && (
Loading more...
)} {hasNextPage && !isFetchingNextPage && ( )} {!hasNextPage && (
End of feed.
)} ); } const RefreshIcon = (props: React.SVGProps) => ( );