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

Configure Feed

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

at 78e2b52c138358ecca07de7f0af645e095ddfe34 540 lines 17 kB view raw
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 key={selectedFeed!} 422 feedUri={selectedFeed!} 423 pdsUrl={identity?.pds} 424 feedServiceDid={feedServiceDid} 425 /> 426 ) : ( 427 <div className="p-4 text-center text-gray-500"> 428 Select a feed to get started. 429 </div> 430 )} 431 {/* {false && restoringScrollPosition && ( 432 <div className="fixed top-1/2 left-1/2 right-1/2"> 433 restoringScrollPosition 434 </div> 435 )} */} 436 </div> 437 ); 438} 439// not even used lmaooo 440 441// export async function cachedResolveDIDWEBDOC({ 442// didweb, 443// cacheTimeout = CACHE_TIMEOUT, 444// get, 445// set, 446// }: { 447// didweb: string; 448// cacheTimeout?: number; 449// get: (key: string) => any; 450// set: (key: string, value: string) => void; 451// }): Promise<any> { 452// const isDidInput = didweb.startsWith("did:web:"); 453// const cacheKey = `didwebdoc:${didweb}`; 454// const now = Date.now(); 455// const cached = get(cacheKey); 456// if ( 457// cached && 458// cached.value && 459// cached.time && 460// now - cached.time < cacheTimeout 461// ) { 462// try { 463// return JSON.parse(cached.value); 464// } catch (_e) {/* whatever*/ } 465// } 466// const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent( 467// didweb 468// )}`; 469// const res = await fetch(url); 470// if (!res.ok) throw new Error("Failed to resolve didwebdoc"); 471// const data = await res.json(); 472// set(cacheKey, JSON.stringify(data)); 473// if (!isDidInput && data.did) { 474// set(`didwebdoc:${data.did}`, JSON.stringify(data)); 475// } 476// return data; 477// } 478 479// export async function cachedGetPrefs({ 480// did, 481// agent, 482// get, 483// set, 484// cacheTimeout = CACHE_TIMEOUT, 485// }: { 486// did: string; 487// agent: any; // or type properly if available 488// get: (key: string) => any; 489// set: (key: string, value: string) => void; 490// cacheTimeout?: number; 491// }): Promise<any> { 492// const cacheKey = `prefs:${did}`; 493// const cached = get(cacheKey); 494// const now = Date.now(); 495 496// if ( 497// cached && 498// cached.value && 499// cached.time && 500// now - cached.time < cacheTimeout 501// ) { 502// try { 503// return JSON.parse(cached.value); 504// } catch { 505// // fall through to fetch 506// } 507// } 508 509// const resolved = await cachedResolveIdentity({ 510// didOrHandle: did, 511// get, 512// set, 513// }); 514 515// if (!resolved?.pdsUrl) throw new Error("Missing resolved PDS info"); 516 517// const fetchUrl = `${resolved.pdsUrl}/xrpc/app.bsky.actor.getPreferences`; 518 519// const res = await agent.fetchHandler(fetchUrl, { 520// method: "GET", 521// headers: { 522// "Content-Type": "application/json", 523// }, 524// }); 525 526// if (!res.ok) throw new Error(`Failed to fetch preferences: ${res.status}`); 527 528// const text = await res.text(); 529 530// let data: any; 531// try { 532// data = JSON.parse(text); 533// } catch (err) { 534// console.error("Failed to parse preferences JSON:", err); 535// throw err; 536// } 537 538// set(cacheKey, JSON.stringify(data)); 539// return data; 540// }