its for when you want to get like notifications for your reposts
1import { createSignal, onCleanup, For, type Component } from "solid-js"; 2 3import type {} from "@atcute/bluesky"; 4import type {} from "@atcute/atproto"; 5import { isDid, isHandle } from "@atcute/lexicons/syntax"; 6import { XrpcHandleResolver } from "@atcute/identity-resolver"; 7import { Notification } from "./types.js"; 8import { ActivityItem } from "./ActivityItem.jsx"; 9 10const handleResolver = new XrpcHandleResolver({ 11 serviceUrl: "https://public.api.bsky.app", 12}); 13 14const App: Component = () => { 15 const [actorId, setActorId] = createSignal<string>(""); 16 const [serviceDomain, setWsUrl] = createSignal<string>("likes.gaze.systems"); 17 const [isConnected, setIsConnected] = createSignal<boolean>(false); 18 const [items, setItems] = createSignal<Notification[]>([]); 19 const [connectionStatus, setConnectionStatus] = createSignal< 20 "disconnected" | "connecting..." | "connected" | "error" 21 >("disconnected"); 22 const [error, setError] = createSignal<string | null>(null); 23 24 let ws: WebSocket | null = null; 25 26 const connectWebSocket = async () => { 27 const didOrHandle = actorId().trim(); 28 const host = serviceDomain().trim(); 29 30 setError(null); 31 setConnectionStatus("connecting..."); 32 33 let did: string; 34 if (!didOrHandle) { 35 setConnectionStatus("error"); 36 setError("please enter a DID or a handle"); 37 return; 38 } else if (isHandle(didOrHandle)) { 39 try { 40 did = await handleResolver.resolve(didOrHandle); 41 } catch (error) { 42 setConnectionStatus("error"); 43 setError(`can't resolve handle: ${error}`); 44 return; 45 } 46 } else if (isDid(didOrHandle)) { 47 did = didOrHandle; 48 } else { 49 setConnectionStatus("error"); 50 setError("inputted DID / handle is not valid"); 51 return; 52 } 53 54 if (!host) { 55 setError("please enter service host"); 56 setConnectionStatus("error"); 57 return; 58 } 59 60 // Close existing connection if any 61 if (ws) { 62 ws.close(); 63 } 64 65 let proto = "wss"; 66 const domain = host.split(":").at(0) ?? ""; 67 if (["localhost", "0.0.0.0", "127.0.0.1"].some((v) => v === domain)) { 68 proto = "ws"; 69 } 70 71 const url = `${proto}://${host}/subscribe/${did}`; 72 73 try { 74 ws = new WebSocket(url); 75 76 ws.onopen = () => { 77 setIsConnected(true); 78 setConnectionStatus("connected"); 79 setError(null); 80 console.log("WebSocket connected to:", url); 81 }; 82 83 ws.onmessage = (event: MessageEvent) => { 84 try { 85 const data: Notification = JSON.parse(event.data); 86 setItems((prev) => [data, ...prev]); // add new items to the top 87 } catch (error) { 88 console.error("Error parsing JSON:", error); 89 } 90 }; 91 92 ws.onclose = () => { 93 setIsConnected(false); 94 setConnectionStatus("disconnected"); 95 console.log("WebSocket disconnected"); 96 }; 97 98 ws.onerror = (error: Event) => { 99 setConnectionStatus("error"); 100 setError(`connection failed: ${error}`); 101 console.error("WebSocket error:", error); 102 }; 103 } catch (error) { 104 setConnectionStatus("error"); 105 setError(`failed to create connection: ${error}`); 106 console.error("Failed to create WebSocket:", error); 107 } 108 }; 109 110 const disconnect = (): void => { 111 if (ws) { 112 ws.close(); 113 ws = null; 114 } 115 }; 116 117 const clearItems = (): void => { 118 setItems([]); 119 }; 120 121 onCleanup(() => { 122 if (ws) { 123 ws.close(); 124 } 125 }); 126 127 return ( 128 <div max-w-4xl mx-auto p-6 bg-gray-50 min-h-screen> 129 <h1 border="l-16 blue" font-bold text="3xl gray-800" pl-2 mb-6> 130 monitor bluesky repost likes 131 </h1> 132 133 {/* connection */} 134 <div mb-6> 135 <div flex gap-2 mb-2> 136 <input 137 type="text" 138 value={serviceDomain()} 139 onInput={(e) => setWsUrl((e.target as HTMLInputElement).value)} 140 placeholder="enter service host (e.g., likes.gaze.systems)" 141 class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-purple-500) bg-white" 142 disabled={isConnected() || connectionStatus() == "connecting..."} 143 /> 144 </div> 145 <div flex gap-2 mb-2> 146 <input 147 type="text" 148 value={actorId()} 149 onInput={(e) => setActorId((e.target as HTMLInputElement).value)} 150 onKeyPress={(e) => { 151 if (!isConnected() && e.key == "Enter") { 152 connectWebSocket(); 153 e.preventDefault(); 154 } 155 }} 156 placeholder="enter handle or DID" 157 class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-blue-500) bg-white" 158 disabled={isConnected() || connectionStatus() == "connecting..."} 159 /> 160 <button 161 onClick={() => (isConnected() ? disconnect() : connectWebSocket())} 162 class={`px-6 py-2 rounded-none font-medium transition-colors ${ 163 isConnected() 164 ? "bg-red-500 hover:bg-red-600 text-white" 165 : "bg-blue-500 hover:bg-blue-600 text-white" 166 }`} 167 > 168 {isConnected() ? "Disconnect" : "Connect"} 169 </button> 170 </div> 171 172 {/* Status indicator */} 173 <div flex gap-2 items-center> 174 <div w-fit border border-gray-300 bg-gray-80 px-1 py="0.5"> 175 <div 176 inline-block 177 w-3 178 h-3 179 rounded-full 180 class={ 181 connectionStatus() === "connected" 182 ? "bg-green-500" 183 : connectionStatus() === "connecting..." 184 ? "bg-yellow-500" 185 : connectionStatus() === "error" 186 ? "bg-red-500" 187 : "bg-gray-400" 188 } 189 /> 190 <span ml-2 align="10%" text="sm gray-600"> 191 status: {connectionStatus()} 192 </span> 193 </div> 194 {error() && ( 195 <div w-fit border border-gray-300 bg-gray-80 p-1> 196 <div text="sm red-500">{error()}</div> 197 </div> 198 )} 199 </div> 200 </div> 201 202 {/* feed */} 203 <div class="mb-4"> 204 <div class="flex justify-between items-center mb-4"> 205 <h2 border="l-8 blue" pl-2 text="xl gray-700" font-semibold> 206 activity feed ({items().length}) 207 </h2> 208 <button 209 onClick={clearItems} 210 text="white sm" 211 class="px-4 py-2 bg-gray-500 hover:bg-gray-600 rounded-none transition-colors disabled:opacity-50" 212 disabled={items().length === 0} 213 > 214 clear feed 215 </button> 216 </div> 217 218 <div class="h-[60vh] max-h-[60vh] overflow-y-auto border border-gray-200 rounded-none p-4 bg-white"> 219 {items().length === 0 ? ( 220 <div flex items-center w-full h-full> 221 <div mx-auto text="center gray-500"> 222 <div text-lg mb-2> 223 👀 224 </div> 225 <div> 226 nothing yet. connect and wait for someone to like a repost of 227 yours! 228 </div> 229 </div> 230 </div> 231 ) : ( 232 <For each={items()}> 233 {(item, index) => ( 234 <div mb={index() == items().length - 1 ? "0" : "2"}> 235 <ActivityItem data={item} /> 236 </div> 237 )} 238 </For> 239 )} 240 </div> 241 </div> 242 243 {/* Instructions */} 244 <div border bg-blue-50 border-blue-200 rounded-none pl="1.5" p-1> 245 <span text="xs blue-800" align="10%"> 246 <span text-pink-400>source</span> <span text-gray>=</span>{" "} 247 <a 248 href="https://tangled.sh/@poor.dog/bsky-repost-likes" 249 text-orange-700 250 hover:text-orange-400 251 > 252 "https://tangled.sh/@poor.dog/bsky-repost-likes" 253 </a>{" "} 254 // made by{" "} 255 <a text-purple-700 hover:text-purple href="https://gaze.systems"> 256 dusk 257 </a> 258 </span> 259 </div> 260 </div> 261 ); 262}; 263 264export default App;