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

persistent data, persistent scroll position, reload feeds, home screen loader

rimar1337 5293a5db bab14a9c

+333 -37
+46
package-lock.json
··· 8 "dependencies": { 9 "@atproto/api": "^0.16.6", 10 "@tailwindcss/vite": "^4.0.6", 11 "@tanstack/react-devtools": "^0.2.2", 12 "@tanstack/react-query": "^5.85.6", 13 "@tanstack/react-router": "^1.130.2", 14 "@tanstack/react-router-devtools": "^1.131.5", 15 "@tanstack/router-plugin": "^1.121.2", ··· 1894 "url": "https://github.com/sponsors/tannerlinsley" 1895 } 1896 }, 1897 "node_modules/@tanstack/react-devtools": { 1898 "version": "0.2.2", 1899 "resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.2.2.tgz", ··· 1929 "url": "https://github.com/sponsors/tannerlinsley" 1930 }, 1931 "peerDependencies": { 1932 "react": "^18 || ^19" 1933 } 1934 },
··· 8 "dependencies": { 9 "@atproto/api": "^0.16.6", 10 "@tailwindcss/vite": "^4.0.6", 11 + "@tanstack/query-sync-storage-persister": "^5.85.6", 12 "@tanstack/react-devtools": "^0.2.2", 13 "@tanstack/react-query": "^5.85.6", 14 + "@tanstack/react-query-persist-client": "^5.85.6", 15 "@tanstack/react-router": "^1.130.2", 16 "@tanstack/react-router-devtools": "^1.131.5", 17 "@tanstack/router-plugin": "^1.121.2", ··· 1896 "url": "https://github.com/sponsors/tannerlinsley" 1897 } 1898 }, 1899 + "node_modules/@tanstack/query-persist-client-core": { 1900 + "version": "5.85.6", 1901 + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.6.tgz", 1902 + "integrity": "sha512-wUdoEurIC0YCNZzR020Xcg3OsJeF4SXmEPqlNwZ6EaGKgWeNjU17hVdK+X4ZeirUm+h0muiEQx+aIQU1lk7roQ==", 1903 + "license": "MIT", 1904 + "dependencies": { 1905 + "@tanstack/query-core": "5.85.6" 1906 + }, 1907 + "funding": { 1908 + "type": "github", 1909 + "url": "https://github.com/sponsors/tannerlinsley" 1910 + } 1911 + }, 1912 + "node_modules/@tanstack/query-sync-storage-persister": { 1913 + "version": "5.85.6", 1914 + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.85.6.tgz", 1915 + "integrity": "sha512-Gj/p0paYsdzj3IbRn6SjMMNdjZ0nVQWszn17qbHLiu3Mt6H0b/YbLL3g9uRWcoyYcaB004RawgM0MuA+xJt5iw==", 1916 + "license": "MIT", 1917 + "dependencies": { 1918 + "@tanstack/query-core": "5.85.6", 1919 + "@tanstack/query-persist-client-core": "5.85.6" 1920 + }, 1921 + "funding": { 1922 + "type": "github", 1923 + "url": "https://github.com/sponsors/tannerlinsley" 1924 + } 1925 + }, 1926 "node_modules/@tanstack/react-devtools": { 1927 "version": "0.2.2", 1928 "resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.2.2.tgz", ··· 1958 "url": "https://github.com/sponsors/tannerlinsley" 1959 }, 1960 "peerDependencies": { 1961 + "react": "^18 || ^19" 1962 + } 1963 + }, 1964 + "node_modules/@tanstack/react-query-persist-client": { 1965 + "version": "5.85.6", 1966 + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.85.6.tgz", 1967 + "integrity": "sha512-zLUfm8JlI6/s0AqvX5l5CcazdHwj5gwcv0mWYOaJJvADyFzl2wwQKqB/H4nYSeygUtrepBgPwVQKNqH9ZwlZpQ==", 1968 + "license": "MIT", 1969 + "dependencies": { 1970 + "@tanstack/query-persist-client-core": "5.85.6" 1971 + }, 1972 + "funding": { 1973 + "type": "github", 1974 + "url": "https://github.com/sponsors/tannerlinsley" 1975 + }, 1976 + "peerDependencies": { 1977 + "@tanstack/react-query": "^5.85.6", 1978 "react": "^18 || ^19" 1979 } 1980 },
+2
package.json
··· 12 "dependencies": { 13 "@atproto/api": "^0.16.6", 14 "@tailwindcss/vite": "^4.0.6", 15 "@tanstack/react-devtools": "^0.2.2", 16 "@tanstack/react-query": "^5.85.6", 17 "@tanstack/react-router": "^1.130.2", 18 "@tanstack/react-router-devtools": "^1.131.5", 19 "@tanstack/router-plugin": "^1.121.2",
··· 12 "dependencies": { 13 "@atproto/api": "^0.16.6", 14 "@tailwindcss/vite": "^4.0.6", 15 + "@tanstack/query-sync-storage-persister": "^5.85.6", 16 "@tanstack/react-devtools": "^0.2.2", 17 "@tanstack/react-query": "^5.85.6", 18 + "@tanstack/react-query-persist-client": "^5.85.6", 19 "@tanstack/react-router": "^1.130.2", 20 "@tanstack/react-router-devtools": "^1.131.5", 21 "@tanstack/router-plugin": "^1.121.2",
+37 -2
src/components/InfiniteCustomFeed.tsx
··· 33 hasNextPage, 34 fetchNextPage, 35 isFetchingNextPage, 36 } = useInfiniteQueryFeedSkeleton({ 37 feedUri: feedUri, 38 agent: agent ?? undefined, ··· 40 pdsUrl: pdsUrl, 41 feedServiceDid: feedServiceDid, 42 }); 43 44 //const { ref, inView } = useInView(); 45 ··· 99 Load More Posts 100 </button> 101 )} 102 - {!hasNextPage && <div className="p-4 text-center text-gray-500">End of feed.</div>} 103 </> 104 ); 105 - }
··· 33 hasNextPage, 34 fetchNextPage, 35 isFetchingNextPage, 36 + refetch, 37 + isRefetching, 38 } = useInfiniteQueryFeedSkeleton({ 39 feedUri: feedUri, 40 agent: agent ?? undefined, ··· 42 pdsUrl: pdsUrl, 43 feedServiceDid: feedServiceDid, 44 }); 45 + 46 + const handleRefresh = () => { 47 + refetch(); 48 + }; 49 50 //const { ref, inView } = useInView(); 51 ··· 105 Load More Posts 106 </button> 107 )} 108 + {!hasNextPage && ( 109 + <div className="p-4 text-center text-gray-500">End of feed.</div> 110 + )} 111 + <button 112 + onClick={handleRefresh} 113 + disabled={isRefetching} 114 + 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" 115 + aria-label="Refresh feed" 116 + > 117 + {isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />} 118 + </button> 119 </> 120 ); 121 + } 122 + 123 + const RefreshIcon = (props: React.SVGProps<SVGSVGElement>) => ( 124 + <svg 125 + xmlns="http://www.w3.org/2000/svg" 126 + //width={360} 127 + //height={360} 128 + viewBox="0 0 24 24" 129 + {...props} 130 + > 131 + <path 132 + fill="none" 133 + stroke="currentColor" 134 + strokeLinecap="round" 135 + strokeLinejoin="round" 136 + strokeWidth={2} 137 + 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" 138 + ></path> 139 + </svg> 140 + );
+24 -4
src/main.tsx
··· 7 8 import "~/styles/app.css"; 9 import reportWebVitals from "./reportWebVitals.ts"; 10 - import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 11 12 - const queryClient = new QueryClient(); 13 // Create a new router instance 14 const router = createRouter({ 15 routeTree, ··· 33 const root = ReactDOM.createRoot(rootElement); 34 root.render( 35 // double queries annoys me 36 - <StrictMode> 37 <QueryClientProvider client={queryClient}> 38 <RouterProvider router={router} /> 39 </QueryClientProvider> 40 - </StrictMode> 41 ); 42 } 43
··· 7 8 import "~/styles/app.css"; 9 import reportWebVitals from "./reportWebVitals.ts"; 10 + import { QueryClient, QueryClientProvider, } from "@tanstack/react-query"; 11 + import { 12 + persistQueryClient, 13 + } from "@tanstack/react-query-persist-client"; 14 + import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; 15 16 + 17 + const queryClient = new QueryClient({ 18 + defaultOptions: { 19 + queries: { 20 + gcTime: 1000 * 60 * 60 * 24 * 24, // 24 days 21 + }, 22 + }, 23 + }); 24 + const localStoragePersister = createSyncStoragePersister({ 25 + storage: window.localStorage, 26 + }); 27 + 28 + persistQueryClient({ 29 + queryClient, 30 + persister: localStoragePersister, 31 + }) 32 + 33 // Create a new router instance 34 const router = createRouter({ 35 routeTree, ··· 53 const root = ReactDOM.createRoot(rootElement); 54 root.render( 55 // double queries annoys me 56 + // <StrictMode> 57 <QueryClientProvider client={queryClient}> 58 <RouterProvider router={router} /> 59 </QueryClientProvider> 60 + // </StrictMode> 61 ); 62 } 63
+5 -1
src/routes/__root.tsx
··· 10 Outlet, 11 Scripts, 12 createRootRoute, 13 useLocation, 14 useNavigate, 15 } from "@tanstack/react-router"; ··· 23 import { AuthProvider, useAuth } from "~/providers/PassAuthProvider"; 24 import { PersistentStoreProvider } from "~/providers/PersistentStoreProvider"; 25 import type AtpAgent from "@atproto/api"; 26 27 - export const Route = createRootRoute({ 28 head: () => ({ 29 meta: [ 30 {
··· 10 Outlet, 11 Scripts, 12 createRootRoute, 13 + createRootRouteWithContext, 14 useLocation, 15 useNavigate, 16 } from "@tanstack/react-router"; ··· 24 import { AuthProvider, useAuth } from "~/providers/PassAuthProvider"; 25 import { PersistentStoreProvider } from "~/providers/PersistentStoreProvider"; 26 import type AtpAgent from "@atproto/api"; 27 + import type { QueryClient } from "@tanstack/react-query"; 28 29 + export const Route = createRootRouteWithContext<{ 30 + queryClient: QueryClient; 31 + }>()({ 32 head: () => ({ 33 meta: [ 34 {
+192 -26
src/routes/index.tsx
··· 13 useQueryPost, 14 useQueryFeedSkeleton, 15 useQueryPreferences, 16 - useQueryArbitrary 17 } from "~/utils/useQuery"; 18 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 19 20 export const Route = createFileRoute("/")({ 21 component: Home, 22 }); 23 - 24 function Home() { 25 const { 26 agent, ··· 30 loading: loadering, 31 authed, 32 } = useAuth(); 33 //const { get, set } = usePersistentStore(); 34 // const [feed, setFeed] = React.useState<any[]>([]); 35 // const [loading, setLoading] = React.useState(true); ··· 67 // }, [prefs]); 68 69 // const savedFeeds = savedFeedsPref?.items || []; 70 - 71 const identityresultmaybe = useQueryIdentity(agent?.did); 72 - const identity = identityresultmaybe?.data 73 74 - const prefsresultmaybe = useQueryPreferences({agent: agent ?? undefined, pdsUrl: identity?.pds}); 75 - const prefs = prefsresultmaybe?.data 76 - 77 const savedFeeds = React.useMemo(() => { 78 const savedFeedsPref = prefs?.preferences?.find( 79 (p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2" ··· 81 return savedFeedsPref?.items || []; 82 }, [prefs]); 83 84 - 85 86 - const [selectedFeed, setSelectedFeed] = React.useState<string | null>(null); 87 - 88 React.useEffect(() => { 89 const fallbackFeed = 90 "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"; 91 if (authed) { 92 if (savedFeeds.length > 0) { 93 setSelectedFeed((prev) => 94 prev && savedFeeds.some((f: any) => f.value === prev) 95 ? prev 96 - : savedFeeds[0].value, 97 ); 98 } else { 99 setSelectedFeed(fallbackFeed); 100 } 101 } else { 102 setSelectedFeed(fallbackFeed); 103 } 104 - }, [savedFeeds, authed]); 105 106 // React.useEffect(() => { 107 // if (loadering || !selectedFeed) return; ··· 185 // ignore = true; 186 // }; 187 // }, [authed, agent, loadering, selectedFeed, get, set]); 188 - 189 190 - const feedGengetrecordquery = useQueryArbitrary(selectedFeed??undefined); 191 const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; 192 193 // const { ··· 204 205 // const feed = feedData?.feed || []; 206 207 - const isReadyForAuthedFeed = authed && agent && identity?.pds && feedServiceDid; 208 const isReadyForUnauthedFeed = !authed && selectedFeed; 209 210 return ( 211 - <div className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800"> 212 <div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin"> 213 {savedFeeds.length > 0 ? ( 214 savedFeeds.map((item: any, idx: number) => { ··· 252 /> 253 ))} */} 254 255 - {(authed && (!identity?.pds || !feedServiceDid)) && ( 256 - <div className="p-4 text-center text-gray-500">Preparing your feed...</div> 257 )} 258 259 - {(isReadyForAuthedFeed || isReadyForUnauthedFeed) ? ( 260 - <InfiniteCustomFeed 261 - feedUri={selectedFeed!} 262 - pdsUrl={identity?.pds} 263 - feedServiceDid={feedServiceDid} 264 - /> 265 ) : ( 266 - <div className="p-4 text-center text-gray-500">Select a feed to get started.</div> 267 )} 268 </div> 269 ); ··· 295 } catch {} 296 } 297 const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent( 298 - didweb, 299 )}`; 300 const res = await fetch(url); 301 if (!res.ok) throw new Error("Failed to resolve didwebdoc");
··· 13 useQueryPost, 14 useQueryFeedSkeleton, 15 useQueryPreferences, 16 + useQueryArbitrary, 17 + constructInfiniteFeedSkeletonQuery, 18 + constructArbitraryQuery, 19 + constructIdentityQuery, 20 + constructPostQuery, 21 } from "~/utils/useQuery"; 22 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 23 + import { useAtom, useSetAtom } from "jotai"; 24 + import { 25 + selectedFeedUriAtom, 26 + store, 27 + agentAtom, 28 + authedAtom, 29 + feedScrollPositionsAtom, 30 + } from "~/utils/atoms"; 31 + import { useEffect, useLayoutEffect } from "react"; 32 33 export const Route = createFileRoute("/")({ 34 + loader: async ({ context }) => { 35 + const { queryClient } = context; 36 + const atomauth = store.get(authedAtom); 37 + const atomagent = store.get(agentAtom); 38 + 39 + let identitypds: string | undefined; 40 + const initialselectedfeed = store.get(selectedFeedUriAtom); 41 + if (atomagent && atomauth && atomagent?.did) { 42 + const identityopts = constructIdentityQuery(atomagent.did); 43 + const identityresultmaybe = 44 + await queryClient.ensureQueryData(identityopts); 45 + identitypds = identityresultmaybe?.pds; 46 + } 47 + 48 + const arbitraryopts = constructArbitraryQuery( 49 + initialselectedfeed ?? 50 + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" 51 + ); 52 + const feedGengetrecordquery = 53 + await queryClient.ensureQueryData(arbitraryopts); 54 + const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; 55 + //queryClient.ensureInfiniteQueryData() 56 + 57 + const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ 58 + feedUri: 59 + initialselectedfeed ?? 60 + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 61 + agent: atomagent ?? undefined, 62 + isAuthed: atomauth ?? false, 63 + pdsUrl: identitypds, 64 + feedServiceDid: feedServiceDid, 65 + }); 66 + 67 + const res = await queryClient.ensureInfiniteQueryData({ 68 + queryKey, 69 + queryFn, 70 + initialPageParam: undefined as never, 71 + getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, 72 + staleTime: Infinity, 73 + //refetchOnWindowFocus: false, 74 + //enabled: true, 75 + }); 76 + await Promise.all( 77 + res.pages.map(async (page) => { 78 + await Promise.all( 79 + page.feed.map(async (feedviewpost) => { 80 + if (!feedviewpost.post) return; 81 + console.log("preloading: ", feedviewpost.post); 82 + const opts = constructPostQuery(feedviewpost.post); 83 + try { 84 + await queryClient.ensureQueryData(opts); 85 + } catch (e) { 86 + console.log(" failed:", e); 87 + } 88 + }) 89 + ); 90 + }) 91 + ); 92 + }, 93 component: Home, 94 + pendingComponent: PendingHome, 95 }); 96 + function PendingHome() { 97 + return <div>loading... (prefetching your timeline)</div>; 98 + } 99 function Home() { 100 const { 101 agent, ··· 105 loading: loadering, 106 authed, 107 } = useAuth(); 108 + 109 + useEffect(() => { 110 + if (agent?.did) { 111 + store.set(authedAtom, true); 112 + } else { 113 + store.set(authedAtom, false); 114 + } 115 + }, [loginStatus, agent, authed]); 116 + useEffect(() => { 117 + if (agent) { 118 + store.set(agentAtom, agent); 119 + } else { 120 + store.set(agentAtom, null); 121 + } 122 + }, [loginStatus, agent, authed]); 123 + 124 //const { get, set } = usePersistentStore(); 125 // const [feed, setFeed] = React.useState<any[]>([]); 126 // const [loading, setLoading] = React.useState(true); ··· 158 // }, [prefs]); 159 160 // const savedFeeds = savedFeedsPref?.items || []; 161 + 162 const identityresultmaybe = useQueryIdentity(agent?.did); 163 + const identity = identityresultmaybe?.data; 164 165 + const prefsresultmaybe = useQueryPreferences({ 166 + agent: agent ?? undefined, 167 + pdsUrl: identity?.pds, 168 + }); 169 + const prefs = prefsresultmaybe?.data; 170 + 171 const savedFeeds = React.useMemo(() => { 172 const savedFeedsPref = prefs?.preferences?.find( 173 (p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2" ··· 175 return savedFeedsPref?.items || []; 176 }, [prefs]); 177 178 + const [persistentSelectedFeed, setPersistentSelectedFeed] = 179 + useAtom(selectedFeedUriAtom); // React.useState<string | null>(null); 180 + const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState( 181 + persistentSelectedFeed 182 + ); // React.useState<string | null>(null); 183 + const selectedFeed = agent?.did 184 + ? persistentSelectedFeed 185 + : unauthedSelectedFeed; 186 + const setSelectedFeed = agent?.did 187 + ? setPersistentSelectedFeed 188 + : setUnauthedSelectedFeed; 189 190 + console.log("my selectedFeed is: ", selectedFeed); 191 React.useEffect(() => { 192 const fallbackFeed = 193 "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"; 194 if (authed) { 195 + if (selectedFeed) return; 196 if (savedFeeds.length > 0) { 197 setSelectedFeed((prev) => 198 prev && savedFeeds.some((f: any) => f.value === prev) 199 ? prev 200 + : savedFeeds[0].value 201 ); 202 } else { 203 + if (selectedFeed) return; 204 setSelectedFeed(fallbackFeed); 205 } 206 } else { 207 + if (selectedFeed) return; 208 setSelectedFeed(fallbackFeed); 209 } 210 + }, [savedFeeds, authed, setSelectedFeed]); 211 212 // React.useEffect(() => { 213 // if (loadering || !selectedFeed) return; ··· 291 // ignore = true; 292 // }; 293 // }, [authed, agent, loadering, selectedFeed, get, set]); 294 + 295 + const [scrollPositions, setScrollPositions] = useAtom( 296 + feedScrollPositionsAtom 297 + ); 298 + 299 + const scrollRef = React.useRef<Record<string, number>>({}); 300 + 301 + useEffect(() => { 302 + const onScroll = () => { 303 + //if (!selectedFeed) return; 304 + scrollRef.current[selectedFeed ?? "null"] = window.scrollY; 305 + }; 306 + window.addEventListener("scroll", onScroll, { passive: true }); 307 + return () => window.removeEventListener("scroll", onScroll); 308 + }, [selectedFeed]); 309 + const [donerestored, setdonerestored] = React.useState(false); 310 + 311 + useEffect(() => { 312 + return () => { 313 + if (!donerestored) return; 314 + console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current); 315 + //if (!selectedFeed) return; 316 + setScrollPositions((prev) => ({ 317 + ...prev, 318 + [selectedFeed ?? "null"]: 319 + scrollRef.current[selectedFeed ?? "null"] ?? 0, 320 + })); 321 + }; 322 + }, [selectedFeed, setScrollPositions, donerestored]); 323 + 324 + const [restoringScrollPosition, setRestoringScrollPosition] = 325 + React.useState(false); 326 + 327 + useLayoutEffect(() => { 328 + setRestoringScrollPosition(true); 329 + const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 330 + 331 + let raf = requestAnimationFrame(() => { 332 + // setRestoringScrollPosition(true); 333 + // raf = requestAnimationFrame(() => { 334 + // window.scrollTo({ top: savedPosition, behavior: "instant" }); 335 + // setRestoringScrollPosition(false); 336 + // setdonerestored(true); 337 + // }); 338 + window.scrollTo({ top: savedPosition, behavior: "instant" }); 339 + setRestoringScrollPosition(false); 340 + setdonerestored(true); 341 + }); 342 + 343 + return () => cancelAnimationFrame(raf); 344 + }, [selectedFeed, scrollPositions]); 345 346 + const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 347 const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; 348 349 // const { ··· 360 361 // const feed = feedData?.feed || []; 362 363 + const isReadyForAuthedFeed = 364 + authed && agent && identity?.pds && feedServiceDid; 365 const isReadyForUnauthedFeed = !authed && selectedFeed; 366 367 return ( 368 + <div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800"> 369 <div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin"> 370 {savedFeeds.length > 0 ? ( 371 savedFeeds.map((item: any, idx: number) => { ··· 409 /> 410 ))} */} 411 412 + {authed && (!identity?.pds || !feedServiceDid) && ( 413 + <div className="p-4 text-center text-gray-500"> 414 + Preparing your feed... 415 + </div> 416 )} 417 418 + {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 419 + <InfiniteCustomFeed 420 + feedUri={selectedFeed!} 421 + pdsUrl={identity?.pds} 422 + feedServiceDid={feedServiceDid} 423 + /> 424 ) : ( 425 + <div className="p-4 text-center text-gray-500"> 426 + Select a feed to get started. 427 + </div> 428 + )} 429 + {false && restoringScrollPosition && ( 430 + <div className="fixed top-1/2 left-1/2 right-1/2"> 431 + restoringScrollPosition 432 + </div> 433 )} 434 </div> 435 ); ··· 461 } catch {} 462 } 463 const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent( 464 + didweb 465 )}`; 466 const res = await fetch(url); 467 if (!res.ok) throw new Error("Failed to resolve didwebdoc");
+23 -3
src/utils/atoms.ts
··· 1 - import { atom } from "jotai"; 2 3 - export const selectedFeedUriAtom = atom<string | null>(null); 4 5 - export const feedScrollPositionsAtom = atom<Record<string, number>>({});
··· 1 + import type AtpAgent from "@atproto/api"; 2 + import { atom, createStore } from "jotai"; 3 + import { atomWithStorage } from 'jotai/utils'; 4 + 5 + export const store = createStore(); 6 7 + export const selectedFeedUriAtom = atomWithStorage<string | null>( 8 + 'selectedFeedUri', 9 + null 10 + ); 11 12 + //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 13 + 14 + export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 15 + 'feedscrollpositions', 16 + {} 17 + ); 18 + 19 + export const likedPostsAtom = atomWithStorage<Record<string, boolean>>( 20 + 'likedPosts', 21 + {} 22 + ); 23 + 24 + export const agentAtom = atom<AtpAgent|null>(null); 25 + export const authedAtom = atom<boolean>(false);
+4 -1
src/utils/useQuery.ts
··· 234 return undefined; 235 } 236 }, 237 }); 238 } 239 export function useQueryConstellation(query: { ··· 399 400 export function constructArbitraryQuery(uri?: string) { 401 return queryOptions({ 402 - queryKey: ["post", uri], 403 queryFn: async () => { 404 if (!uri) return undefined as undefined 405 const res = await fetch(
··· 234 return undefined; 235 } 236 }, 237 + // enforce short lifespan 238 + staleTime: 5 * 60 * 1000, // 5 minutes 239 + gcTime: 5 * 60 * 1000, 240 }); 241 } 242 export function useQueryConstellation(query: { ··· 402 403 export function constructArbitraryQuery(uri?: string) { 404 return queryOptions({ 405 + queryKey: ["arbitrary", uri], 406 queryFn: async () => { 407 if (!uri) return undefined as undefined 408 const res = await fetch(