an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

minimal working virtual

rimar1337 9356009e 833207ed

+28
package-lock.json
··· 15 15 "@tanstack/react-query-persist-client": "^5.85.6", 16 16 "@tanstack/react-router": "^1.130.2", 17 17 "@tanstack/react-router-devtools": "^1.131.5", 18 + "@tanstack/react-virtual": "^3.13.12", 18 19 "@tanstack/router-plugin": "^1.121.2", 19 20 "idb-keyval": "^6.2.2", 20 21 "jotai": "^2.13.1", ··· 2579 2580 "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 2580 2581 } 2581 2582 }, 2583 + "node_modules/@tanstack/react-virtual": { 2584 + "version": "3.13.12", 2585 + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", 2586 + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", 2587 + "license": "MIT", 2588 + "dependencies": { 2589 + "@tanstack/virtual-core": "3.13.12" 2590 + }, 2591 + "funding": { 2592 + "type": "github", 2593 + "url": "https://github.com/sponsors/tannerlinsley" 2594 + }, 2595 + "peerDependencies": { 2596 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 2597 + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 2598 + } 2599 + }, 2582 2600 "node_modules/@tanstack/router-core": { 2583 2601 "version": "1.131.28", 2584 2602 "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz", ··· 2731 2749 "version": "0.7.4", 2732 2750 "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz", 2733 2751 "integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==", 2752 + "license": "MIT", 2753 + "funding": { 2754 + "type": "github", 2755 + "url": "https://github.com/sponsors/tannerlinsley" 2756 + } 2757 + }, 2758 + "node_modules/@tanstack/virtual-core": { 2759 + "version": "3.13.12", 2760 + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", 2761 + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", 2734 2762 "license": "MIT", 2735 2763 "funding": { 2736 2764 "type": "github",
+1
package.json
··· 19 19 "@tanstack/react-query-persist-client": "^5.85.6", 20 20 "@tanstack/react-router": "^1.130.2", 21 21 "@tanstack/react-router-devtools": "^1.131.5", 22 + "@tanstack/react-virtual": "^3.13.12", 22 23 "@tanstack/router-plugin": "^1.121.2", 23 24 "idb-keyval": "^6.2.2", 24 25 "jotai": "^2.13.1",
+213 -23
src/components/InfiniteCustomFeed.tsx
··· 1 + /* eslint-disable react-hooks/refs */ 2 + import { useWindowVirtualizer } from "@tanstack/react-virtual"; 3 + import { useAtom } from "jotai"; 1 4 import * as React from "react"; 5 + import { useEffect, useLayoutEffect } from "react"; 6 + 2 7 //import { useInView } from "react-intersection-observer"; 3 8 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 4 9 import { useAuth } from "~/providers/UnifiedAuthProvider"; 5 - import { 6 - useQueryArbitrary, 7 - useQueryIdentity, 8 - useInfiniteQueryFeedSkeleton, 9 - } from "~/utils/useQuery"; 10 + import { feedHeightsAtom, feedScrollIndexAtom } from "~/utils/atoms"; 11 + import { useInfiniteQueryFeedSkeleton } from "~/utils/useQuery"; 10 12 11 13 interface InfiniteCustomFeedProps { 12 14 feedUri: string; 13 15 pdsUrl?: string; 14 16 feedServiceDid?: string; 17 + initialScrollIndex?: number; 18 + //onVisibleIndexChange?: (index: number) => void; 15 19 } 16 20 17 21 export function InfiniteCustomFeed({ 18 22 feedUri, 19 23 pdsUrl, 20 24 feedServiceDid, 25 + initialScrollIndex, 26 + //onVisibleIndexChange, 21 27 }: InfiniteCustomFeedProps) { 28 + const OVERSCAN_COUNT = 10; 29 + const ESTIMATE_HEIGHT = 150; 30 + 22 31 const { agent } = useAuth(); 23 32 const authed = !!agent?.did; 33 + 34 + const listRef = React.useRef<HTMLDivElement | null>(null); 35 + const [offsetTop, setOffsetTop] = React.useState(0); 36 + const [scrollIndexes, setScrollIndexes] = useAtom(feedScrollIndexAtom); 37 + //const initialScrollIndex = scrollIndexes[feedUri]; 24 38 25 39 // const identityresultmaybe = useQueryIdentity(agent?.did); 26 40 // const identity = identityresultmaybe?.data; ··· 56 70 // } 57 71 // }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); 58 72 73 + const allPosts = React.useMemo(() => { 74 + const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? []; 75 + 76 + const seenUris = new Set<string>(); 77 + 78 + return flattenedPosts.filter((item) => { 79 + if (!item?.post) return false; 80 + 81 + if (seenUris.has(item.post)) { 82 + return false; 83 + } 84 + 85 + seenUris.add(item.post); 86 + return true; 87 + }); 88 + }, [data]); 89 + 90 + const [feedHeights, setFeedHeights] = useAtom(feedHeightsAtom); 91 + const currentFeedCache = feedHeights[feedUri] ?? {}; 92 + 93 + const virtualizerRef = React.useRef<ReturnType< 94 + typeof useWindowVirtualizer 95 + > | null>(null); 96 + 97 + const virtualizer = useWindowVirtualizer({ 98 + count: allPosts.length, 99 + // + 100 + // (isFetchingNextPage ? 1 : 0) + 101 + // (hasNextPage && !isFetchingNextPage ? 1 : 0) + 102 + // (!hasNextPage ? 1 : 0) + 103 + // 1, 104 + estimateSize: (index) => { 105 + const post = allPosts[index]; 106 + if (!post) return ESTIMATE_HEIGHT; 107 + 108 + if (currentFeedCache[post.post]) { 109 + return currentFeedCache[post.post]; 110 + } 111 + 112 + return ESTIMATE_HEIGHT; 113 + }, 114 + // measureElement: measureElement, 115 + overscan: OVERSCAN_COUNT, 116 + scrollMargin: offsetTop, 117 + }); 118 + // React.useEffect(() => { 119 + // virtualizer.measure(); 120 + // }, [data]); 121 + 122 + const measureElement = React.useCallback( 123 + (node: HTMLElement | null) => { 124 + if (!node) return; 125 + 126 + virtualizer.measureElement(node); 127 + 128 + const postUri = node.dataset.postUri; 129 + const newHeight = node.offsetHeight; 130 + 131 + if (postUri && newHeight > 0 && currentFeedCache[postUri] !== newHeight) { 132 + setFeedHeights((prev) => ({ 133 + ...prev, 134 + [feedUri]: { 135 + ...prev[feedUri], 136 + [postUri]: newHeight, 137 + }, 138 + })); 139 + } 140 + }, 141 + [virtualizer, setFeedHeights, feedUri, currentFeedCache] 142 + ); 143 + 144 + virtualizerRef.current = virtualizer; 145 + 146 + useLayoutEffect(() => { 147 + const update = () => { 148 + if (listRef.current) { 149 + setOffsetTop(listRef.current.offsetTop); 150 + } 151 + //if (virtualizerRef.current) { 152 + // virtualizerRef.current.measure(); 153 + // } 154 + }; 155 + 156 + update(); 157 + 158 + let debounceTimeout: NodeJS.Timeout; 159 + 160 + const debouncedUpdate = () => { 161 + clearTimeout(debounceTimeout); 162 + debounceTimeout = setTimeout(update, 100); 163 + }; 164 + 165 + window.addEventListener("resize", debouncedUpdate); 166 + 167 + return () => { 168 + window.removeEventListener("resize", debouncedUpdate); 169 + clearTimeout(debounceTimeout); 170 + }; 171 + }, []); 172 + 173 + const hasRestoredScroll = React.useRef(false); 174 + useLayoutEffect(() => { 175 + if ( 176 + hasRestoredScroll.current || 177 + !initialScrollIndex || 178 + initialScrollIndex === 0 179 + ) { 180 + return; 181 + } 182 + 183 + if (initialScrollIndex < allPosts.length) { 184 + console.log(`Restoring scroll to index: ${initialScrollIndex}`); 185 + virtualizer.scrollToIndex(initialScrollIndex, { 186 + align: "start", 187 + behavior: "auto", 188 + }); 189 + hasRestoredScroll.current = true; 190 + } 191 + }, [initialScrollIndex, allPosts.length, virtualizer]); 192 + 193 + // React.useEffect(() => { 194 + // const handleScroll = () => { 195 + // const topVisibleItem = virtualizer.getVirtualItems()[0]; 196 + // if (topVisibleItem && onVisibleIndexChange) { 197 + // onVisibleIndexChange(topVisibleItem.index); 198 + // } 199 + // }; 200 + 201 + // window.addEventListener('scroll', handleScroll, { passive: true }); 202 + // return () => window.removeEventListener('scroll', handleScroll); 203 + // }, [virtualizer, onVisibleIndexChange]); 204 + 205 + useEffect(() => { 206 + return () => { 207 + const topVisibleItem = virtualizer.getVirtualItems()[OVERSCAN_COUNT]; 208 + 209 + if (topVisibleItem) { 210 + console.log( 211 + `Saving final scroll index ${topVisibleItem.index} for feed ${feedUri}` 212 + ); 213 + setScrollIndexes((prev) => ({ 214 + ...prev, 215 + [feedUri]: topVisibleItem.index, 216 + })); 217 + } 218 + }; 219 + }, [virtualizer, feedUri, setScrollIndexes]); 220 + 59 221 if (isLoading) { 60 222 return <div className="p-4 text-center text-gray-500">Loading feed...</div>; 61 223 } ··· 65 227 <div className="p-4 text-center text-red-500">Error: {error.message}</div> 66 228 ); 67 229 } 68 - 69 - const allPosts = 70 - data?.pages.flatMap((page) => { 71 - if (page) return page.feed; 72 - }) ?? []; 73 230 74 231 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 75 232 return ( ··· 79 236 ); 80 237 } 81 238 239 + //if (offsetTop === 0) { 240 + // return <div ref={listRef}>Calculating...</div>; 241 + //} 242 + 82 243 return ( 83 244 <> 84 - {allPosts.map((item, i) => { 85 - if (item) 86 - return ( 87 - <UniversalPostRendererATURILoader 88 - key={item.post || i} 89 - atUri={item.post} 90 - feedviewpost={true} 91 - repostedby={!!item.reason?.$type && (item.reason as any)?.repost} 92 - /> 93 - ); 94 - })} 245 + <div ref={listRef}> 246 + <div 247 + style={{ 248 + height: `${virtualizer.getTotalSize()}px`, 249 + width: "100%", 250 + position: "relative", 251 + }} 252 + > 253 + {virtualizer.getVirtualItems().map((virtualItem) => { 254 + const item = allPosts[virtualItem.index]; 255 + const i = virtualItem.index; 256 + if (item) 257 + return ( 258 + <UniversalPostRendererATURILoader 259 + key={item.post || i} 260 + atUri={item.post} 261 + dataIndexPropPass={i} 262 + feedviewpost={true} 263 + ref={measureElement} 264 + repostedby={ 265 + !!item.reason?.$type && (item.reason as any)?.repost 266 + } 267 + style={{ 268 + position: "absolute", 269 + top: 0, 270 + left: 0, 271 + width: "100%", 272 + //height: `${item.size}px`, 273 + transform: `translateY(${virtualItem.start - offsetTop}px)`, 274 + }} 275 + /> 276 + ); 277 + })} 278 + </div> 279 + </div> 280 + 95 281 {/* allPosts?: {allPosts ? "true" : "false"} 96 282 hasNextPage?: {hasNextPage ? "true" : "false"} 97 283 isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */} ··· 115 301 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" 116 302 aria-label="Refresh feed" 117 303 > 118 - {isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />} 304 + {isRefetching ? ( 305 + <RefreshIcon className="h-6 w-6 animate-spin" /> 306 + ) : ( 307 + <RefreshIcon className="h-6 w-6" /> 308 + )} 119 309 </button> 120 310 </> 121 311 ); ··· 138 328 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" 139 329 ></path> 140 330 </svg> 141 - ); 331 + );
+32 -1
src/components/UniversalPostRenderer.tsx
··· 28 28 bottomBorder?: boolean; 29 29 feedviewpost?: boolean; 30 30 repostedby?: string; 31 + style?: React.CSSProperties; 32 + ref?: React.Ref<HTMLDivElement>; 33 + dataIndexPropPass?: number; 31 34 } 32 35 33 36 // export async function cachedGetRecord({ ··· 132 135 bottomBorder = true, 133 136 feedviewpost = false, 134 137 repostedby, 138 + style, 139 + ref, 140 + dataIndexPropPass, 135 141 }: UniversalPostRendererATURILoaderProps) { 136 142 // /*mass comment*/ console.log("atUri", atUri); 137 143 //const { get, set } = usePersistentStore(); ··· 406 412 bottomBorder={bottomBorder} 407 413 feedviewpost={feedviewpost} 408 414 repostedby={repostedby} 415 + style={style} 416 + ref={ref} 417 + dataIndexPropPass={dataIndexPropPass} 409 418 /> 410 419 ); 411 420 } ··· 430 439 bottomBorder = true, 431 440 feedviewpost = false, 432 441 repostedby, 442 + style, 443 + ref, 444 + dataIndexPropPass, 433 445 }: { 434 446 postRecord: any; 435 447 profileRecord: any; ··· 444 456 bottomBorder?: boolean; 445 457 feedviewpost?: boolean; 446 458 repostedby?: string; 459 + style?: React.CSSProperties; 460 + ref?: React.Ref<HTMLDivElement>; 461 + dataIndexPropPass?: number; 447 462 }) { 448 463 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 449 464 const navigate = useNavigate(); ··· 638 653 //extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}} 639 654 feedviewpostreplyhandle={feedviewpostreplyhandle} 640 655 repostedby={feedviewpostrepostedbyhandle} 656 + style={style} 657 + ref={ref} 658 + dataIndexPropPass={dataIndexPropPass} 641 659 /> 642 660 </> 643 661 ); ··· 1079 1097 feedviewpostreplyhandle, 1080 1098 depth = 0, 1081 1099 repostedby, 1100 + style, 1101 + ref, 1102 + dataIndexPropPass, 1082 1103 }: { 1083 1104 post: PostView; 1084 1105 // optional for now because i havent ported every use to this yet ··· 1098 1119 feedviewpostreplyhandle?: string; 1099 1120 depth?: number; 1100 1121 repostedby?: string; 1122 + style?: React.CSSProperties; 1123 + ref?: React.Ref<HTMLDivElement>; 1124 + dataIndexPropPass?: number; 1101 1125 }) { 1102 1126 const navigate = useNavigate(); 1103 1127 const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom); ··· 1171 1195 /* fuck you */ 1172 1196 const isMainItem = false; 1173 1197 const setMainItem = (any: any) => {}; 1198 + // eslint-disable-next-line react-hooks/refs 1199 + console.log("Received ref in UniversalPostRenderer:", ref); 1174 1200 return ( 1201 + <div ref={ref} style={style} data-index={dataIndexPropPass}> 1175 1202 <div 1203 + //ref={ref} 1176 1204 key={salt + "-" + (post.uri || emergencySalt)} 1177 1205 onClick={ 1178 1206 isMainItem ··· 1188 1216 } 1189 1217 : undefined 1190 1218 } 1191 - style={{ 1219 + style={ 1220 + { 1221 + //...style, 1192 1222 //border: "1px solid #e1e8ed", 1193 1223 //borderRadius: 12, 1194 1224 opacity: "1 !important", ··· 1572 1602 /> 1573 1603 </div> 1574 1604 </div> 1605 + </div> 1575 1606 </div> 1576 1607 ); 1577 1608 }
+127 -104
src/routes/index.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 import { useAtom } from "jotai"; 3 3 import * as React from "react"; 4 - import { useEffect, useLayoutEffect } from "react"; 4 + import { useEffect } from "react"; 5 5 6 6 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 7 7 import { useAuth } from "~/providers/UnifiedAuthProvider"; 8 8 import { 9 9 agentAtom, 10 10 authedAtom, 11 - feedScrollPositionsAtom, 11 + feedScrollIndexAtom, 12 12 selectedFeedUriAtom, 13 13 store, 14 14 } from "~/utils/atoms"; 15 15 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 16 16 import { 17 - constructArbitraryQuery, 18 - constructIdentityQuery, 19 - constructInfiniteFeedSkeletonQuery, 20 - constructPostQuery, 17 + //constructArbitraryQuery, 18 + //constructIdentityQuery, 19 + //constructInfiniteFeedSkeletonQuery, 20 + //constructPostQuery, 21 21 useQueryArbitrary, 22 22 useQueryIdentity, 23 23 useQueryPreferences, 24 24 } from "~/utils/useQuery"; 25 25 26 26 export const Route = createFileRoute("/")({ 27 - loader: async ({ context }) => { 28 - const { queryClient } = context; 29 - const atomauth = store.get(authedAtom); 30 - const atomagent = store.get(agentAtom); 27 + // loader: async ({ context }) => { 28 + // const { queryClient } = context; 29 + // const atomauth = store.get(authedAtom); 30 + // const atomagent = store.get(agentAtom); 31 31 32 - let identitypds: string | undefined; 33 - const initialselectedfeed = store.get(selectedFeedUriAtom); 34 - if (atomagent && atomauth && atomagent?.did) { 35 - const identityopts = constructIdentityQuery(atomagent.did); 36 - const identityresultmaybe = 37 - await queryClient.ensureQueryData(identityopts); 38 - identitypds = identityresultmaybe?.pds; 39 - } 32 + // let identitypds: string | undefined; 33 + // const initialselectedfeed = store.get(selectedFeedUriAtom); 34 + // if (atomagent && atomauth && atomagent?.did) { 35 + // const identityopts = constructIdentityQuery(atomagent.did); 36 + // const identityresultmaybe = 37 + // await queryClient.ensureQueryData(identityopts); 38 + // identitypds = identityresultmaybe?.pds; 39 + // } 40 40 41 - const arbitraryopts = constructArbitraryQuery( 42 - initialselectedfeed ?? 43 - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" 44 - ); 45 - const feedGengetrecordquery = 46 - await queryClient.ensureQueryData(arbitraryopts); 47 - const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; 48 - //queryClient.ensureInfiniteQueryData() 41 + // const arbitraryopts = constructArbitraryQuery( 42 + // initialselectedfeed ?? 43 + // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" 44 + // ); 45 + // const feedGengetrecordquery = 46 + // await queryClient.ensureQueryData(arbitraryopts); 47 + // const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; 48 + // //queryClient.ensureInfiniteQueryData() 49 49 50 - const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ 51 - feedUri: 52 - initialselectedfeed ?? 53 - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 54 - agent: atomagent ?? undefined, 55 - isAuthed: atomauth ?? false, 56 - pdsUrl: identitypds, 57 - feedServiceDid: feedServiceDid, 58 - }); 50 + // const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ 51 + // feedUri: 52 + // initialselectedfeed ?? 53 + // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 54 + // agent: atomagent ?? undefined, 55 + // isAuthed: atomauth ?? false, 56 + // pdsUrl: identitypds, 57 + // feedServiceDid: feedServiceDid, 58 + // }); 59 59 60 - const res = await queryClient.ensureInfiniteQueryData({ 61 - queryKey, 62 - queryFn, 63 - initialPageParam: undefined as never, 64 - getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, 65 - staleTime: Infinity, 66 - //refetchOnWindowFocus: false, 67 - //enabled: true, 68 - }); 69 - await Promise.all( 70 - res.pages.map(async (page) => { 71 - await Promise.all( 72 - page.feed.map(async (feedviewpost) => { 73 - if (!feedviewpost.post) return; 74 - // /*mass comment*/ console.log("preloading: ", feedviewpost.post); 75 - const opts = constructPostQuery(feedviewpost.post); 76 - try { 77 - await queryClient.ensureQueryData(opts); 78 - } catch (e) { 79 - // /*mass comment*/ console.log(" failed:", e); 80 - } 81 - }) 82 - ); 83 - }) 84 - ); 85 - }, 60 + // const res = await queryClient.ensureInfiniteQueryData({ 61 + // queryKey, 62 + // queryFn, 63 + // initialPageParam: undefined as never, 64 + // getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, 65 + // staleTime: Infinity, 66 + // //refetchOnWindowFocus: false, 67 + // //enabled: true, 68 + // }); 69 + // await Promise.all( 70 + // res.pages.map(async (page) => { 71 + // await Promise.all( 72 + // page.feed.map(async (feedviewpost) => { 73 + // if (!feedviewpost.post) return; 74 + // // /*mass comment*/ console.log("preloading: ", feedviewpost.post); 75 + // const opts = constructPostQuery(feedviewpost.post); 76 + // try { 77 + // await queryClient.ensureQueryData(opts); 78 + // } catch (e) { 79 + // // /*mass comment*/ console.log(" failed:", e); 80 + // } 81 + // }) 82 + // ); 83 + // }) 84 + // ); 85 + // }, 86 86 component: Home, 87 87 pendingComponent: PendingHome, 88 88 }); ··· 288 288 // }; 289 289 // }, [authed, agent, loadering, selectedFeed, get, set]); 290 290 291 - const [scrollPositions, setScrollPositions] = useAtom( 292 - feedScrollPositionsAtom 293 - ); 291 + // const [scrollPositions, setScrollPositions] = useAtom( 292 + // feedScrollPositionsAtom 293 + // ); 294 294 295 - const scrollRef = React.useRef<Record<string, number>>({}); 295 + const [scrollIndexes] = useAtom(feedScrollIndexAtom); 296 + 297 + //const latestVisibleIndexRef = React.useRef(0); 298 + 299 + // const handleVisibleIndexChange = React.useCallback((index: number) => { 300 + // latestVisibleIndexRef.current = index; 301 + // }, []); 296 302 297 - useEffect(() => { 298 - const onScroll = () => { 299 - //if (!selectedFeed) return; 300 - scrollRef.current[selectedFeed ?? "null"] = window.scrollY; 301 - }; 302 - window.addEventListener("scroll", onScroll, { passive: true }); 303 - return () => window.removeEventListener("scroll", onScroll); 304 - }, [selectedFeed]); 305 - const [donerestored, setdonerestored] = React.useState(false); 303 + // React.useEffect(() => { 304 + // // This return function is the cleanup effect. 305 + // return () => { 306 + // if (selectedFeed) { 307 + // console.log(`Saving scroll index ${latestVisibleIndexRef.current} for feed ${selectedFeed}`); 308 + // setScrollIndexes((prev) => ({ 309 + // ...prev, 310 + // [selectedFeed]: latestVisibleIndexRef.current, 311 + // })); 312 + // } 313 + // }; 314 + // }, [selectedFeed, setScrollIndexes]); 315 + 316 + // useEffect(() => { 317 + // const onScroll = () => { 318 + // //if (!selectedFeed) return; 319 + // scrollRef.current[selectedFeed ?? "null"] = window.scrollY; 320 + // }; 321 + // window.addEventListener("scroll", onScroll, { passive: true }); 322 + // return () => window.removeEventListener("scroll", onScroll); 323 + // }, [selectedFeed]); 324 + // const [donerestored, setdonerestored] = React.useState(false); 306 325 307 - useEffect(() => { 308 - return () => { 309 - if (!donerestored) return; 310 - // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current); 311 - //if (!selectedFeed) return; 312 - setScrollPositions((prev) => ({ 313 - ...prev, 314 - [selectedFeed ?? "null"]: 315 - scrollRef.current[selectedFeed ?? "null"] ?? 0, 316 - })); 317 - }; 318 - }, [selectedFeed, setScrollPositions, donerestored]); 326 + // useEffect(() => { 327 + // return () => { 328 + // if (!donerestored) return; 329 + // // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current); 330 + // //if (!selectedFeed) return; 331 + // setScrollPositions((prev) => ({ 332 + // ...prev, 333 + // [selectedFeed ?? "null"]: 334 + // scrollRef.current[selectedFeed ?? "null"] ?? 0, 335 + // })); 336 + // }; 337 + // }, [selectedFeed, setScrollPositions, donerestored]); 319 338 320 - const [restoringScrollPosition, setRestoringScrollPosition] = 321 - React.useState(false); 339 + // const [restoringScrollPosition, setRestoringScrollPosition] = 340 + // React.useState(false); 322 341 323 - useLayoutEffect(() => { 324 - setRestoringScrollPosition(true); 325 - const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 342 + // useLayoutEffect(() => { 343 + // setRestoringScrollPosition(true); 344 + // const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 326 345 327 - const raf = requestAnimationFrame(() => { 328 - // setRestoringScrollPosition(true); 329 - // raf = requestAnimationFrame(() => { 330 - // window.scrollTo({ top: savedPosition, behavior: "instant" }); 331 - // setRestoringScrollPosition(false); 332 - // setdonerestored(true); 333 - // }); 334 - window.scrollTo({ top: savedPosition, behavior: "instant" }); 335 - setRestoringScrollPosition(false); 336 - setdonerestored(true); 337 - }); 346 + // const raf = requestAnimationFrame(() => { 347 + // // setRestoringScrollPosition(true); 348 + // // raf = requestAnimationFrame(() => { 349 + // // window.scrollTo({ top: savedPosition, behavior: "instant" }); 350 + // // setRestoringScrollPosition(false); 351 + // // setdonerestored(true); 352 + // // }); 353 + // window.scrollTo({ top: savedPosition, behavior: "instant" }); 354 + // setRestoringScrollPosition(false); 355 + // setdonerestored(true); 356 + // }); 338 357 339 - return () => cancelAnimationFrame(raf); 340 - }, [selectedFeed, scrollPositions]); 358 + // return () => cancelAnimationFrame(raf); 359 + // }, [selectedFeed, scrollPositions]); 341 360 342 361 const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 343 362 const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; ··· 359 378 const isReadyForAuthedFeed = 360 379 authed && agent && identity?.pds && feedServiceDid; 361 380 const isReadyForUnauthedFeed = !authed && selectedFeed; 381 + 382 + const savedIndex = selectedFeed ? scrollIndexes[selectedFeed] : 0; 362 383 363 384 return ( 364 385 <div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800"> ··· 416 437 feedUri={selectedFeed!} 417 438 pdsUrl={identity?.pds} 418 439 feedServiceDid={feedServiceDid} 440 + initialScrollIndex={savedIndex} 441 + //onVisibleIndexChange={handleVisibleIndexChange} 419 442 /> 420 443 ) : ( 421 444 <div className="p-4 text-center text-gray-500">
+10
src/utils/atoms.ts
··· 11 11 12 12 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 13 13 14 + /** 15 + * @deprecated use the Tanstack Virtual index thanks 16 + */ 14 17 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 15 18 'feedscrollpositions', 19 + {} 20 + ); 21 + 22 + export const feedScrollIndexAtom = atomWithStorage<Record<string, number>>('feedScrollIndexes',{}); 23 + 24 + export const feedHeightsAtom = atomWithStorage<Record<string, Record<string, number>>>( 25 + 'feedPostHeights', 16 26 {} 17 27 ); 18 28