From 9356009e165241c7b2fb1b8af255980edd290fd7 Mon Sep 17 00:00:00 2001 From: rimar1337 <132627503+rimar1337@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:29:25 +0700 Subject: [PATCH] minimal working virtual --- package-lock.json | 28 +++ package.json | 1 + src/components/InfiniteCustomFeed.tsx | 236 +++++++++++++++++++--- src/components/UniversalPostRenderer.tsx | 33 ++- src/routes/index.tsx | 245 +++++++++++++---------- src/utils/atoms.ts | 10 + 6 files changed, 418 insertions(+), 135 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f964d5..b027234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@tanstack/react-query-persist-client": "^5.85.6", "@tanstack/react-router": "^1.130.2", "@tanstack/react-router-devtools": "^1.131.5", + "@tanstack/react-virtual": "^3.13.12", "@tanstack/router-plugin": "^1.121.2", "idb-keyval": "^6.2.2", "jotai": "^2.13.1", @@ -2579,6 +2580,23 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "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/router-core": { "version": "1.131.28", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz", @@ -2737,6 +2755,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-file-routes": { "version": "1.131.2", "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.131.2.tgz", diff --git a/package.json b/package.json index 8b27774..56dc358 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@tanstack/react-query-persist-client": "^5.85.6", "@tanstack/react-router": "^1.130.2", "@tanstack/react-router-devtools": "^1.131.5", + "@tanstack/react-virtual": "^3.13.12", "@tanstack/router-plugin": "^1.121.2", "idb-keyval": "^6.2.2", "jotai": "^2.13.1", diff --git a/src/components/InfiniteCustomFeed.tsx b/src/components/InfiniteCustomFeed.tsx index 6795741..cf9a46a 100644 --- a/src/components/InfiniteCustomFeed.tsx +++ b/src/components/InfiniteCustomFeed.tsx @@ -1,27 +1,41 @@ +/* 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 { - useQueryArbitrary, - useQueryIdentity, - useInfiniteQueryFeedSkeleton, -} from "~/utils/useQuery"; +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); @@ -56,6 +70,154 @@ export function InfiniteCustomFeed({ // } // }, [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...
; } @@ -66,11 +228,6 @@ export function InfiniteCustomFeed({ ); } - const allPosts = - data?.pages.flatMap((page) => { - if (page) return page.feed; - }) ?? []; - if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { return (
@@ -79,19 +236,48 @@ export function InfiniteCustomFeed({ ); } + //if (offsetTop === 0) { + // return
Calculating...
; + //} + return ( <> - {allPosts.map((item, i) => { - if (item) - 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"} */} @@ -115,7 +301,11 @@ export function InfiniteCustomFeed({ className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed" aria-label="Refresh feed" > - {isRefetching ? : } + {isRefetching ? ( + + ) : ( + + )} ); @@ -138,4 +328,4 @@ const RefreshIcon = (props: React.SVGProps) => ( d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" > -); \ No newline at end of file +); diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index fba2ed8..0ec3347 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -28,6 +28,9 @@ export interface UniversalPostRendererATURILoaderProps { bottomBorder?: boolean; feedviewpost?: boolean; repostedby?: string; + style?: React.CSSProperties; + ref?: React.Ref; + dataIndexPropPass?: number; } // export async function cachedGetRecord({ @@ -132,6 +135,9 @@ export function UniversalPostRendererATURILoader({ bottomBorder = true, feedviewpost = false, repostedby, + style, + ref, + dataIndexPropPass, }: UniversalPostRendererATURILoaderProps) { // /*mass comment*/ console.log("atUri", atUri); //const { get, set } = usePersistentStore(); @@ -406,6 +412,9 @@ export function UniversalPostRendererATURILoader({ bottomBorder={bottomBorder} feedviewpost={feedviewpost} repostedby={repostedby} + style={style} + ref={ref} + dataIndexPropPass={dataIndexPropPass} /> ); } @@ -430,6 +439,9 @@ export function UniversalPostRendererRawRecordShim({ bottomBorder = true, feedviewpost = false, repostedby, + style, + ref, + dataIndexPropPass, }: { postRecord: any; profileRecord: any; @@ -444,6 +456,9 @@ export function UniversalPostRendererRawRecordShim({ bottomBorder?: boolean; feedviewpost?: boolean; repostedby?: string; + style?: React.CSSProperties; + ref?: React.Ref; + dataIndexPropPass?: number; }) { // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); const navigate = useNavigate(); @@ -638,6 +653,9 @@ export function UniversalPostRendererRawRecordShim({ //extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}} feedviewpostreplyhandle={feedviewpostreplyhandle} repostedby={feedviewpostrepostedbyhandle} + style={style} + ref={ref} + dataIndexPropPass={dataIndexPropPass} /> ); @@ -1079,6 +1097,9 @@ function UniversalPostRenderer({ feedviewpostreplyhandle, depth = 0, repostedby, + style, + ref, + dataIndexPropPass, }: { post: PostView; // optional for now because i havent ported every use to this yet @@ -1098,6 +1119,9 @@ function UniversalPostRenderer({ feedviewpostreplyhandle?: string; depth?: number; repostedby?: string; + style?: React.CSSProperties; + ref?: React.Ref; + dataIndexPropPass?: number; }) { const navigate = useNavigate(); const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom); @@ -1171,8 +1195,12 @@ function UniversalPostRenderer({ /* fuck you */ const isMainItem = false; const setMainItem = (any: any) => {}; + // eslint-disable-next-line react-hooks/refs + console.log("Received ref in UniversalPostRenderer:", ref); return ( +
+
); } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 03e030b..c029a02 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,88 +1,88 @@ import { createFileRoute } from "@tanstack/react-router"; import { useAtom } from "jotai"; import * as React from "react"; -import { useEffect, useLayoutEffect } from "react"; +import { useEffect } from "react"; import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { agentAtom, authedAtom, - feedScrollPositionsAtom, + feedScrollIndexAtom, selectedFeedUriAtom, store, } from "~/utils/atoms"; //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; import { - constructArbitraryQuery, - constructIdentityQuery, - constructInfiniteFeedSkeletonQuery, - constructPostQuery, + //constructArbitraryQuery, + //constructIdentityQuery, + //constructInfiniteFeedSkeletonQuery, + //constructPostQuery, useQueryArbitrary, useQueryIdentity, useQueryPreferences, } from "~/utils/useQuery"; export const Route = createFileRoute("/")({ - loader: async ({ context }) => { - const { queryClient } = context; - const atomauth = store.get(authedAtom); - const atomagent = store.get(agentAtom); - - let identitypds: string | undefined; - const initialselectedfeed = store.get(selectedFeedUriAtom); - if (atomagent && atomauth && atomagent?.did) { - const identityopts = constructIdentityQuery(atomagent.did); - const identityresultmaybe = - await queryClient.ensureQueryData(identityopts); - identitypds = identityresultmaybe?.pds; - } + // loader: async ({ context }) => { + // const { queryClient } = context; + // const atomauth = store.get(authedAtom); + // const atomagent = store.get(agentAtom); + + // let identitypds: string | undefined; + // const initialselectedfeed = store.get(selectedFeedUriAtom); + // if (atomagent && atomauth && atomagent?.did) { + // const identityopts = constructIdentityQuery(atomagent.did); + // const identityresultmaybe = + // await queryClient.ensureQueryData(identityopts); + // identitypds = identityresultmaybe?.pds; + // } - const arbitraryopts = constructArbitraryQuery( - initialselectedfeed ?? - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" - ); - const feedGengetrecordquery = - await queryClient.ensureQueryData(arbitraryopts); - const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; - //queryClient.ensureInfiniteQueryData() - - const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ - feedUri: - initialselectedfeed ?? - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", - agent: atomagent ?? undefined, - isAuthed: atomauth ?? false, - pdsUrl: identitypds, - feedServiceDid: feedServiceDid, - }); - - const res = await queryClient.ensureInfiniteQueryData({ - queryKey, - queryFn, - initialPageParam: undefined as never, - getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, - staleTime: Infinity, - //refetchOnWindowFocus: false, - //enabled: true, - }); - await Promise.all( - res.pages.map(async (page) => { - await Promise.all( - page.feed.map(async (feedviewpost) => { - if (!feedviewpost.post) return; - // /*mass comment*/ console.log("preloading: ", feedviewpost.post); - const opts = constructPostQuery(feedviewpost.post); - try { - await queryClient.ensureQueryData(opts); - } catch (e) { - // /*mass comment*/ console.log(" failed:", e); - } - }) - ); - }) - ); - }, + // const arbitraryopts = constructArbitraryQuery( + // initialselectedfeed ?? + // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" + // ); + // const feedGengetrecordquery = + // await queryClient.ensureQueryData(arbitraryopts); + // const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; + // //queryClient.ensureInfiniteQueryData() + + // const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ + // feedUri: + // initialselectedfeed ?? + // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", + // agent: atomagent ?? undefined, + // isAuthed: atomauth ?? false, + // pdsUrl: identitypds, + // feedServiceDid: feedServiceDid, + // }); + + // const res = await queryClient.ensureInfiniteQueryData({ + // queryKey, + // queryFn, + // initialPageParam: undefined as never, + // getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, + // staleTime: Infinity, + // //refetchOnWindowFocus: false, + // //enabled: true, + // }); + // await Promise.all( + // res.pages.map(async (page) => { + // await Promise.all( + // page.feed.map(async (feedviewpost) => { + // if (!feedviewpost.post) return; + // // /*mass comment*/ console.log("preloading: ", feedviewpost.post); + // const opts = constructPostQuery(feedviewpost.post); + // try { + // await queryClient.ensureQueryData(opts); + // } catch (e) { + // // /*mass comment*/ console.log(" failed:", e); + // } + // }) + // ); + // }) + // ); + // }, component: Home, pendingComponent: PendingHome, }); @@ -288,56 +288,75 @@ function Home() { // }; // }, [authed, agent, loadering, selectedFeed, get, set]); - const [scrollPositions, setScrollPositions] = useAtom( - feedScrollPositionsAtom - ); + // const [scrollPositions, setScrollPositions] = useAtom( + // feedScrollPositionsAtom + // ); - const scrollRef = React.useRef>({}); + const [scrollIndexes] = useAtom(feedScrollIndexAtom); - useEffect(() => { - const onScroll = () => { - //if (!selectedFeed) return; - scrollRef.current[selectedFeed ?? "null"] = window.scrollY; - }; - window.addEventListener("scroll", onScroll, { passive: true }); - return () => window.removeEventListener("scroll", onScroll); - }, [selectedFeed]); - const [donerestored, setdonerestored] = React.useState(false); + //const latestVisibleIndexRef = React.useRef(0); - useEffect(() => { - return () => { - if (!donerestored) return; - // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current); - //if (!selectedFeed) return; - setScrollPositions((prev) => ({ - ...prev, - [selectedFeed ?? "null"]: - scrollRef.current[selectedFeed ?? "null"] ?? 0, - })); - }; - }, [selectedFeed, setScrollPositions, donerestored]); - - const [restoringScrollPosition, setRestoringScrollPosition] = - React.useState(false); - - useLayoutEffect(() => { - setRestoringScrollPosition(true); - const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; - - const raf = requestAnimationFrame(() => { - // setRestoringScrollPosition(true); - // raf = requestAnimationFrame(() => { - // window.scrollTo({ top: savedPosition, behavior: "instant" }); - // setRestoringScrollPosition(false); - // setdonerestored(true); - // }); - window.scrollTo({ top: savedPosition, behavior: "instant" }); - setRestoringScrollPosition(false); - setdonerestored(true); - }); - - return () => cancelAnimationFrame(raf); - }, [selectedFeed, scrollPositions]); + // const handleVisibleIndexChange = React.useCallback((index: number) => { + // latestVisibleIndexRef.current = index; + // }, []); + + // React.useEffect(() => { + // // This return function is the cleanup effect. + // return () => { + // if (selectedFeed) { + // console.log(`Saving scroll index ${latestVisibleIndexRef.current} for feed ${selectedFeed}`); + // setScrollIndexes((prev) => ({ + // ...prev, + // [selectedFeed]: latestVisibleIndexRef.current, + // })); + // } + // }; + // }, [selectedFeed, setScrollIndexes]); + + // useEffect(() => { + // const onScroll = () => { + // //if (!selectedFeed) return; + // scrollRef.current[selectedFeed ?? "null"] = window.scrollY; + // }; + // window.addEventListener("scroll", onScroll, { passive: true }); + // return () => window.removeEventListener("scroll", onScroll); + // }, [selectedFeed]); + // const [donerestored, setdonerestored] = React.useState(false); + + // useEffect(() => { + // return () => { + // if (!donerestored) return; + // // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current); + // //if (!selectedFeed) return; + // setScrollPositions((prev) => ({ + // ...prev, + // [selectedFeed ?? "null"]: + // scrollRef.current[selectedFeed ?? "null"] ?? 0, + // })); + // }; + // }, [selectedFeed, setScrollPositions, donerestored]); + + // const [restoringScrollPosition, setRestoringScrollPosition] = + // React.useState(false); + + // useLayoutEffect(() => { + // setRestoringScrollPosition(true); + // const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; + + // const raf = requestAnimationFrame(() => { + // // setRestoringScrollPosition(true); + // // raf = requestAnimationFrame(() => { + // // window.scrollTo({ top: savedPosition, behavior: "instant" }); + // // setRestoringScrollPosition(false); + // // setdonerestored(true); + // // }); + // window.scrollTo({ top: savedPosition, behavior: "instant" }); + // setRestoringScrollPosition(false); + // setdonerestored(true); + // }); + + // return () => cancelAnimationFrame(raf); + // }, [selectedFeed, scrollPositions]); const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; @@ -360,6 +379,8 @@ function Home() { authed && agent && identity?.pds && feedServiceDid; const isReadyForUnauthedFeed = !authed && selectedFeed; + const savedIndex = selectedFeed ? scrollIndexes[selectedFeed] : 0; + return (
@@ -416,6 +437,8 @@ function Home() { feedUri={selectedFeed!} pdsUrl={identity?.pds} feedServiceDid={feedServiceDid} + initialScrollIndex={savedIndex} + //onVisibleIndexChange={handleVisibleIndexChange} /> ) : (
diff --git a/src/utils/atoms.ts b/src/utils/atoms.ts index 5f3a203..94f6a48 100644 --- a/src/utils/atoms.ts +++ b/src/utils/atoms.ts @@ -11,11 +11,21 @@ export const selectedFeedUriAtom = atomWithStorage( //export const feedScrollPositionsAtom = atom>({}); +/** + * @deprecated use the Tanstack Virtual index thanks + */ export const feedScrollPositionsAtom = atomWithStorage>( 'feedscrollpositions', {} ); +export const feedScrollIndexAtom = atomWithStorage>('feedScrollIndexes',{}); + +export const feedHeightsAtom = atomWithStorage>>( + 'feedPostHeights', + {} +); + export const likedPostsAtom = atomWithStorage>( 'likedPosts', {} -- 2.51.2