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 { AppProps, ConnectionStatus, Notification } from "./types.js"; 6import { ActivityItem } from "./ActivityItem.jsx"; 7import { connect as connectService } from "./ws.ts"; 8 9const Wrapped: Component = () => { 10 const [actorId, setActorId] = createSignal<string>(""); 11 const [serviceDomain, setWsUrl] = createSignal<string>("likes.gaze.systems"); 12 const [items, setItems] = createSignal<Notification[]>([]); 13 const [connectionStatus, setConnectionStatus] = 14 createSignal<ConnectionStatus>("disconnected"); 15 const [error, setError] = createSignal<string | null>(null); 16 const [ws, setWs] = createSignal<WebSocket | null>(null); 17 18 const connect = async () => { 19 // close existing connection if any 20 ws()?.close(); 21 setWs( 22 (await connectService({ 23 actorId: actorId(), 24 serviceDomain: serviceDomain(), 25 pushNotification: (item) => setItems((prev) => [item, ...prev]), 26 setConnectionStatus, 27 setError, 28 })) ?? null, 29 ); 30 }; 31 32 const disconnect = (): void => { 33 ws()?.close(); 34 setWs(null); 35 }; 36 37 onCleanup(disconnect); 38 39 const props: AppProps = { 40 actorIdSignal: [actorId, setActorId], 41 serviceDomainSignal: [serviceDomain, setWsUrl], 42 items, 43 clearItems: () => { 44 setItems([]); 45 }, 46 connectionStatus, 47 error, 48 connect, 49 disconnect, 50 isExtension: false, 51 }; 52 53 return <App {...props} />; 54}; 55export default Wrapped; 56 57export const App: Component<AppProps> = (props) => { 58 const [actorId, setActorId] = props.actorIdSignal; 59 const [serviceDomain, setWsUrl] = props.serviceDomainSignal; 60 const { disconnect, connect, error, connectionStatus, items, clearItems } = 61 props; 62 63 const isConnected = () => { 64 return ( 65 connectionStatus() == "connecting..." || connectionStatus() == "connected" 66 ); 67 }; 68 69 const inputStyle = `flex-1 ${props.isExtension ? "px-2 py-1" : "px-4 py-2"} border border-gray-300 rounded-none bg-white focus:(outline-none ring-2)`; 70 71 return ( 72 <div max-w-4xl mx-auto p-2 bg-gray-50 min-h-screen> 73 {props.isExtension ? ( 74 <></> 75 ) : ( 76 <h1 border="l-16 blue" font-bold text="3xl gray-800" pl-2 mb-6> 77 monitor bluesky repost likes 78 </h1> 79 )} 80 81 {/* connection */} 82 <div mb-6> 83 <div flex gap-2 mb-2> 84 <input 85 type="text" 86 value={serviceDomain()} 87 onInput={(e) => setWsUrl((e.target as HTMLInputElement).value)} 88 placeholder="enter service host (e.g., likes.gaze.systems)" 89 class={`${inputStyle} focus-ring-purple-500`} 90 disabled={isConnected()} 91 /> 92 </div> 93 <div flex gap-2 mb-2> 94 <input 95 type="text" 96 value={actorId()} 97 onInput={(e) => setActorId((e.target as HTMLInputElement).value)} 98 onKeyPress={(e) => { 99 if (!isConnected() && e.key == "Enter") { 100 connect(); 101 e.preventDefault(); 102 } 103 }} 104 placeholder="enter handle or DID" 105 class={`${inputStyle} focus-ring-blue-500`} 106 disabled={isConnected()} 107 /> 108 <button 109 onClick={() => (isConnected() ? disconnect() : connect())} 110 class={`${props.isExtension ? "px-3 py-1" : "px-6 py-2"} rounded-none font-medium transition-colors ${ 111 isConnected() 112 ? "bg-red-500 hover:bg-red-600 text-white" 113 : "bg-blue-500 hover:bg-blue-600 text-white" 114 }`} 115 > 116 {isConnected() ? "disconnect" : "connect"} 117 </button> 118 </div> 119 120 {/* Status indicator */} 121 <div flex gap-2 items-center> 122 <div w-fit border border-gray-300 bg-gray-80 px-1 py="0.5"> 123 <div 124 inline-block 125 w-3 126 h-3 127 rounded-full 128 class={ 129 connectionStatus() === "connected" 130 ? "bg-green-500" 131 : connectionStatus() === "connecting..." 132 ? "bg-yellow-500" 133 : connectionStatus() === "error" 134 ? "bg-red-500" 135 : "bg-gray-400" 136 } 137 /> 138 <span 139 ml-2 140 align="10%" 141 text={`${props.isExtension ? "xs" : "sm"} gray-600`} 142 > 143 status: {connectionStatus()} 144 </span> 145 </div> 146 {error() && ( 147 <div w-fit border border-gray-300 bg-gray-80 p-1> 148 <div text={`${props.isExtension ? "xs" : "sm"} red-500`}> 149 {error()} 150 </div> 151 </div> 152 )} 153 </div> 154 </div> 155 156 {/* feed */} 157 <div> 158 <div class="flex justify-between items-center mb-4"> 159 <h2 160 border="l-8 blue" 161 pl-2 162 text={`${props.isExtension ? "base" : "xl"} gray-700`} 163 font-semibold 164 > 165 activity feed ({items().length}) 166 </h2> 167 <button 168 onClick={clearItems} 169 text={`white ${props.isExtension ? "xs" : "sm"}`} 170 class={`${props.isExtension ? "px-2 py-1" : "px-4 py-2"} ml-1 bg-gray-500 hover:bg-gray-600 rounded-none transition-colors disabled:opacity-50`} 171 disabled={items().length === 0} 172 > 173 clear feed 174 </button> 175 </div> 176 177 <div 178 class={`${props.isExtension ? "p-2" : "p-4"} h-[60vh] max-h-[60vh] overflow-y-auto border border-gray-200 rounded-none bg-white`} 179 > 180 {items().length === 0 ? ( 181 <div flex items-center w-full h-full> 182 <div mx-auto text="center gray-500"> 183 <div text-lg mb-2> 184 👀 185 </div> 186 <div> 187 nothing yet. connect and wait for someone to like a repost of 188 yours! 189 </div> 190 </div> 191 </div> 192 ) : ( 193 <For each={items()}> 194 {(item, index) => ( 195 <div mb={index() == items().length - 1 ? "0" : "2"}> 196 <ActivityItem data={item} isExtension={props.isExtension} /> 197 </div> 198 )} 199 </For> 200 )} 201 </div> 202 </div> 203 204 {props.isExtension ? ( 205 <></> 206 ) : ( 207 <div border bg-blue-50 border-blue-200 rounded-none pl="1.5" p-1 mt-4> 208 <span text="xs blue-800" align="10%"> 209 <span text-pink-400>source</span> <span text-gray>=</span>{" "} 210 <a 211 href="https://tangled.sh/@poor.dog/bsky-repost-likes" 212 text-orange-700 213 hover:text-orange-400 214 > 215 "https://tangled.sh/@poor.dog/bsky-repost-likes" 216 </a>{" "} 217 // made by{" "} 218 <a text-purple-700 hover:text-purple href="https://gaze.systems"> 219 dusk 220 </a> 221 </span> 222 </div> 223 )} 224 </div> 225 ); 226};