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