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};