an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
93
fork

Configure Feed

Select the types of activity you want to include in your feed.

minimal working virtual

rimar1337 9356009e 833207ed

+411 -128
+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