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 { ATProtoActivity } 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<ATProtoActivity[]>([]); 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 const url = `wss://${host}/subscribe/${did}`; 66 67 try { 68 ws = new WebSocket(url); 69 70 ws.onopen = () => { 71 setIsConnected(true); 72 setConnectionStatus("connected"); 73 setError(null); 74 console.log("WebSocket connected to:", url); 75 }; 76 77 ws.onmessage = (event: MessageEvent) => { 78 try { 79 const data: ATProtoActivity = JSON.parse(event.data); 80 setItems((prev) => [data, ...prev]); // add new items to the top 81 } catch (error) { 82 console.error("Error parsing JSON:", error); 83 } 84 }; 85 86 ws.onclose = () => { 87 setIsConnected(false); 88 setConnectionStatus("disconnected"); 89 console.log("WebSocket disconnected"); 90 }; 91 92 ws.onerror = (error: Event) => { 93 setConnectionStatus("error"); 94 setError(`connection failed: ${error}`); 95 console.error("WebSocket error:", error); 96 }; 97 } catch (error) { 98 setConnectionStatus("error"); 99 setError(`failed to create connection: ${error}`); 100 console.error("Failed to create WebSocket:", error); 101 } 102 }; 103 104 const disconnect = (): void => { 105 if (ws) { 106 ws.close(); 107 ws = null; 108 } 109 }; 110 111 const clearItems = (): void => { 112 setItems([]); 113 }; 114 115 onCleanup(() => { 116 if (ws) { 117 ws.close(); 118 } 119 }); 120 121 return ( 122 <div max-w-4xl mx-auto p-6 bg-gray-50 min-h-screen> 123 <h1 border="l-16 blue" font-bold text="3xl gray-800" pl-2 mb-6> 124 monitor bluesky repost likes 125 </h1> 126 127 {/* connection */} 128 <div mb-6> 129 <div flex gap-2 mb-2> 130 <input 131 type="text" 132 value={serviceDomain()} 133 onInput={(e) => setWsUrl((e.target as HTMLInputElement).value)} 134 placeholder="enter service host (e.g., likes.gaze.systems)" 135 class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-purple-500) bg-white" 136 disabled={isConnected() || connectionStatus() == "connecting..."} 137 /> 138 </div> 139 <div flex gap-2 mb-2> 140 <input 141 type="text" 142 value={actorId()} 143 onInput={(e) => setActorId((e.target as HTMLInputElement).value)} 144 onKeyPress={(e) => { 145 if (!isConnected() && e.key == "Enter") { 146 connectWebSocket(); 147 e.preventDefault(); 148 } 149 }} 150 placeholder="enter handle or DID" 151 class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-blue-500) bg-white" 152 disabled={isConnected() || connectionStatus() == "connecting..."} 153 /> 154 <button 155 onClick={() => (isConnected() ? disconnect() : connectWebSocket())} 156 class={`px-6 py-2 rounded-none font-medium transition-colors ${ 157 isConnected() 158 ? "bg-red-500 hover:bg-red-600 text-white" 159 : "bg-blue-500 hover:bg-blue-600 text-white" 160 }`} 161 > 162 {isConnected() ? "Disconnect" : "Connect"} 163 </button> 164 </div> 165 166 {/* Status indicator */} 167 <div flex gap-2 items-center> 168 <div w-fit border border-gray-300 bg-gray-80 px-1 py="0.5"> 169 <div 170 inline-block 171 w-3 172 h-3 173 rounded-full 174 class={ 175 connectionStatus() === "connected" 176 ? "bg-green-500" 177 : connectionStatus() === "connecting..." 178 ? "bg-yellow-500" 179 : connectionStatus() === "error" 180 ? "bg-red-500" 181 : "bg-gray-400" 182 } 183 /> 184 <span ml-2 align="10%" text="sm gray-600"> 185 status: {connectionStatus()} 186 </span> 187 </div> 188 {error() && ( 189 <div w-fit border border-gray-300 bg-gray-80 p-1> 190 <div text="sm red-500">{error()}</div> 191 </div> 192 )} 193 </div> 194 </div> 195 196 {/* feed */} 197 <div class="mb-4"> 198 <div class="flex justify-between items-center mb-4"> 199 <h2 border="l-8 blue" pl-2 text="xl gray-700" font-semibold> 200 activity feed ({items().length}) 201 </h2> 202 <button 203 onClick={clearItems} 204 text="white sm" 205 class="px-4 py-2 bg-gray-500 hover:bg-gray-600 rounded-none transition-colors disabled:opacity-50" 206 disabled={items().length === 0} 207 > 208 clear feed 209 </button> 210 </div> 211 212 <div class="h-[60vh] max-h-[60vh] overflow-y-auto border border-gray-200 rounded-none p-4 bg-white"> 213 {items().length === 0 ? ( 214 <div flex items-center w-full h-full> 215 <div mx-auto text="center gray-500"> 216 <div text-lg mb-2> 217 👀 218 </div> 219 <div> 220 nothing yet. connect and wait for someone to like a repost of 221 yours! 222 </div> 223 </div> 224 </div> 225 ) : ( 226 <For each={items()}> 227 {(item, index) => ( 228 <div mb={index() == items().length - 1 ? "0" : "2"}> 229 <ActivityItem data={item} /> 230 </div> 231 )} 232 </For> 233 )} 234 </div> 235 </div> 236 237 {/* Instructions */} 238 <div border bg-blue-50 border-blue-200 rounded-none pl="1.5" p-1> 239 <span text="xs blue-800" align="10%"> 240 <span text-pink-400>source</span> <span text-gray>=</span>{" "} 241 <a 242 href="https://tangled.sh/@poor.dog/bsky-repost-likes" 243 text-orange-700 244 hover:text-orange-400 245 > 246 "https://tangled.sh/@poor.dog/bsky-repost-likes" 247 </a>{" "} 248 // made by{" "} 249 <a text-purple-700 hover:text-purple href="https://gaze.systems"> 250 dusk 251 </a> 252 </span> 253 </div> 254 </div> 255 ); 256}; 257 258export default App;