its for when you want to get like notifications for your reposts

feat: extension specific theming, improve extension websocket handling

ptr.pet e500f4a1 2ec37d72

verified
+128 -67
+39 -18
extension/entrypoints/background.ts
··· 12 12 13 13 export default defineBackground({ 14 14 persistent: true, 15 - main: () => { 16 - onMessage("connectService", ({ data: { actorId, serviceDomain } }) => { 17 - connectService({ 18 - actorId, 19 - serviceDomain, 20 - pushNotification: (item) => { 21 - items = [item, ...items]; 22 - sendMessage("setItems", items, "popup"); 23 - }, 24 - setConnectionStatus, 25 - setError, 26 - }).then((ws) => (websocket = ws ?? null)); 27 - }); 28 - onMessage("disconnectService", () => { 29 - setConnectionStatus("disconnecting..."); 30 - websocket?.close(); 31 - websocket = null; 32 - }); 15 + main: async () => { 16 + onMessage("connectService", connect); 17 + onMessage("disconnectService", disconnect); 33 18 onMessage("connectionStatus", () => { 34 19 return connectionStatus; 35 20 }); ··· 42 27 onMessage("setItems", ({ data }) => { 43 28 items = data; 44 29 }); 30 + 31 + // connect on service start once 32 + let actorId = await store.actorId.getValue(); 33 + let serviceDomain = await store.serviceDomain.getValue(); 34 + if (actorId.length > 0 && serviceDomain.length > 0) { 35 + connect({ data: { actorId, serviceDomain } }); 36 + } 37 + 38 + // send pings to keep service alive 39 + setInterval(() => { 40 + if (websocket && connectionStatus === "connected") websocket.send(""); 41 + }, 1000 * 15); 45 42 }, 46 43 }); 47 44 45 + const connect = ({ 46 + data: { actorId, serviceDomain }, 47 + }: { 48 + data: { actorId: string; serviceDomain: string }; 49 + }) => { 50 + connectService({ 51 + actorId, 52 + serviceDomain, 53 + pushNotification: (item) => { 54 + items = [item, ...items]; 55 + sendMessage("setItems", items, "popup"); 56 + }, 57 + setConnectionStatus, 58 + setError, 59 + backoff: 1000 * 1, 60 + }).then((ws) => { 61 + websocket = ws ?? null; 62 + }); 63 + }; 64 + const disconnect = () => { 65 + setConnectionStatus("disconnecting..."); 66 + websocket?.close(); 67 + websocket = null; 68 + }; 48 69 const setConnectionStatus = (status: ConnectionStatus) => { 49 70 connectionStatus = status; 50 71 sendMessage("setConnectionStatus", status, "popup");
+1 -1
extension/entrypoints/popup/index.html
··· 6 6 <title>bsky repost likes monitor</title> 7 7 <meta name="manifest.type" content="browser_action" /> 8 8 </head> 9 - <body> 9 + <body style="width: 60ch"> 10 10 <div id="root"></div> 11 11 <script type="module" src="./main.tsx"></script> 12 12 </body>
+1
extension/entrypoints/popup/main.tsx
··· 78 78 error, 79 79 connect, 80 80 disconnect, 81 + isExtension: true, 81 82 }; 82 83 83 84 return <App {...props}></App>;
+1
server/.gitignore
··· 1 + bsky-repost-likes.exe
+14 -9
webapp/src/ActivityItem.tsx
··· 6 6 7 7 interface ActivityItemProps { 8 8 data: Notification; 9 + isExtension: boolean; 9 10 } 10 11 11 12 export const ActivityItem: Component<ActivityItemProps> = (props) => { ··· 33 34 }`} 34 35 > 35 36 <p text-wrap> 36 - <span text-lg>{props.data.liked ? "❤️" : "💔"}</span>{" "} 37 + <span text={props.isExtension ? "sm" : "lg"}> 38 + {props.data.liked ? "❤️" : "💔"} 39 + </span>{" "} 37 40 {(profile && ( 38 - <span font-medium text="sm gray-700"> 39 - {profile!.displayName ?? profile!.handle}{" "} 40 - {profile!.displayName && ( 41 - <span font-normal text-gray-500> 42 - (@{profile!.handle}) 43 - </span> 44 - )} 41 + <span 42 + font-medium 43 + text={`${props.isExtension ? "xs" : "sm"} gray-700`} 44 + title={`@${profile!.handle}`} 45 + > 46 + {profile!.displayName ?? profile!.handle} 45 47 </span> 46 48 )) || ( 47 - <span font-medium text="sm gray-700"> 49 + <span 50 + font-medium 51 + text={`${props.isExtension ? "xs" : "sm"} gray-700`} 52 + > 48 53 {props.data.actor.did} 49 54 </span> 50 55 )}{" "}
+55 -32
webapp/src/App.tsx
··· 48 48 error, 49 49 connect, 50 50 disconnect, 51 + isExtension: false, 51 52 }; 52 53 53 54 return <App {...props} />; ··· 66 67 ); 67 68 }; 68 69 70 + 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)`; 71 + 69 72 return ( 70 - <div max-w-4xl mx-auto p-4 bg-gray-50 min-h-screen> 71 - <h1 border="l-16 blue" font-bold text="3xl gray-800" pl-2 mb-6> 72 - monitor bluesky repost likes 73 - </h1> 73 + <div max-w-4xl mx-auto p-2 bg-gray-50 min-h-screen> 74 + {props.isExtension ? ( 75 + <></> 76 + ) : ( 77 + <h1 border="l-16 blue" font-bold text="3xl gray-800" pl-2 mb-6> 78 + monitor bluesky repost likes 79 + </h1> 80 + )} 74 81 75 82 {/* connection */} 76 83 <div mb-6> ··· 80 87 value={serviceDomain()} 81 88 onInput={(e) => setWsUrl((e.target as HTMLInputElement).value)} 82 89 placeholder="enter service host (e.g., likes.gaze.systems)" 83 - class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-purple-500) bg-white" 90 + class={`${inputStyle} focus-ring-purple-500`} 84 91 disabled={isConnected()} 85 92 /> 86 93 </div> ··· 96 103 } 97 104 }} 98 105 placeholder="enter handle or DID" 99 - class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-blue-500) bg-white" 106 + class={`${inputStyle} focus-ring-blue-500`} 100 107 disabled={isConnected()} 101 108 /> 102 109 <button 103 110 onClick={() => (isConnected() ? disconnect() : connect())} 104 - class={`px-6 py-2 rounded-none font-medium transition-colors ${ 111 + class={`${props.isExtension ? "px-3 py-1" : "px-6 py-2"} rounded-none font-medium transition-colors ${ 105 112 isConnected() 106 113 ? "bg-red-500 hover:bg-red-600 text-white" 107 114 : "bg-blue-500 hover:bg-blue-600 text-white" ··· 129 136 : "bg-gray-400" 130 137 } 131 138 /> 132 - <span ml-2 align="10%" text="sm gray-600"> 139 + <span 140 + ml-2 141 + align="10%" 142 + text={`${props.isExtension ? "xs" : "sm"} gray-600`} 143 + > 133 144 status: {connectionStatus()} 134 145 </span> 135 146 </div> 136 147 {error() && ( 137 148 <div w-fit border border-gray-300 bg-gray-80 p-1> 138 - <div text="sm red-500">{error()}</div> 149 + <div text={`${props.isExtension ? "xs" : "sm"} red-500`}> 150 + {error()} 151 + </div> 139 152 </div> 140 153 )} 141 154 </div> 142 155 </div> 143 156 144 157 {/* feed */} 145 - <div class="mb-4"> 158 + <div> 146 159 <div class="flex justify-between items-center mb-4"> 147 - <h2 border="l-8 blue" pl-2 text="xl gray-700" font-semibold> 160 + <h2 161 + border="l-8 blue" 162 + pl-2 163 + text={`${props.isExtension ? "base" : "xl"} gray-700`} 164 + font-semibold 165 + > 148 166 activity feed ({items().length}) 149 167 </h2> 150 168 <button 151 169 onClick={clearItems} 152 - text="white sm" 153 - class="px-4 py-2 bg-gray-500 hover:bg-gray-600 rounded-none transition-colors disabled:opacity-50" 170 + text={`white ${props.isExtension ? "xs" : "sm"}`} 171 + 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`} 154 172 disabled={items().length === 0} 155 173 > 156 174 clear feed 157 175 </button> 158 176 </div> 159 177 160 - <div class="h-[60vh] max-h-[60vh] overflow-y-auto border border-gray-200 rounded-none p-4 bg-white"> 178 + <div 179 + class={`${props.isExtension ? "p-2" : "p-4"} h-[60vh] max-h-[60vh] overflow-y-auto border border-gray-200 rounded-none bg-white`} 180 + > 161 181 {items().length === 0 ? ( 162 182 <div flex items-center w-full h-full> 163 183 <div mx-auto text="center gray-500"> ··· 174 194 <For each={items()}> 175 195 {(item, index) => ( 176 196 <div mb={index() == items().length - 1 ? "0" : "2"}> 177 - <ActivityItem data={item} /> 197 + <ActivityItem data={item} isExtension={props.isExtension} /> 178 198 </div> 179 199 )} 180 200 </For> ··· 182 202 </div> 183 203 </div> 184 204 185 - {/* Instructions */} 186 - <div border bg-blue-50 border-blue-200 rounded-none pl="1.5" p-1> 187 - <span text="xs blue-800" align="10%"> 188 - <span text-pink-400>source</span> <span text-gray>=</span>{" "} 189 - <a 190 - href="https://tangled.sh/@poor.dog/bsky-repost-likes" 191 - text-orange-700 192 - hover:text-orange-400 193 - > 194 - "https://tangled.sh/@poor.dog/bsky-repost-likes" 195 - </a>{" "} 196 - // made by{" "} 197 - <a text-purple-700 hover:text-purple href="https://gaze.systems"> 198 - dusk 199 - </a> 200 - </span> 201 - </div> 205 + {props.isExtension ? ( 206 + <></> 207 + ) : ( 208 + <div border bg-blue-50 border-blue-200 rounded-none pl="1.5" p-1 mt-4> 209 + <span text="xs blue-800" align="10%"> 210 + <span text-pink-400>source</span> <span text-gray>=</span>{" "} 211 + <a 212 + href="https://tangled.sh/@poor.dog/bsky-repost-likes" 213 + text-orange-700 214 + hover:text-orange-400 215 + > 216 + "https://tangled.sh/@poor.dog/bsky-repost-likes" 217 + </a>{" "} 218 + // made by{" "} 219 + <a text-purple-700 hover:text-purple href="https://gaze.systems"> 220 + dusk 221 + </a> 222 + </span> 223 + </div> 224 + )} 202 225 </div> 203 226 ); 204 227 };
+2
webapp/src/types.ts
··· 11 11 error: () => string | null; 12 12 connect: () => void; 13 13 disconnect: () => void; 14 + // options 15 + isExtension: boolean; 14 16 } 15 17 16 18 export interface Notification {
+15 -2
webapp/src/ws.ts
··· 8 8 pushNotification: (item: Notification) => void; 9 9 actorId: string; 10 10 serviceDomain: string; 11 + backoff?: number; 11 12 } 12 13 13 14 const handleResolver = new XrpcHandleResolver({ ··· 73 74 } 74 75 }; 75 76 76 - ws.onclose = () => { 77 + ws.onclose = (ev) => { 77 78 cb.setConnectionStatus("disconnected"); 78 79 console.log("WebSocket disconnected"); 80 + // abnormal closure 81 + if (ev.code === 1006 && cb.backoff) { 82 + cb.setConnectionStatus("error"); 83 + cb.setError(`websocket closed abnormally: (${ev.code}) ${ev.reason}`); 84 + const newData = { backoff: cb.backoff * 2, ...cb }; 85 + setTimeout(() => connect(newData), cb.backoff); 86 + } else if (ev.code === 1000 || ev.code === 1001 || ev.code === 1005) { 87 + cb.setError(null); 88 + } else { 89 + cb.setConnectionStatus("error"); 90 + cb.setError(`websocket failed: (${ev.code}) ${ev.reason}`); 91 + } 79 92 }; 80 93 81 94 ws.onerror = (error: Event) => { 82 95 cb.setConnectionStatus("error"); 83 - cb.setError(`connection failed: ${error}`); 96 + cb.setError("connection failed"); 84 97 console.error("WebSocket error:", error); 85 98 }; 86 99
-5
webapp/vite.config.lib.ts
··· 40 40 }, 41 41 rollupOptions: { 42 42 external: ["solid-js", "solid-js/web"], 43 - output: { 44 - globals: { 45 - "solid-js": "SolidJS", 46 - }, 47 - }, 48 43 }, 49 44 }, 50 45 });