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 React, { useEffect, useRef,useState } from "react"; 4 5import { useAuth } from "~/providers/UnifiedAuthProvider"; 6import { constellationURLAtom } from "~/utils/atoms"; 7 8const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 9 10export const Route = createFileRoute("/notifications")({ 11 component: NotificationsComponent, 12}); 13 14function NotificationsComponent() { 15 // /*mass comment*/ console.log("NotificationsComponent render"); 16 const { agent, status } = useAuth(); 17 const authed = !!agent?.did; 18 const authLoading = status === "loading"; 19 const [did, setDid] = useState<string | null>(null); 20 const [resolving, setResolving] = useState(false); 21 const [error, setError] = useState<string | null>(null); 22 const [responses, setResponses] = useState<any[]>([null, null, null]); 23 const [loading, setLoading] = useState(false); 24 const inputRef = useRef<HTMLInputElement>(null); 25 26 useEffect(() => { 27 if (authLoading) return; 28 if (authed && agent && agent.assertDid) { 29 setDid(agent.assertDid); 30 } 31 }, [authed, agent, authLoading]); 32 33 async function handleSubmit() { 34 // /*mass comment*/ console.log("handleSubmit called"); 35 setError(null); 36 setResponses([null, null, null]); 37 const value = inputRef.current?.value?.trim() || ""; 38 if (!value) return; 39 if (value.startsWith("did:")) { 40 setDid(value); 41 setError(null); 42 return; 43 } 44 setResolving(true); 45 const cacheKey = `handleDid:${value}`; 46 const now = Date.now(); 47 const cached = undefined // await get(cacheKey); 48 // if ( 49 // cached && 50 // cached.value && 51 // cached.time && 52 // now - cached.time < HANDLE_DID_CACHE_TIMEOUT 53 // ) { 54 // try { 55 // const data = JSON.parse(cached.value); 56 // setDid(data.did); 57 // setResolving(false); 58 // return; 59 // } catch {} 60 // } 61 try { 62 const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(value)}`; 63 const res = await fetch(url); 64 if (!res.ok) throw new Error("Failed to resolve handle"); 65 const data = await res.json(); 66 //set(cacheKey, JSON.stringify(data)); 67 setDid(data.did); 68 } catch (e: any) { 69 setError("Failed to resolve handle: " + (e?.message || e)); 70 } finally { 71 setResolving(false); 72 } 73 } 74 75 const [constellationURL] = useAtom(constellationURLAtom) 76 77 useEffect(() => { 78 if (!did) return; 79 setLoading(true); 80 setError(null); 81 const urls = [ 82 `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`, 83 `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`, 84 `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`, 85 ]; 86 let ignore = false; 87 Promise.all( 88 urls.map(async (url) => { 89 try { 90 const r = await fetch(url); 91 if (!r.ok) throw new Error("Failed to fetch"); 92 const text = await r.text(); 93 if (!text) return null; 94 try { 95 return JSON.parse(text); 96 } catch { 97 return null; 98 } 99 } catch (e: any) { 100 return { error: e?.message || String(e) }; 101 } 102 }) 103 ) 104 .then((results) => { 105 if (!ignore) setResponses(results); 106 }) 107 .catch((e) => { 108 if (!ignore) 109 setError("Failed to fetch notifications: " + (e?.message || e)); 110 }) 111 .finally(() => { 112 if (!ignore) setLoading(false); 113 }); 114 return () => { 115 ignore = true; 116 }; 117 }, [did]); 118 119 return ( 120 <div className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800"> 121 <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-800"> 122 <span className="text-xl font-bold ml-2">Notifications</span> 123 {!authed && ( 124 <div className="flex items-center gap-2"> 125 <input 126 type="text" 127 placeholder="Enter handle or DID" 128 ref={inputRef} 129 className="ml-4 px-2 py-1 rounded border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100" 130 style={{ minWidth: 220 }} 131 disabled={resolving} 132 /> 133 <button 134 type="button" 135 className="px-3 py-1 rounded bg-blue-600 text-white font-semibold disabled:opacity-50" 136 disabled={resolving} 137 onClick={handleSubmit} 138 > 139 {resolving ? "Resolving..." : "Submit"} 140 </button> 141 </div> 142 )} 143 </div> 144 {error && <div className="p-4 text-red-500">{error}</div>} 145 {loading && ( 146 <div className="p-4 text-gray-500">Loading notifications...</div> 147 )} 148 {!loading && 149 !error && 150 responses.map((resp, i) => ( 151 <div key={i} className="p-4"> 152 <div className="font-bold mb-2">Query {i + 1}</div> 153 {!resp || 154 (typeof resp === "object" && Object.keys(resp).length === 0) || 155 (Array.isArray(resp) && resp.length === 0) ? ( 156 <div className="text-gray-500">No notifications found.</div> 157 ) : ( 158 <pre 159 style={{ 160 background: "#222", 161 color: "#eee", 162 borderRadius: 8, 163 padding: 12, 164 fontSize: 13, 165 overflowX: "auto", 166 }} 167 > 168 {JSON.stringify(resp, null, 2)} 169 </pre> 170 )} 171 </div> 172 ))} 173 {/* <div className="p-4"> yo this project sucks, ill remake it some other time, like cmon inputting anything into the textbox makes it break. ive warned you</div> */} 174 </div> 175 ); 176}