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// }