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