tangled
alpha
login
or
join now
olaren.dev
/
pdsls
forked from
pds.ls/pdsls
0
fork
atom
atmosphere explorer
0
fork
atom
overview
issues
pulls
pipelines
new notification system
handle.invalid
4 months ago
0040a4a7
c67f0065
verified
This commit was signed with the committer's
known signature
.
handle.invalid
SSH Key Fingerprint:
SHA256:mBrT4x0JdzLpbVR95g1hjI1aaErfC02kmLRkPXwsYCk=
+181
-42
8 changed files
expand all
collapse all
unified
split
src
components
create.tsx
json.tsx
notification.tsx
layout.tsx
utils
copy.ts
views
collection.tsx
record.tsx
repo.tsx
+11
-3
src/components/create.tsx
···
6
6
import { createEffect, createSignal, For, onCleanup, onMount, Show } from "solid-js";
7
7
import { Editor, editorView } from "../components/editor.jsx";
8
8
import { agent } from "../components/login.jsx";
9
9
-
import { setNotif } from "../layout.jsx";
10
9
import { sessions } from "./account.jsx";
11
10
import { Button } from "./button.jsx";
12
11
import { Modal } from "./modal.jsx";
12
12
+
import { addNotification, removeNotification } from "./notification.jsx";
13
13
import { TextInput } from "./text-input.jsx";
14
14
import Tooltip from "./tooltip.jsx";
15
15
···
93
93
return;
94
94
}
95
95
setOpenDialog(false);
96
96
-
setNotif({ show: true, icon: "lucide--file-check", text: "Record created" });
96
96
+
const id = addNotification({
97
97
+
message: "Record created",
98
98
+
type: "success",
99
99
+
});
100
100
+
setTimeout(() => removeNotification(id), 3000);
97
101
navigate(`/${res.data.uri}`);
98
102
};
99
103
···
143
147
}
144
148
}
145
149
setOpenDialog(false);
146
146
-
setNotif({ show: true, icon: "lucide--file-check", text: "Record edited" });
150
150
+
const id = addNotification({
151
151
+
message: "Record edited",
152
152
+
type: "success",
153
153
+
});
154
154
+
setTimeout(() => removeNotification(id), 3000);
147
155
props.refetch();
148
156
} catch (err: any) {
149
157
setNotice(err.message);
+5
-5
src/components/json.tsx
···
1
1
import { isCid, isDid, isNsid, Nsid } from "@atcute/lexicons/syntax";
2
2
import { A, useNavigate, useParams } from "@solidjs/router";
3
3
import { createEffect, createSignal, ErrorBoundary, For, on, Show } from "solid-js";
4
4
-
import { setNotif } from "../layout";
5
4
import { resolveLexiconAuthority } from "../utils/api";
6
5
import { ATURI_RE } from "../utils/types/at-uri";
7
6
import { hideMedia } from "../views/settings";
8
7
import { pds } from "./navbar";
8
8
+
import { addNotification, removeNotification } from "./notification";
9
9
import VideoPlayer from "./video-player";
10
10
11
11
interface AtBlob {
···
43
43
navigate(`/at://${authority}/com.atproto.lexicon.schema/${nsid}${hash}`);
44
44
} catch (err) {
45
45
console.error("Failed to resolve lexicon authority:", err);
46
46
-
setNotif({
47
47
-
show: true,
48
48
-
icon: "lucide--circle-alert",
49
49
-
text: "Could not resolve schema",
46
46
+
const id = addNotification({
47
47
+
message: "Could not resolve schema",
48
48
+
type: "error",
50
49
});
50
50
+
setTimeout(() => removeNotification(id), 5000);
51
51
}
52
52
};
53
53
+80
src/components/notification.tsx
···
1
1
+
import { createSignal, For, Show } from "solid-js";
2
2
+
3
3
+
export type Notification = {
4
4
+
id: string;
5
5
+
message: string;
6
6
+
progress?: number;
7
7
+
total?: number;
8
8
+
type?: "info" | "success" | "error";
9
9
+
};
10
10
+
11
11
+
const [notifications, setNotifications] = createSignal<Notification[]>([]);
12
12
+
13
13
+
export const addNotification = (notification: Omit<Notification, "id">) => {
14
14
+
const id = `notification-${Date.now()}-${Math.random()}`;
15
15
+
setNotifications([...notifications(), { ...notification, id }]);
16
16
+
return id;
17
17
+
};
18
18
+
19
19
+
export const updateNotification = (id: string, updates: Partial<Notification>) => {
20
20
+
setNotifications(notifications().map((n) => (n.id === id ? { ...n, ...updates } : n)));
21
21
+
};
22
22
+
23
23
+
export const removeNotification = (id: string) => {
24
24
+
setNotifications(notifications().filter((n) => n.id !== id));
25
25
+
};
26
26
+
27
27
+
export const NotificationContainer = () => {
28
28
+
return (
29
29
+
<div class="pointer-events-none fixed bottom-4 left-4 z-50 flex flex-col gap-2">
30
30
+
<For each={notifications()}>
31
31
+
{(notification) => (
32
32
+
<div
33
33
+
class="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto flex min-w-64 flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 shadow-md dark:border-neutral-700"
34
34
+
classList={{
35
35
+
"border-blue-500 dark:border-blue-400": notification.type === "info",
36
36
+
"border-green-500 dark:border-green-400": notification.type === "success",
37
37
+
"border-red-500 dark:border-red-400": notification.type === "error",
38
38
+
}}
39
39
+
onClick={() => removeNotification(notification.id)}
40
40
+
>
41
41
+
<div class="flex items-center gap-2 text-sm">
42
42
+
<Show when={notification.progress !== undefined}>
43
43
+
<span class="iconify lucide--download" />
44
44
+
</Show>
45
45
+
<Show when={notification.type === "success"}>
46
46
+
<span class="iconify lucide--check-circle text-green-600 dark:text-green-400" />
47
47
+
</Show>
48
48
+
<Show when={notification.type === "error"}>
49
49
+
<span class="iconify lucide--x-circle text-red-500 dark:text-red-400" />
50
50
+
</Show>
51
51
+
<span>{notification.message}</span>
52
52
+
</div>
53
53
+
<Show when={notification.progress !== undefined}>
54
54
+
<div class="flex flex-col gap-1">
55
55
+
<Show
56
56
+
when={notification.total !== undefined && notification.total > 0}
57
57
+
fallback={
58
58
+
<div class="text-xs text-neutral-600 dark:text-neutral-400">
59
59
+
{notification.progress} MB
60
60
+
</div>
61
61
+
}
62
62
+
>
63
63
+
<div class="h-2 w-full overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700">
64
64
+
<div
65
65
+
class="h-full rounded-full bg-blue-500 transition-all dark:bg-blue-400"
66
66
+
style={{ width: `${notification.progress}%` }}
67
67
+
/>
68
68
+
</div>
69
69
+
<div class="text-xs text-neutral-600 dark:text-neutral-400">
70
70
+
{notification.progress}%
71
71
+
</div>
72
72
+
</Show>
73
73
+
</div>
74
74
+
</Show>
75
75
+
</div>
76
76
+
)}
77
77
+
</For>
78
78
+
</div>
79
79
+
);
80
80
+
};
+3
-24
src/layout.tsx
···
1
1
import { Handle } from "@atcute/lexicons";
2
2
import { Meta, MetaProvider } from "@solidjs/meta";
3
3
import { A, RouteSectionProps, useLocation, useNavigate } from "@solidjs/router";
4
4
-
import { createEffect, createSignal, ErrorBoundary, onMount, Show, Suspense } from "solid-js";
4
4
+
import { createEffect, ErrorBoundary, onMount, Show, Suspense } from "solid-js";
5
5
import { AccountManager } from "./components/account.jsx";
6
6
import { RecordEditor } from "./components/create.jsx";
7
7
import { DropdownMenu, MenuProvider, NavMenu } from "./components/dropdown.jsx";
8
8
import { agent } from "./components/login.jsx";
9
9
import { NavBar } from "./components/navbar.jsx";
10
10
+
import { NotificationContainer } from "./components/notification.jsx";
10
11
import { Search, SearchButton, showSearch } from "./components/search.jsx";
11
12
import { themeEvent, ThemeSelection } from "./components/theme.jsx";
12
13
import { resolveHandle } from "./utils/api.js";
13
14
14
15
export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1;
15
15
-
16
16
-
export const [notif, setNotif] = createSignal<{
17
17
-
show: boolean;
18
18
-
icon?: string;
19
19
-
text?: string;
20
20
-
}>({ show: false });
21
16
22
17
const headers: Record<string, string> = {
23
18
"did:plc:ia76kvnndjutgedggx2ibrem": "bunny.jpg",
···
33
28
const Layout = (props: RouteSectionProps<unknown>) => {
34
29
const location = useLocation();
35
30
const navigate = useNavigate();
36
36
-
let timeout: number;
37
31
38
32
if (location.search.includes("hrt=true")) localStorage.setItem("hrt", "true");
39
33
else if (location.search.includes("hrt=false")) localStorage.setItem("hrt", "false");
···
44
38
if (props.params.repo && !props.params.repo.startsWith("did:")) {
45
39
const did = await resolveHandle(props.params.repo as Handle);
46
40
navigate(location.pathname.replace(props.params.repo, did));
47
47
-
}
48
48
-
});
49
49
-
50
50
-
createEffect(() => {
51
51
-
if (notif().show) {
52
52
-
clearTimeout(timeout);
53
53
-
timeout = setTimeout(() => setNotif({ show: false }), 3000);
54
41
}
55
42
});
56
43
···
197
184
</ErrorBoundary>
198
185
</Show>
199
186
</div>
200
200
-
<Show when={notif().show}>
201
201
-
<button
202
202
-
class="dark:shadow-dark-700 dark:bg-dark-100 fixed bottom-10 z-50 flex items-center rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md dark:border-neutral-700"
203
203
-
onClick={() => setNotif({ show: false })}
204
204
-
>
205
205
-
<span class={`iconify ${notif().icon} mr-1`}></span>
206
206
-
{notif().text}
207
207
-
</button>
208
208
-
</Show>
187
187
+
<NotificationContainer />
209
188
</div>
210
189
);
211
190
};
+6
-2
src/utils/copy.ts
···
1
1
-
import { setNotif } from "../layout";
1
1
+
import { addNotification, removeNotification } from "../components/notification";
2
2
3
3
export const addToClipboard = (text: string) => {
4
4
navigator.clipboard.writeText(text);
5
5
-
setNotif({ show: true, icon: "lucide--clipboard-check", text: "Copied to clipboard" });
5
5
+
const id = addNotification({
6
6
+
message: "Copied to clipboard",
7
7
+
type: "success",
8
8
+
});
9
9
+
setTimeout(() => removeNotification(id), 3000);
6
10
};
+5
-5
src/views/collection.tsx
···
9
9
import { JSONType, JSONValue } from "../components/json.jsx";
10
10
import { agent } from "../components/login.jsx";
11
11
import { Modal } from "../components/modal.jsx";
12
12
+
import { addNotification, removeNotification } from "../components/notification.jsx";
12
13
import { StickyOverlay } from "../components/sticky.jsx";
13
14
import { TextInput } from "../components/text-input.jsx";
14
15
import Tooltip from "../components/tooltip.jsx";
15
15
-
import { setNotif } from "../layout.jsx";
16
16
import { resolvePDS } from "../utils/api.js";
17
17
import { localDateFromTimestamp } from "../utils/date.js";
18
18
···
149
149
},
150
150
});
151
151
}
152
152
-
setNotif({
153
153
-
show: true,
154
154
-
icon: "lucide--trash-2",
155
155
-
text: `${recsToDel.length} records ${recreate() ? "recreated" : "deleted"}`,
152
152
+
const id = addNotification({
153
153
+
message: `${recsToDel.length} records ${recreate() ? "recreated" : "deleted"}`,
154
154
+
type: "success",
156
155
});
156
156
+
setTimeout(() => removeNotification(id), 3000);
157
157
setBatchDelete(false);
158
158
setRecords([]);
159
159
setCursor(undefined);
+6
-2
src/views/record.tsx
···
15
15
import { agent } from "../components/login.jsx";
16
16
import { Modal } from "../components/modal.jsx";
17
17
import { pds } from "../components/navbar.jsx";
18
18
+
import { addNotification, removeNotification } from "../components/notification.jsx";
18
19
import Tooltip from "../components/tooltip.jsx";
19
19
-
import { setNotif } from "../layout.jsx";
20
20
import { resolveLexiconAuthority, resolveLexiconSchema, resolvePDS } from "../utils/api.js";
21
21
import { AtUri, uriTemplates } from "../utils/templates.js";
22
22
import { lexicons } from "../utils/types/lexicons.js";
···
134
134
rkey: params.rkey,
135
135
},
136
136
});
137
137
-
setNotif({ show: true, icon: "lucide--trash-2", text: "Record deleted" });
137
137
+
const id = addNotification({
138
138
+
message: "Record deleted",
139
139
+
type: "success",
140
140
+
});
141
141
+
setTimeout(() => removeNotification(id), 3000);
138
142
navigate(`/at://${params.repo}/${params.collection}`);
139
143
};
140
144
+65
-1
src/views/repo.tsx
···
22
22
NavMenu,
23
23
} from "../components/dropdown.jsx";
24
24
import { setPDS } from "../components/navbar.jsx";
25
25
+
import {
26
26
+
addNotification,
27
27
+
removeNotification,
28
28
+
updateNotification,
29
29
+
} from "../components/notification.jsx";
25
30
import { TextInput } from "../components/text-input.jsx";
26
31
import Tooltip from "../components/tooltip.jsx";
27
32
import {
···
155
160
};
156
161
157
162
const downloadRepo = async () => {
163
163
+
let notificationId: string | null = null;
164
164
+
158
165
try {
159
166
setDownloading(true);
167
167
+
notificationId = addNotification({
168
168
+
message: "Downloading repository...",
169
169
+
progress: 0,
170
170
+
total: 0,
171
171
+
type: "info",
172
172
+
});
173
173
+
160
174
const response = await fetch(`${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`);
161
175
if (!response.ok) {
162
176
throw new Error(`HTTP error status: ${response.status}`);
163
177
}
164
178
165
165
-
const blob = await response.blob();
179
179
+
const contentLength = response.headers.get("content-length");
180
180
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
181
181
+
let loaded = 0;
182
182
+
183
183
+
const reader = response.body?.getReader();
184
184
+
const chunks: Uint8Array[] = [];
185
185
+
186
186
+
if (reader) {
187
187
+
while (true) {
188
188
+
const { done, value } = await reader.read();
189
189
+
if (done) break;
190
190
+
191
191
+
chunks.push(value);
192
192
+
loaded += value.length;
193
193
+
194
194
+
if (total > 0) {
195
195
+
const progress = Math.round((loaded / total) * 100);
196
196
+
updateNotification(notificationId, {
197
197
+
progress,
198
198
+
total,
199
199
+
});
200
200
+
} else {
201
201
+
const progressMB = Math.round((loaded / (1024 * 1024)) * 10) / 10;
202
202
+
updateNotification(notificationId, {
203
203
+
progress: progressMB,
204
204
+
total: 0,
205
205
+
});
206
206
+
}
207
207
+
}
208
208
+
}
209
209
+
210
210
+
const blob = new Blob(chunks);
166
211
const url = window.URL.createObjectURL(blob);
167
212
const a = document.createElement("a");
168
213
a.href = url;
···
172
217
173
218
window.URL.revokeObjectURL(url);
174
219
document.body.removeChild(a);
220
220
+
221
221
+
updateNotification(notificationId, {
222
222
+
message: "Repository downloaded successfully",
223
223
+
type: "success",
224
224
+
progress: undefined,
225
225
+
});
226
226
+
setTimeout(() => {
227
227
+
if (notificationId) removeNotification(notificationId);
228
228
+
}, 3000);
175
229
} catch (error) {
176
230
console.error("Download failed:", error);
231
231
+
if (notificationId) {
232
232
+
updateNotification(notificationId, {
233
233
+
message: "Download failed",
234
234
+
type: "error",
235
235
+
progress: undefined,
236
236
+
});
237
237
+
setTimeout(() => {
238
238
+
if (notificationId) removeNotification(notificationId);
239
239
+
}, 5000);
240
240
+
}
177
241
}
178
242
setDownloading(false);
179
243
};