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