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 74d406fb9f99bd7a0e287ebbbe29ed6cf202eb1b 550 lines 18 kB view raw
1import { createFileRoute } from "@tanstack/react-router"; 2import { useAtom } from "jotai"; 3import * as React from "react"; 4import { useLayoutEffect, useState } from "react"; 5 6import { Header } from "~/components/Header"; 7import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8import { useAuth } from "~/providers/UnifiedAuthProvider"; 9import { 10 feedScrollPositionsAtom, 11 isAtTopAtom, 12 quickAuthAtom, 13 selectedFeedUriAtom, 14} from "~/utils/atoms"; 15//import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 16import { 17 //constructArbitraryQuery, 18 //constructIdentityQuery, 19 //constructInfiniteFeedSkeletonQuery, 20 //constructPostQuery, 21 useQueryArbitrary, 22 useQueryIdentity, 23 useQueryPreferences, 24} from "~/utils/useQuery"; 25 26export const Route = createFileRoute("/")({ 27 // loader: async ({ context }) => { 28 // const { queryClient } = context; 29 // const atomauth = store.get(authedAtom); 30 // const atomagent = store.get(agentAtom); 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 // } 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() 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 // }); 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 // }, 86 component: Home, 87 pendingComponent: PendingHome, // PendingHome, 88 staticData: { keepAlive: true }, 89}); 90function PendingHome() { 91 return <div>loading... (prefetching your timeline)</div>; 92} 93 94//function Homer() { 95// return <div></div> 96//} 97export function Home({ hidden = false }: { hidden?: boolean }) { 98 const { 99 agent, 100 status, 101 authMethod, 102 loginWithPassword, 103 loginWithOAuth, 104 logout, 105 } = useAuth(); 106 const authed = !!agent?.did; 107 108 // i dont remember why this is even here 109 // useEffect(() => { 110 // if (agent?.did) { 111 // store.set(authedAtom, true); 112 // } else { 113 // store.set(authedAtom, false); 114 // } 115 // }, [status, agent, authed]); 116 // useEffect(() => { 117 // if (agent) { 118 // // eslint-disable-next-line @typescript-eslint/ban-ts-comment 119 // // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent 120 // store.set(agentAtom, agent); 121 // } else { 122 // store.set(agentAtom, null); 123 // } 124 // }, [status, agent, authed]); 125 126 //const { get, set } = usePersistentStore(); 127 // const [feed, setFeed] = React.useState<any[]>([]); 128 // const [loading, setLoading] = React.useState(true); 129 // const [error, setError] = React.useState<string | null>(null); 130 131 // const [prefs, setPrefs] = React.useState<any>({}); 132 // React.useEffect(() => { 133 // if (!loadering && authed && agent && agent.did) { 134 // const run = async () => { 135 // try { 136 // if (!agent.did) return; 137 // const prefs = await cachedGetPrefs({ 138 // did: agent.did, 139 // agent, 140 // get, 141 // set, 142 // }); 143 144 // // /*mass comment*/ console.log("alistoffeeds", prefs); 145 // setPrefs(prefs || {}); 146 // } catch (err) { 147 // console.error("alistoffeeds Fetch error in preferences effect:", err); 148 // } 149 // }; 150 151 // run(); 152 // } 153 // }, [loadering, authed, agent]); 154 155 // const savedFeedsPref = React.useMemo(() => { 156 // if (!prefs?.preferences) return null; 157 // return prefs.preferences.find( 158 // (p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2", 159 // ); 160 // }, [prefs]); 161 162 // const savedFeeds = savedFeedsPref?.items || []; 163 164 const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 165 const isAuthRestoring = quickAuth ? status === "loading" : false; 166 167 const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined); 168 const identity = identityresultmaybe?.data; 169 170 const prefsresultmaybe = useQueryPreferences({ 171 agent: !isAuthRestoring ? (agent ?? undefined) : undefined, 172 pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined, 173 }); 174 const prefs = prefsresultmaybe?.data; 175 176 const savedFeeds = React.useMemo(() => { 177 const savedFeedsPref = prefs?.preferences?.find( 178 (p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2" 179 ); 180 return savedFeedsPref?.items || []; 181 }, [prefs]); 182 183 const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 184 const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed); 185 const selectedFeed = agent?.did 186 ? persistentSelectedFeed 187 : unauthedSelectedFeed; 188 const setSelectedFeed = agent?.did 189 ? setPersistentSelectedFeed 190 : setUnauthedSelectedFeed; 191 192 // /*mass comment*/ console.log("my selectedFeed is: ", selectedFeed); 193 React.useEffect(() => { 194 const fallbackFeed = 195 "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"; 196 if (authed) { 197 if (selectedFeed) return; 198 if (savedFeeds.length > 0) { 199 setSelectedFeed((prev) => 200 prev && savedFeeds.some((f: any) => f.value === prev) 201 ? prev 202 : savedFeeds[0].value 203 ); 204 } else { 205 if (selectedFeed) return; 206 setSelectedFeed(fallbackFeed); 207 } 208 } else { 209 if (selectedFeed) return; 210 setSelectedFeed(fallbackFeed); 211 } 212 }, [savedFeeds, authed, setSelectedFeed]); 213 214 // React.useEffect(() => { 215 // if (loadering || !selectedFeed) return; 216 217 // let ignore = false; 218 219 // const run = async () => { 220 // setLoading(true); 221 // setError(null); 222 223 // try { 224 // if (authed && agent) { 225 // if (!agent.did) return; 226 227 // const pdsurl = await cachedResolveIdentity({ 228 // didOrHandle: agent.did, 229 // get, 230 // set, 231 // }); 232 233 // const fetchstringcomplex = `${pdsurl.pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${selectedFeed}`; 234 // // /*mass comment*/ console.log("fetching feed authed: " + fetchstringcomplex); 235 236 // const feeddef = await cachedGetRecord({ 237 // atUri: selectedFeed, 238 // get, 239 // set, 240 // }); 241 242 // const feedservicedid = feeddef.value.did; 243 244 // const res = await agent.fetchHandler(fetchstringcomplex, { 245 // method: "GET", 246 // headers: { 247 // "atproto-proxy": `${feedservicedid}#bsky_fg`, 248 // "Content-Type": "application/json", 249 // }, 250 // }); 251 252 // if (!res.ok) throw new Error("Failed to fetch feed"); 253 // const data = await res.json(); 254 255 // if (!ignore) setFeed(data.feed || []); 256 // } else { 257 // // /*mass comment*/ console.log("falling back"); 258 // // always use fallback feed for not logged in 259 // const fallbackFeed = 260 // "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"; 261 // // const feeddef = await cachedGetRecord({ 262 // // atUri: fallbackFeed, 263 // // get, 264 // // set, 265 // // }); 266 267 // //const feedservicedid = "did:web:discover.bsky.app" //feeddef.did; 268 // const fetchstringsimple = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${fallbackFeed}`; 269 // // /*mass comment*/ console.log("fetching feed unauthed: " + fetchstringsimple); 270 271 // const res = await fetch(fetchstringsimple); 272 // if (!res.ok) throw new Error("Failed to fetch feed"); 273 // const data = await res.json(); 274 275 // if (!ignore) setFeed(data.feed || []); 276 // } 277 // } catch (e) { 278 // if (!ignore) { 279 // if (e instanceof Error) { 280 // setError(e.message); 281 // } else { 282 // setError("Unknown error"); 283 // } 284 // } 285 // } finally { 286 // if (!ignore) setLoading(false); 287 // } 288 // }; 289 290 // run(); 291 292 // return () => { 293 // ignore = true; 294 // }; 295 // }, [authed, agent, loadering, selectedFeed, get, set]); 296 297 const [scrollPositions, setScrollPositions] = useAtom( 298 feedScrollPositionsAtom 299 ); 300 301 const scrollPositionsRef = React.useRef(scrollPositions); 302 303 React.useEffect(() => { 304 scrollPositionsRef.current = scrollPositions; 305 }, [scrollPositions]); 306 307 useLayoutEffect(() => { 308 if (isAuthRestoring) return; 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, isAuthRestoring]); 314 315 useLayoutEffect(() => { 316 if (!selectedFeed || isAuthRestoring) 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 }, [isAuthRestoring, selectedFeed, setScrollPositions]); 332 333 const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined); 334 const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined; 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 = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid; 351 const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed; 352 353 354 const [isAtTop] = useAtom(isAtTopAtom); 355 356 return ( 357 <div 358 className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 359 > 360 {!isAuthRestoring && savedFeeds.length > 0 ? ( 361 <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`}> 362 {savedFeeds.map((item: any, idx: number) => {return <FeedTabOnTop key={item} item={item} idx={idx} />})} 363 </div> 364 ) : ( 365 // <span className="text-xl font-bold ml-2">Home</span> 366 <Header title="Home" /> 367 )} 368 {/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>} 369 {feedError && <div className="p-4 text-red-500">{feedError.message}</div>} 370 {!isFeedLoading && !feedError && feed.length === 0 && ( 371 <div className="p-4 text-gray-500">No posts found.</div> 372 )} */} 373 {/* {feed.map((item, i) => ( 374 <UniversalPostRendererATURILoader 375 key={item.post || i} 376 atUri={item.post} 377 /> 378 ))} */} 379 380 {isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && ( 381 <div className="p-4 text-center text-gray-500"> 382 Preparing your feed... 383 </div> 384 )} 385 386 {!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? ( 387 <InfiniteCustomFeed 388 key={selectedFeed!} 389 feedUri={selectedFeed!} 390 pdsUrl={identity?.pds} 391 feedServiceDid={feedServiceDid} 392 /> 393 ) : ( 394 <div className="p-4 text-center text-gray-500"> 395 Loading....... 396 </div> 397 )} 398 {/* {false && restoringScrollPosition && ( 399 <div className="fixed top-1/2 left-1/2 right-1/2"> 400 restoringScrollPosition 401 </div> 402 )} */} 403 </div> 404 ); 405} 406 407 408// todo please use types this is dangerous very dangerous. 409// todo fix this whenever proper preferences is handled 410function FeedTabOnTop({item, idx}:{item: any, idx: number}) { 411 const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 412 const selectedFeed = persistentSelectedFeed 413 const setSelectedFeed = setPersistentSelectedFeed 414 const rkey = item.value.split("/").pop() || item.value; 415 const isActive = selectedFeed === item.value; 416 const { data: feedrecord } = useQueryArbitrary(item.value) 417 const label = feedrecord?.value?.displayName || rkey 418 return ( 419 <button 420 key={item.value || idx} 421 className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${ 422 isActive 423 ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 424 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 425 // ? "bg-gray-500 text-white" 426 // : item.pinned 427 // ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" 428 // : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200" 429 }`} 430 onClick={() => setSelectedFeed(item.value)} 431 title={item.value} 432 > 433 {label} 434 {item.pinned && ( 435 <span 436 className={`ml-1 text-xs ${ 437 isActive 438 ? "text-gray-900 dark:text-gray-100" 439 : "text-gray-600 dark:text-gray-400" 440 }`} 441 > 442 443 </span> 444 )} 445 </button> 446 ); 447} 448 449// not even used lmaooo 450 451// export async function cachedResolveDIDWEBDOC({ 452// didweb, 453// cacheTimeout = CACHE_TIMEOUT, 454// get, 455// set, 456// }: { 457// didweb: string; 458// cacheTimeout?: number; 459// get: (key: string) => any; 460// set: (key: string, value: string) => void; 461// }): Promise<any> { 462// const isDidInput = didweb.startsWith("did:web:"); 463// const cacheKey = `didwebdoc:${didweb}`; 464// const now = Date.now(); 465// const cached = get(cacheKey); 466// if ( 467// cached && 468// cached.value && 469// cached.time && 470// now - cached.time < cacheTimeout 471// ) { 472// try { 473// return JSON.parse(cached.value); 474// } catch (_e) {/* whatever*/ } 475// } 476// const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent( 477// didweb 478// )}`; 479// const res = await fetch(url); 480// if (!res.ok) throw new Error("Failed to resolve didwebdoc"); 481// const data = await res.json(); 482// set(cacheKey, JSON.stringify(data)); 483// if (!isDidInput && data.did) { 484// set(`didwebdoc:${data.did}`, JSON.stringify(data)); 485// } 486// return data; 487// } 488 489// export async function cachedGetPrefs({ 490// did, 491// agent, 492// get, 493// set, 494// cacheTimeout = CACHE_TIMEOUT, 495// }: { 496// did: string; 497// agent: any; // or type properly if available 498// get: (key: string) => any; 499// set: (key: string, value: string) => void; 500// cacheTimeout?: number; 501// }): Promise<any> { 502// const cacheKey = `prefs:${did}`; 503// const cached = get(cacheKey); 504// const now = Date.now(); 505 506// if ( 507// cached && 508// cached.value && 509// cached.time && 510// now - cached.time < cacheTimeout 511// ) { 512// try { 513// return JSON.parse(cached.value); 514// } catch { 515// // fall through to fetch 516// } 517// } 518 519// const resolved = await cachedResolveIdentity({ 520// didOrHandle: did, 521// get, 522// set, 523// }); 524 525// if (!resolved?.pdsUrl) throw new Error("Missing resolved PDS info"); 526 527// const fetchUrl = `${resolved.pdsUrl}/xrpc/app.bsky.actor.getPreferences`; 528 529// const res = await agent.fetchHandler(fetchUrl, { 530// method: "GET", 531// headers: { 532// "Content-Type": "application/json", 533// }, 534// }); 535 536// if (!res.ok) throw new Error(`Failed to fetch preferences: ${res.status}`); 537 538// const text = await res.text(); 539 540// let data: any; 541// try { 542// data = JSON.parse(text); 543// } catch (err) { 544// console.error("Failed to parse preferences JSON:", err); 545// throw err; 546// } 547 548// set(cacheKey, JSON.stringify(data)); 549// return data; 550// }