an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
1import { createFileRoute } from "@tanstack/react-router"; 2import { useAtom } from "jotai"; 3import * as React from "react"; 4import { useEffect, useLayoutEffect } from "react"; 5 6import { Header } from "~/components/Header"; 7import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8import { useAuth } from "~/providers/UnifiedAuthProvider"; 9import { 10 agentAtom, 11 authedAtom, 12 feedScrollPositionsAtom, 13 isAtTopAtom, 14 selectedFeedUriAtom, 15 store, 16} from "~/utils/atoms"; 17//import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 18import { 19 //constructArbitraryQuery, 20 //constructIdentityQuery, 21 //constructInfiniteFeedSkeletonQuery, 22 //constructPostQuery, 23 useQueryArbitrary, 24 useQueryIdentity, 25 useQueryPreferences, 26} from "~/utils/useQuery"; 27 28export const Route = createFileRoute("/")({ 29 // loader: async ({ context }) => { 30 // const { queryClient } = context; 31 // const atomauth = store.get(authedAtom); 32 // const atomagent = store.get(agentAtom); 33 34 // let identitypds: string | undefined; 35 // const initialselectedfeed = store.get(selectedFeedUriAtom); 36 // if (atomagent && atomauth && atomagent?.did) { 37 // const identityopts = constructIdentityQuery(atomagent.did); 38 // const identityresultmaybe = 39 // await queryClient.ensureQueryData(identityopts); 40 // identitypds = identityresultmaybe?.pds; 41 // } 42 43 // const arbitraryopts = constructArbitraryQuery( 44 // initialselectedfeed ?? 45 // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" 46 // ); 47 // const feedGengetrecordquery = 48 // await queryClient.ensureQueryData(arbitraryopts); 49 // const feedServiceDid = (feedGengetrecordquery?.value as any)?.did; 50 // //queryClient.ensureInfiniteQueryData() 51 52 // const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({ 53 // feedUri: 54 // initialselectedfeed ?? 55 // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 56 // agent: atomagent ?? undefined, 57 // isAuthed: atomauth ?? false, 58 // pdsUrl: identitypds, 59 // feedServiceDid: feedServiceDid, 60 // }); 61 62 // const res = await queryClient.ensureInfiniteQueryData({ 63 // queryKey, 64 // queryFn, 65 // initialPageParam: undefined as never, 66 // getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined, 67 // staleTime: Infinity, 68 // //refetchOnWindowFocus: false, 69 // //enabled: true, 70 // }); 71 // await Promise.all( 72 // res.pages.map(async (page) => { 73 // await Promise.all( 74 // page.feed.map(async (feedviewpost) => { 75 // if (!feedviewpost.post) return; 76 // // /*mass comment*/ console.log("preloading: ", feedviewpost.post); 77 // const opts = constructPostQuery(feedviewpost.post); 78 // try { 79 // await queryClient.ensureQueryData(opts); 80 // } catch (e) { 81 // // /*mass comment*/ console.log(" failed:", e); 82 // } 83 // }) 84 // ); 85 // }) 86 // ); 87 // }, 88 component: Home, 89 pendingComponent: PendingHome, // PendingHome, 90 staticData: { keepAlive: true }, 91}); 92function PendingHome() { 93 return <div>loading... (prefetching your timeline)</div>; 94} 95 96//function Homer() { 97// return <div></div> 98//} 99export function Home({ hidden = false }: { hidden?: boolean }) { 100 const { 101 agent, 102 status, 103 authMethod, 104 loginWithPassword, 105 loginWithOAuth, 106 logout, 107 } = useAuth(); 108 const authed = !!agent?.did; 109 110 useEffect(() => { 111 if (agent?.did) { 112 store.set(authedAtom, true); 113 } else { 114 store.set(authedAtom, false); 115 } 116 }, [status, agent, authed]); 117 useEffect(() => { 118 if (agent) { 119 // eslint-disable-next-line @typescript-eslint/ban-ts-comment 120 // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent 121 store.set(agentAtom, agent); 122 } else { 123 store.set(agentAtom, null); 124 } 125 }, [status, agent, authed]); 126 127 //const { get, set } = usePersistentStore(); 128 // const [feed, setFeed] = React.useState<any[]>([]); 129 // const [loading, setLoading] = React.useState(true); 130 // const [error, setError] = React.useState<string | null>(null); 131 132 // const [prefs, setPrefs] = React.useState<any>({}); 133 // React.useEffect(() => { 134 // if (!loadering && authed && agent && agent.did) { 135 // const run = async () => { 136 // try { 137 // if (!agent.did) return; 138 // const prefs = await cachedGetPrefs({ 139 // did: agent.did, 140 // agent, 141 // get, 142 // set, 143 // }); 144 145 // // /*mass comment*/ console.log("alistoffeeds", prefs); 146 // setPrefs(prefs || {}); 147 // } catch (err) { 148 // console.error("alistoffeeds Fetch error in preferences effect:", err); 149 // } 150 // }; 151 152 // run(); 153 // } 154 // }, [loadering, authed, agent]); 155 156 // const savedFeedsPref = React.useMemo(() => { 157 // if (!prefs?.preferences) return null; 158 // return prefs.preferences.find( 159 // (p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2", 160 // ); 161 // }, [prefs]); 162 163 // const savedFeeds = savedFeedsPref?.items || []; 164 165 const identityresultmaybe = useQueryIdentity(agent?.did); 166 const identity = identityresultmaybe?.data; 167 168 const prefsresultmaybe = useQueryPreferences({ 169 agent: agent ?? undefined, 170 pdsUrl: identity?.pds, 171 }); 172 const prefs = prefsresultmaybe?.data; 173 174 const savedFeeds = React.useMemo(() => { 175 const savedFeedsPref = prefs?.preferences?.find( 176 (p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2" 177 ); 178 return savedFeedsPref?.items || []; 179 }, [prefs]); 180 181 const [persistentSelectedFeed, setPersistentSelectedFeed] = 182 useAtom(selectedFeedUriAtom); // React.useState<string | null>(null); 183 const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState( 184 persistentSelectedFeed 185 ); // React.useState<string | null>(null); 186 const selectedFeed = agent?.did 187 ? persistentSelectedFeed 188 : unauthedSelectedFeed; 189 const setSelectedFeed = agent?.did 190 ? setPersistentSelectedFeed 191 : setUnauthedSelectedFeed; 192 193 // /*mass comment*/ console.log("my selectedFeed is: ", selectedFeed); 194 React.useEffect(() => { 195 const fallbackFeed = 196 "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"; 197 if (authed) { 198 if (selectedFeed) return; 199 if (savedFeeds.length > 0) { 200 setSelectedFeed((prev) => 201 prev && savedFeeds.some((f: any) => f.value === prev) 202 ? prev 203 : savedFeeds[0].value 204 ); 205 } else { 206 if (selectedFeed) return; 207 setSelectedFeed(fallbackFeed); 208 } 209 } else { 210 if (selectedFeed) return; 211 setSelectedFeed(fallbackFeed); 212 } 213 }, [savedFeeds, authed, setSelectedFeed]); 214 215 // React.useEffect(() => { 216 // if (loadering || !selectedFeed) return; 217 218 // let ignore = false; 219 220 // const run = async () => { 221 // setLoading(true); 222 // setError(null); 223 224 // try { 225 // if (authed && agent) { 226 // if (!agent.did) return; 227 228 // const pdsurl = await cachedResolveIdentity({ 229 // didOrHandle: agent.did, 230 // get, 231 // set, 232 // }); 233 234 // const fetchstringcomplex = `${pdsurl.pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${selectedFeed}`; 235 // // /*mass comment*/ console.log("fetching feed authed: " + fetchstringcomplex); 236 237 // const feeddef = await cachedGetRecord({ 238 // atUri: selectedFeed, 239 // get, 240 // set, 241 // }); 242 243 // const feedservicedid = feeddef.value.did; 244 245 // const res = await agent.fetchHandler(fetchstringcomplex, { 246 // method: "GET", 247 // headers: { 248 // "atproto-proxy": `${feedservicedid}#bsky_fg`, 249 // "Content-Type": "application/json", 250 // }, 251 // }); 252 253 // if (!res.ok) throw new Error("Failed to fetch feed"); 254 // const data = await res.json(); 255 256 // if (!ignore) setFeed(data.feed || []); 257 // } else { 258 // // /*mass comment*/ console.log("falling back"); 259 // // always use fallback feed for not logged in 260 // const fallbackFeed = 261 // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"; 262 // // const feeddef = await cachedGetRecord({ 263 // // atUri: fallbackFeed, 264 // // get, 265 // // set, 266 // // }); 267 268 // //const feedservicedid = "did:web:discover.bsky.app" //feeddef.did; 269 // const fetchstringsimple = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${fallbackFeed}`; 270 // // /*mass comment*/ console.log("fetching feed unauthed: " + fetchstringsimple); 271 272 // const res = await fetch(fetchstringsimple); 273 // if (!res.ok) throw new Error("Failed to fetch feed"); 274 // const data = await res.json(); 275 276 // if (!ignore) setFeed(data.feed || []); 277 // } 278 // } catch (e) { 279 // if (!ignore) { 280 // if (e instanceof Error) { 281 // setError(e.message); 282 // } else { 283 // setError("Unknown error"); 284 // } 285 // } 286 // } finally { 287 // if (!ignore) setLoading(false); 288 // } 289 // }; 290 291 // run(); 292 293 // return () => { 294 // ignore = true; 295 // }; 296 // }, [authed, agent, loadering, selectedFeed, get, set]); 297 298 const [scrollPositions, setScrollPositions] = useAtom( 299 feedScrollPositionsAtom 300 ); 301 302 const scrollPositionsRef = React.useRef(scrollPositions); 303 304 React.useEffect(() => { 305 scrollPositionsRef.current = scrollPositions; 306 }, [scrollPositions]); 307 308 useLayoutEffect(() => { 309 const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 310 311 window.scrollTo({ top: savedPosition, behavior: "instant" }); 312 // eslint-disable-next-line react-hooks/exhaustive-deps 313 }, [selectedFeed]); 314 315 useLayoutEffect(() => { 316 if (!selectedFeed) return; 317 318 const handleScroll = () => { 319 scrollPositionsRef.current = { 320 ...scrollPositionsRef.current, 321 [selectedFeed]: window.scrollY, 322 }; 323 }; 324 325 window.addEventListener("scroll", handleScroll, { passive: true }); 326 return () => { 327 window.removeEventListener("scroll", handleScroll); 328 329 setScrollPositions(scrollPositionsRef.current); 330 }; 331 }, [selectedFeed, setScrollPositions]); 332 333 const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 334 const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; 335 336 // const { 337 // data: feedData, 338 // isLoading: isFeedLoading, 339 // error: feedError, 340 // } = useQueryFeedSkeleton({ 341 // feedUri: selectedFeed!, 342 // agent: agent ?? undefined, 343 // isAuthed: authed ?? false, 344 // pdsUrl: identity?.pds, 345 // feedServiceDid: feedServiceDid, 346 // }); 347 348 // const feed = feedData?.feed || []; 349 350 const isReadyForAuthedFeed = 351 authed && agent && identity?.pds && feedServiceDid; 352 const isReadyForUnauthedFeed = !authed && selectedFeed; 353 354 355 const [isAtTop] = useAtom(isAtTopAtom); 356 357 return ( 358 <div 359 className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 360 > 361 {savedFeeds.length > 0 ? ( 362 <div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}> 363 {savedFeeds.map((item: any, idx: number) => { 364 const label = item.value.split("/").pop() || item.value; 365 const isActive = selectedFeed === item.value; 366 return ( 367 <button 368 key={item.value || idx} 369 className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${ 370 isActive 371 ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 372 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 373 // ? "bg-gray-500 text-white" 374 // : item.pinned 375 // ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" 376 // : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200" 377 }`} 378 onClick={() => setSelectedFeed(item.value)} 379 title={item.value} 380 > 381 {label} 382 {item.pinned && ( 383 <span 384 className={`ml-1 text-xs ${ 385 isActive 386 ? "text-gray-900 dark:text-gray-100" 387 : "text-gray-600 dark:text-gray-400" 388 }`} 389 > 390 391 </span> 392 )} 393 </button> 394 ); 395 })} 396 </div> 397 ) : ( 398 // <span className="text-xl font-bold ml-2">Home</span> 399 <Header title="Home" /> 400 )} 401 {/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>} 402 {feedError && <div className="p-4 text-red-500">{feedError.message}</div>} 403 {!isFeedLoading && !feedError && feed.length === 0 && ( 404 <div className="p-4 text-gray-500">No posts found.</div> 405 )} */} 406 {/* {feed.map((item, i) => ( 407 <UniversalPostRendererATURILoader 408 key={item.post || i} 409 atUri={item.post} 410 /> 411 ))} */} 412 413 {authed && (!identity?.pds || !feedServiceDid) && ( 414 <div className="p-4 text-center text-gray-500"> 415 Preparing your feed... 416 </div> 417 )} 418 419 {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 420 <InfiniteCustomFeed 421 feedUri={selectedFeed!} 422 pdsUrl={identity?.pds} 423 feedServiceDid={feedServiceDid} 424 /> 425 ) : ( 426 <div className="p-4 text-center text-gray-500"> 427 Select a feed to get started. 428 </div> 429 )} 430 {/* {false && restoringScrollPosition && ( 431 <div className="fixed top-1/2 left-1/2 right-1/2"> 432 restoringScrollPosition 433 </div> 434 )} */} 435 </div> 436 ); 437} 438// not even used lmaooo 439 440// export async function cachedResolveDIDWEBDOC({ 441// didweb, 442// cacheTimeout = CACHE_TIMEOUT, 443// get, 444// set, 445// }: { 446// didweb: string; 447// cacheTimeout?: number; 448// get: (key: string) => any; 449// set: (key: string, value: string) => void; 450// }): Promise<any> { 451// const isDidInput = didweb.startsWith("did:web:"); 452// const cacheKey = `didwebdoc:${didweb}`; 453// const now = Date.now(); 454// const cached = get(cacheKey); 455// if ( 456// cached && 457// cached.value && 458// cached.time && 459// now - cached.time < cacheTimeout 460// ) { 461// try { 462// return JSON.parse(cached.value); 463// } catch (_e) {/* whatever*/ } 464// } 465// const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent( 466// didweb 467// )}`; 468// const res = await fetch(url); 469// if (!res.ok) throw new Error("Failed to resolve didwebdoc"); 470// const data = await res.json(); 471// set(cacheKey, JSON.stringify(data)); 472// if (!isDidInput && data.did) { 473// set(`didwebdoc:${data.did}`, JSON.stringify(data)); 474// } 475// return data; 476// } 477 478// export async function cachedGetPrefs({ 479// did, 480// agent, 481// get, 482// set, 483// cacheTimeout = CACHE_TIMEOUT, 484// }: { 485// did: string; 486// agent: any; // or type properly if available 487// get: (key: string) => any; 488// set: (key: string, value: string) => void; 489// cacheTimeout?: number; 490// }): Promise<any> { 491// const cacheKey = `prefs:${did}`; 492// const cached = get(cacheKey); 493// const now = Date.now(); 494 495// if ( 496// cached && 497// cached.value && 498// cached.time && 499// now - cached.time < cacheTimeout 500// ) { 501// try { 502// return JSON.parse(cached.value); 503// } catch { 504// // fall through to fetch 505// } 506// } 507 508// const resolved = await cachedResolveIdentity({ 509// didOrHandle: did, 510// get, 511// set, 512// }); 513 514// if (!resolved?.pdsUrl) throw new Error("Missing resolved PDS info"); 515 516// const fetchUrl = `${resolved.pdsUrl}/xrpc/app.bsky.actor.getPreferences`; 517 518// const res = await agent.fetchHandler(fetchUrl, { 519// method: "GET", 520// headers: { 521// "Content-Type": "application/json", 522// }, 523// }); 524 525// if (!res.ok) throw new Error(`Failed to fetch preferences: ${res.status}`); 526 527// const text = await res.text(); 528 529// let data: any; 530// try { 531// data = JSON.parse(text); 532// } catch (err) { 533// console.error("Failed to parse preferences JSON:", err); 534// throw err; 535// } 536 537// set(cacheKey, JSON.stringify(data)); 538// return data; 539// }