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
Changed files
+128 -67
extension
entrypoints
server
webapp
+39 -18
extension/entrypoints/background.ts
···
export default defineBackground({
persistent: true,
-
main: () => {
-
onMessage("connectService", ({ data: { actorId, serviceDomain } }) => {
-
connectService({
-
actorId,
-
serviceDomain,
-
pushNotification: (item) => {
-
items = [item, ...items];
-
sendMessage("setItems", items, "popup");
-
},
-
setConnectionStatus,
-
setError,
-
}).then((ws) => (websocket = ws ?? null));
-
});
-
onMessage("disconnectService", () => {
-
setConnectionStatus("disconnecting...");
-
websocket?.close();
-
websocket = null;
-
});
+
main: async () => {
+
onMessage("connectService", connect);
+
onMessage("disconnectService", disconnect);
onMessage("connectionStatus", () => {
return connectionStatus;
});
···
onMessage("setItems", ({ data }) => {
items = data;
});
+
+
// connect on service start once
+
let actorId = await store.actorId.getValue();
+
let serviceDomain = await store.serviceDomain.getValue();
+
if (actorId.length > 0 && serviceDomain.length > 0) {
+
connect({ data: { actorId, serviceDomain } });
+
}
+
+
// send pings to keep service alive
+
setInterval(() => {
+
if (websocket && connectionStatus === "connected") websocket.send("");
+
}, 1000 * 15);
},
});
+
const connect = ({
+
data: { actorId, serviceDomain },
+
}: {
+
data: { actorId: string; serviceDomain: string };
+
}) => {
+
connectService({
+
actorId,
+
serviceDomain,
+
pushNotification: (item) => {
+
items = [item, ...items];
+
sendMessage("setItems", items, "popup");
+
},
+
setConnectionStatus,
+
setError,
+
backoff: 1000 * 1,
+
}).then((ws) => {
+
websocket = ws ?? null;
+
});
+
};
+
const disconnect = () => {
+
setConnectionStatus("disconnecting...");
+
websocket?.close();
+
websocket = null;
+
};
const setConnectionStatus = (status: ConnectionStatus) => {
connectionStatus = status;
sendMessage("setConnectionStatus", status, "popup");
+1 -1
extension/entrypoints/popup/index.html
···
<title>bsky repost likes monitor</title>
<meta name="manifest.type" content="browser_action" />
</head>
-
<body>
+
<body style="width: 60ch">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
+1
extension/entrypoints/popup/main.tsx
···
error,
connect,
disconnect,
+
isExtension: true,
};
return <App {...props}></App>;
+1
server/.gitignore
···
+
bsky-repost-likes.exe
+14 -9
webapp/src/ActivityItem.tsx
···
interface ActivityItemProps {
data: Notification;
+
isExtension: boolean;
}
export const ActivityItem: Component<ActivityItemProps> = (props) => {
···
}`}
>
<p text-wrap>
-
<span text-lg>{props.data.liked ? "❤️" : "💔"}</span>{" "}
+
<span text={props.isExtension ? "sm" : "lg"}>
+
{props.data.liked ? "❤️" : "💔"}
+
</span>{" "}
{(profile && (
-
<span font-medium text="sm gray-700">
-
{profile!.displayName ?? profile!.handle}{" "}
-
{profile!.displayName && (
-
<span font-normal text-gray-500>
-
(@{profile!.handle})
-
</span>
-
)}
+
<span
+
font-medium
+
text={`${props.isExtension ? "xs" : "sm"} gray-700`}
+
title={`@${profile!.handle}`}
+
>
+
{profile!.displayName ?? profile!.handle}
</span>
)) || (
-
<span font-medium text="sm gray-700">
+
<span
+
font-medium
+
text={`${props.isExtension ? "xs" : "sm"} gray-700`}
+
>
{props.data.actor.did}
</span>
)}{" "}
+55 -32
webapp/src/App.tsx
···
error,
connect,
disconnect,
+
isExtension: false,
};
return <App {...props} />;
···
);
};
+
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)`;
+
return (
-
<div max-w-4xl mx-auto p-4 bg-gray-50 min-h-screen>
-
<h1 border="l-16 blue" font-bold text="3xl gray-800" pl-2 mb-6>
-
monitor bluesky repost likes
-
</h1>
+
<div max-w-4xl mx-auto p-2 bg-gray-50 min-h-screen>
+
{props.isExtension ? (
+
<></>
+
) : (
+
<h1 border="l-16 blue" font-bold text="3xl gray-800" pl-2 mb-6>
+
monitor bluesky repost likes
+
</h1>
+
)}
{/* connection */}
<div mb-6>
···
value={serviceDomain()}
onInput={(e) => setWsUrl((e.target as HTMLInputElement).value)}
placeholder="enter service host (e.g., likes.gaze.systems)"
-
class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-purple-500) bg-white"
+
class={`${inputStyle} focus-ring-purple-500`}
disabled={isConnected()}
/>
</div>
···
}
}}
placeholder="enter handle or DID"
-
class="flex-1 px-4 py-2 border border-gray-300 rounded-none focus:(outline-none ring-2 ring-blue-500) bg-white"
+
class={`${inputStyle} focus-ring-blue-500`}
disabled={isConnected()}
/>
<button
onClick={() => (isConnected() ? disconnect() : connect())}
-
class={`px-6 py-2 rounded-none font-medium transition-colors ${
+
class={`${props.isExtension ? "px-3 py-1" : "px-6 py-2"} rounded-none font-medium transition-colors ${
isConnected()
? "bg-red-500 hover:bg-red-600 text-white"
: "bg-blue-500 hover:bg-blue-600 text-white"
···
: "bg-gray-400"
}
/>
-
<span ml-2 align="10%" text="sm gray-600">
+
<span
+
ml-2
+
align="10%"
+
text={`${props.isExtension ? "xs" : "sm"} gray-600`}
+
>
status: {connectionStatus()}
</span>
</div>
{error() && (
<div w-fit border border-gray-300 bg-gray-80 p-1>
-
<div text="sm red-500">{error()}</div>
+
<div text={`${props.isExtension ? "xs" : "sm"} red-500`}>
+
{error()}
+
</div>
</div>
)}
</div>
</div>
{/* feed */}
-
<div class="mb-4">
+
<div>
<div class="flex justify-between items-center mb-4">
-
<h2 border="l-8 blue" pl-2 text="xl gray-700" font-semibold>
+
<h2
+
border="l-8 blue"
+
pl-2
+
text={`${props.isExtension ? "base" : "xl"} gray-700`}
+
font-semibold
+
>
activity feed ({items().length})
</h2>
<button
onClick={clearItems}
-
text="white sm"
-
class="px-4 py-2 bg-gray-500 hover:bg-gray-600 rounded-none transition-colors disabled:opacity-50"
+
text={`white ${props.isExtension ? "xs" : "sm"}`}
+
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`}
disabled={items().length === 0}
>
clear feed
</button>
</div>
-
<div class="h-[60vh] max-h-[60vh] overflow-y-auto border border-gray-200 rounded-none p-4 bg-white">
+
<div
+
class={`${props.isExtension ? "p-2" : "p-4"} h-[60vh] max-h-[60vh] overflow-y-auto border border-gray-200 rounded-none bg-white`}
+
>
{items().length === 0 ? (
<div flex items-center w-full h-full>
<div mx-auto text="center gray-500">
···
<For each={items()}>
{(item, index) => (
<div mb={index() == items().length - 1 ? "0" : "2"}>
-
<ActivityItem data={item} />
+
<ActivityItem data={item} isExtension={props.isExtension} />
</div>
)}
</For>
···
</div>
</div>
-
{/* Instructions */}
-
<div border bg-blue-50 border-blue-200 rounded-none pl="1.5" p-1>
-
<span text="xs blue-800" align="10%">
-
<span text-pink-400>source</span> <span text-gray>=</span>{" "}
-
<a
-
href="https://tangled.sh/@poor.dog/bsky-repost-likes"
-
text-orange-700
-
hover:text-orange-400
-
>
-
"https://tangled.sh/@poor.dog/bsky-repost-likes"
-
</a>{" "}
-
// made by{" "}
-
<a text-purple-700 hover:text-purple href="https://gaze.systems">
-
dusk
-
</a>
-
</span>
-
</div>
+
{props.isExtension ? (
+
<></>
+
) : (
+
<div border bg-blue-50 border-blue-200 rounded-none pl="1.5" p-1 mt-4>
+
<span text="xs blue-800" align="10%">
+
<span text-pink-400>source</span> <span text-gray>=</span>{" "}
+
<a
+
href="https://tangled.sh/@poor.dog/bsky-repost-likes"
+
text-orange-700
+
hover:text-orange-400
+
>
+
"https://tangled.sh/@poor.dog/bsky-repost-likes"
+
</a>{" "}
+
// made by{" "}
+
<a text-purple-700 hover:text-purple href="https://gaze.systems">
+
dusk
+
</a>
+
</span>
+
</div>
+
)}
</div>
);
};
+2
webapp/src/types.ts
···
error: () => string | null;
connect: () => void;
disconnect: () => void;
+
// options
+
isExtension: boolean;
}
export interface Notification {
+15 -2
webapp/src/ws.ts
···
pushNotification: (item: Notification) => void;
actorId: string;
serviceDomain: string;
+
backoff?: number;
}
const handleResolver = new XrpcHandleResolver({
···
}
};
-
ws.onclose = () => {
+
ws.onclose = (ev) => {
cb.setConnectionStatus("disconnected");
console.log("WebSocket disconnected");
+
// abnormal closure
+
if (ev.code === 1006 && cb.backoff) {
+
cb.setConnectionStatus("error");
+
cb.setError(`websocket closed abnormally: (${ev.code}) ${ev.reason}`);
+
const newData = { backoff: cb.backoff * 2, ...cb };
+
setTimeout(() => connect(newData), cb.backoff);
+
} else if (ev.code === 1000 || ev.code === 1001 || ev.code === 1005) {
+
cb.setError(null);
+
} else {
+
cb.setConnectionStatus("error");
+
cb.setError(`websocket failed: (${ev.code}) ${ev.reason}`);
+
}
};
ws.onerror = (error: Event) => {
cb.setConnectionStatus("error");
-
cb.setError(`connection failed: ${error}`);
+
cb.setError("connection failed");
console.error("WebSocket error:", error);
};
-5
webapp/vite.config.lib.ts
···
},
rollupOptions: {
external: ["solid-js", "solid-js/web"],
-
output: {
-
globals: {
-
"solid-js": "SolidJS",
-
},
-
},
},
},
});