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 tanstack-virtual 172 lines 6.0 kB view raw
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}