+1
.prettierrc
+1
.prettierrc
+2
-6
index.html
+2
-6
index.html
···
7
7
<meta property="og:title" content="PDSls" />
8
8
<meta property="og:type" content="website" />
9
9
<meta property="og:url" content="https://pdsls.dev" />
10
-
<meta
11
-
property="og:description"
12
-
content="Browse and manage atproto repositories"
13
-
/>
10
+
<meta property="og:description" content="Browse and manage atproto repositories" />
14
11
<title>PDSls</title>
15
12
<script>
16
13
if (
17
14
localStorage.theme === "dark" ||
18
-
(!("theme" in localStorage) &&
19
-
window.matchMedia("(prefers-color-scheme: dark)").matches)
15
+
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
20
16
)
21
17
document.documentElement.classList.add("dark");
22
18
else document.documentElement.classList.remove("dark");
+3
-15
src/components/account.tsx
+3
-15
src/components/account.tsx
···
1
-
import {
2
-
createSignal,
3
-
onMount,
4
-
Show,
5
-
onCleanup,
6
-
createEffect,
7
-
For,
8
-
} from "solid-js";
1
+
import { createSignal, onMount, Show, onCleanup, createEffect, For } from "solid-js";
9
2
import Tooltip from "./tooltip.jsx";
10
-
import {
11
-
deleteStoredSession,
12
-
getSession,
13
-
OAuthUserAgent,
14
-
} from "@atcute/oauth-browser-client";
3
+
import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
15
4
import { agent, Login, setLoginState } from "./login.jsx";
16
5
import { At } from "@atcute/client/lexicons";
17
6
···
89
78
classList={{
90
79
"basis-full text-left font-mono max-w-[32ch] text-sm truncate group-hover/select:bg-slate-300 dark:group-hover/select:bg-neutral-700":
91
80
true,
92
-
"text-green-500 dark:text-green-400":
93
-
session === agent?.sub,
81
+
"text-green-500 dark:text-green-400": session === agent?.sub,
94
82
}}
95
83
onclick={() => resumeSession(session)}
96
84
>
+7
-32
src/components/backlinks.tsx
+7
-32
src/components/backlinks.tsx
···
45
45
{({ collection, path, matchesFilter, counts }) => (
46
46
<div class="mt-2 font-mono text-sm sm:text-base">
47
47
<p classList={{ "text-stone-400": matchesFilter }}>
48
-
<span title="Collection containing linking records">
49
-
{collection}
50
-
</span>
48
+
<span title="Collection containing linking records">{collection}</span>
51
49
<span class="text-cyan-500">@</span>
52
-
<span title="Record path where the link is found">
53
-
{path.slice(1)}
54
-
</span>
55
-
:
50
+
<span title="Record path where the link is found">{path.slice(1)}</span>:
56
51
</p>
57
52
<div class="pl-2.5 font-sans">
58
53
<p>
···
78
73
href="#"
79
74
title="Show linking DIDs"
80
75
onclick={() =>
81
-
(
82
-
show()?.collection === collection &&
83
-
show()?.path === path &&
84
-
show()?.showDids
85
-
) ?
76
+
show()?.collection === collection && show()?.path === path && show()?.showDids ?
86
77
setShow(null)
87
78
: setShow({ collection, path, showDids: true })
88
79
}
···
91
82
{counts.distinct_dids < 2 ? "" : "s"}
92
83
</a>
93
84
</p>
94
-
<Show
95
-
when={
96
-
show()?.collection === collection && show()?.path === path
97
-
}
98
-
>
85
+
<Show when={show()?.collection === collection && show()?.path === path}>
99
86
<Show when={show()?.showDids}>
100
87
{/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */}
101
88
<p class="w-full font-semibold text-stone-600 dark:text-stone-400">
102
89
Distinct identities
103
90
</p>
104
-
<BacklinkItems
105
-
target={target}
106
-
collection={collection}
107
-
path={path}
108
-
dids={true}
109
-
/>
91
+
<BacklinkItems target={target} collection={collection} path={path} dids={true} />
110
92
</Show>
111
93
<Show when={!show()?.showDids}>
112
-
<p class="w-full font-semibold text-stone-600 dark:text-stone-400">
113
-
Records
114
-
</p>
115
-
<BacklinkItems
116
-
target={target}
117
-
collection={collection}
118
-
path={path}
119
-
dids={false}
120
-
/>
94
+
<p class="w-full font-semibold text-stone-600 dark:text-stone-400">Records</p>
95
+
<BacklinkItems target={target} collection={collection} path={path} dids={false} />
121
96
</Show>
122
97
</Show>
123
98
</div>
+1
-3
src/components/create.tsx
+1
-3
src/components/create.tsx
···
137
137
</div>
138
138
<Editor theme={theme().color} model={model!} />
139
139
<div class="flex flex-col gap-x-2">
140
-
<div class="text-red-500 dark:text-red-400">
141
-
{createNotice()}
142
-
</div>
140
+
<div class="text-red-500 dark:text-red-400">{createNotice()}</div>
143
141
<div class="flex items-center justify-end gap-2">
144
142
<button
145
143
onclick={() => setOpenCreate(false)}
+5
-29
src/components/json.tsx
+5
-29
src/components/json.tsx
···
11
11
}
12
12
13
13
export const syntaxHighlight = (json: string) => {
14
-
json = json
15
-
.replace(/&/g, "&")
16
-
.replace(/</g, "<")
17
-
.replace(/>/g, ">");
14
+
json = json.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
18
15
19
16
return json.replace(
20
17
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
···
70
67
["http:", "https:", "web+at:"].includes(new URL(part).protocol) &&
71
68
part.split("\n").length === 1
72
69
) ?
73
-
<a
74
-
class="underline"
75
-
href={part}
76
-
target="_blank"
77
-
rel="noopener noreferer"
78
-
>
70
+
<a class="underline" href={part} target="_blank" rel="noopener noreferer">
79
71
{part}
80
72
</a>
81
73
: part}
···
91
83
};
92
84
93
85
const JSONBoolean = ({ data }: { data: boolean }) => {
94
-
return (
95
-
<span class="text-[#f57d26] dark:text-orange-300">
96
-
{data ? "true" : "false"}
97
-
</span>
98
-
);
86
+
return <span class="text-[#f57d26] dark:text-orange-300">{data ? "true" : "false"}</span>;
99
87
};
100
88
101
89
const JSONNull = () => {
102
90
return <span class="text-neutral-400 dark:text-neutral-500">null</span>;
103
91
};
104
92
105
-
const JSONObject = ({
106
-
data,
107
-
repo,
108
-
}: {
109
-
data: { [x: string]: JSONType };
110
-
repo: string;
111
-
}) => {
93
+
const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => {
112
94
const [clip, setClip] = createSignal(false);
113
95
const rawObj = (
114
96
<For each={Object.entries(data)}>
···
214
196
return <JSONObject data={data} repo={repo} />;
215
197
};
216
198
217
-
export type JSONType =
218
-
| string
219
-
| number
220
-
| boolean
221
-
| null
222
-
| { [x: string]: JSONType }
223
-
| JSONType[];
199
+
export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
+3
-9
src/components/search.tsx
+3
-9
src/components/search.tsx
···
13
13
!input.startsWith("https://main.bsky.dev/") &&
14
14
(input.startsWith("https://") || input.startsWith("http://"))
15
15
)
16
-
throw redirect(
17
-
`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`,
18
-
);
16
+
throw redirect(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`);
19
17
20
18
const uri = input
21
19
.replace("at://", "")
···
30
28
} catch {
31
29
throw redirect(`/${actor}`);
32
30
}
33
-
throw redirect(
34
-
`/at://${did}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`,
35
-
);
31
+
throw redirect(`/at://${did}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`);
36
32
});
37
33
38
34
const Search = () => {
···
77
73
</Show>
78
74
</div>
79
75
</form>
80
-
<Show when={submission.error}>
81
-
{(err) => <div class="mt-3">{err().message}</div>}
82
-
</Show>
76
+
<Show when={submission.error}>{(err) => <div class="mt-3">{err().message}</div>}</Show>
83
77
</>
84
78
);
85
79
};
+10
-33
src/components/settings.tsx
+10
-33
src/components/settings.tsx
···
4
4
const getInitialTheme = () => {
5
5
const isDarkMode =
6
6
localStorage.theme === "dark" ||
7
-
(!("theme" in localStorage) &&
8
-
window.matchMedia("(prefers-color-scheme: dark)").matches);
7
+
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches);
9
8
return {
10
9
color: isDarkMode ? "dark" : "light",
11
10
system: !("theme" in localStorage),
···
13
12
};
14
13
15
14
export const [theme, setTheme] = createSignal(getInitialTheme());
16
-
const [backlinksEnabled, setBacklinksEnabled] = createSignal(
17
-
localStorage.backlinks === "true",
18
-
);
15
+
const [backlinksEnabled, setBacklinksEnabled] = createSignal(localStorage.backlinks === "true");
19
16
20
17
const Settings = () => {
21
18
const [modal, setModal] = createSignal<HTMLDialogElement>();
···
41
38
onMount(() => {
42
39
window.addEventListener("keydown", keyEvent);
43
40
window.addEventListener("click", clickEvent);
44
-
window
45
-
.matchMedia("(prefers-color-scheme: dark)")
46
-
.addEventListener("change", themeEvent);
41
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent);
47
42
});
48
43
49
44
onCleanup(() => {
50
45
window.removeEventListener("keydown", keyEvent);
51
46
window.removeEventListener("click", clickEvent);
52
-
window
53
-
.matchMedia("(prefers-color-scheme: dark)")
54
-
.removeEventListener("change", themeEvent);
47
+
window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", themeEvent);
55
48
});
56
49
57
50
createEffect(() => {
···
61
54
62
55
const updateTheme = (newTheme: { color: string; system: boolean }) => {
63
56
setTheme(newTheme);
64
-
document.documentElement.classList.toggle(
65
-
"dark",
66
-
newTheme.color === "dark",
67
-
);
57
+
document.documentElement.classList.toggle("dark", newTheme.color === "dark");
68
58
if (newTheme.system) {
69
59
localStorage.removeItem("theme");
70
60
} else {
···
80
70
class="backdrop-brightness-60 fixed left-0 top-0 z-20 flex h-screen w-screen items-center justify-center bg-transparent"
81
71
>
82
72
<div class="dark:bg-dark-400 top-10% absolute rounded-md border border-slate-900 bg-slate-100 p-4 text-slate-900 dark:border-slate-100 dark:text-slate-100">
83
-
<h3 class="mb-2 border-b border-neutral-500 pb-2 text-xl font-bold">
84
-
Settings
85
-
</h3>
73
+
<h3 class="mb-2 border-b border-neutral-500 pb-2 text-xl font-bold">Settings</h3>
86
74
<h4 class="mb-1 font-semibold">Theme</h4>
87
75
<div class="w-xs flex divide-x divide-neutral-500 overflow-hidden rounded-lg border border-neutral-500">
88
76
<button
···
94
82
onclick={() =>
95
83
updateTheme({
96
84
color:
97
-
(
98
-
window.matchMedia("(prefers-color-scheme: dark)")
99
-
.matches
100
-
) ?
101
-
"dark"
102
-
: "light",
85
+
window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
103
86
system: true,
104
87
})
105
88
}
···
111
94
"basis-1/3 p-2": true,
112
95
"hover:bg-slate-200 dark:hover:bg-dark-200":
113
96
theme().color !== "light" || theme().system,
114
-
"bg-neutral-500 text-slate-100":
115
-
theme().color === "light" && !theme().system,
97
+
"bg-neutral-500 text-slate-100": theme().color === "light" && !theme().system,
116
98
}}
117
99
onclick={() => updateTheme({ color: "light", system: false })}
118
100
>
···
161
143
name="constellation"
162
144
type="text"
163
145
spellcheck={false}
164
-
value={
165
-
localStorage.constellationHost ||
166
-
"https://constellation.microcosm.blue"
167
-
}
146
+
value={localStorage.constellationHost || "https://constellation.microcosm.blue"}
168
147
disabled={!backlinksEnabled()}
169
148
class="dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300 disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 dark:disabled:border-gray-700 dark:disabled:bg-gray-800/20"
170
-
onInput={(e) =>
171
-
(localStorage.constellationHost = e.currentTarget.value)
172
-
}
149
+
onInput={(e) => (localStorage.constellationHost = e.currentTarget.value)}
173
150
/>
174
151
</div>
175
152
</div>
+5
-20
src/layout.tsx
+5
-20
src/layout.tsx
···
28
28
window.location.href = location.pathname.replace(params.repo, did);
29
29
}
30
30
await retrieveSession();
31
-
if (loginState() && location.pathname === "/")
32
-
window.location.href = `/at://${agent.sub}`;
31
+
if (loginState() && location.pathname === "/") window.location.href = `/at://${agent.sub}`;
33
32
});
34
33
35
34
return (
36
-
<div
37
-
id="main"
38
-
class="m-5 flex flex-col items-center text-slate-900 dark:text-slate-100"
39
-
>
35
+
<div id="main" class="m-5 flex flex-col items-center text-slate-900 dark:text-slate-100">
40
36
<Show when={location.pathname !== "/"}>
41
37
<MetaProvider>
42
38
<Meta name="robots" content="noindex, nofollow" />
···
69
65
</div>
70
66
</div>
71
67
<div class="mb-5 flex max-w-full flex-col items-center text-pretty md:max-w-screen-md">
72
-
<Show
73
-
when={
74
-
location.pathname !== "/jetstream" &&
75
-
location.pathname !== "/firehose"
76
-
}
77
-
>
68
+
<Show when={location.pathname !== "/jetstream" && location.pathname !== "/firehose"}>
78
69
<Search />
79
70
</Show>
80
71
<Show when={params.pds}>
···
82
73
</Show>
83
74
<Show keyed when={location.pathname}>
84
75
<ErrorBoundary
85
-
fallback={(err) => (
86
-
<div class="mt-3 break-words">Error: {err.message}</div>
87
-
)}
76
+
fallback={(err) => <div class="mt-3 break-words">Error: {err.message}</div>}
88
77
>
89
-
<Suspense
90
-
fallback={
91
-
<div class="i-line-md-loading-twotone-loop mt-3 text-xl" />
92
-
}
93
-
>
78
+
<Suspense fallback={<div class="i-line-md-loading-twotone-loop mt-3 text-xl" />}>
94
79
{props.children}
95
80
</Suspense>
96
81
</ErrorBoundary>
+2
-2
src/styles/index.css
+2
-2
src/styles/index.css
···
4
4
samp,
5
5
pre {
6
6
font-family:
7
-
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
8
-
"Liberation Mono", "Courier New", monospace;
7
+
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
8
+
"Courier New", monospace;
9
9
}
10
10
11
11
.string {
+11
-7
src/styles/tailwind.css
+11
-7
src/styles/tailwind.css
···
15
15
16
16
::before,
17
17
::after {
18
-
--un-content: '';
18
+
--un-content: "";
19
19
}
20
20
21
21
/*
···
34
34
-webkit-text-size-adjust: 100%; /* 2 */
35
35
-moz-tab-size: 4; /* 3 */
36
36
tab-size: 4; /* 3 */
37
-
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
37
+
font-family:
38
+
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
39
+
"Noto Color Emoji"; /* 4 */
38
40
font-feature-settings: normal; /* 5 */
39
41
font-variation-settings: normal; /* 6 */
40
42
-webkit-tap-highlight-color: transparent; /* 7 */
···
113
115
kbd,
114
116
samp,
115
117
pre {
116
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
118
+
font-family:
119
+
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
120
+
monospace; /* 1 */
117
121
font-feature-settings: normal; /* 2 */
118
122
font-variation-settings: normal; /* 3 */
119
123
font-size: 1em; /* 4 */
···
196
200
*/
197
201
198
202
button,
199
-
[type='button'],
200
-
[type='reset'],
201
-
[type='submit'] {
203
+
[type="button"],
204
+
[type="reset"],
205
+
[type="submit"] {
202
206
-webkit-appearance: button; /* 1 */
203
207
background-color: transparent; /* 2 */
204
208
background-image: none; /* 2 */
···
242
246
2. Correct the outline style in Safari.
243
247
*/
244
248
245
-
[type='search'] {
249
+
[type="search"] {
246
250
-webkit-appearance: textfield; /* 1 */
247
251
outline-offset: -2px; /* 2 */
248
252
}
+5
-18
src/utils/api.ts
+5
-18
src/utils/api.ts
···
90
90
cursor?: string,
91
91
limit?: number,
92
92
) => {
93
-
const url = new URL(
94
-
localStorage.constellationHost || "https://constellation.microcosm.blue",
95
-
);
93
+
const url = new URL(localStorage.constellationHost || "https://constellation.microcosm.blue");
96
94
url.pathname = endpoint;
97
95
url.searchParams.set("target", target);
98
96
if (collection) {
99
-
if (!path)
100
-
throw new Error("collection and path must either both be set or neither");
97
+
if (!path) throw new Error("collection and path must either both be set or neither");
101
98
url.searchParams.set("collection", collection);
102
99
url.searchParams.set("path", path);
103
100
} else {
104
-
if (path)
105
-
throw new Error("collection and path must either both be set or neither");
101
+
if (path) throw new Error("collection and path must either both be set or neither");
106
102
}
107
103
if (limit) url.searchParams.set("limit", `${limit}`);
108
104
if (cursor) url.searchParams.set("cursor", `${cursor}`);
···
111
107
return await res.json();
112
108
};
113
109
114
-
const getAllBacklinks = (target: string) =>
115
-
getConstellation("/links/all", target);
110
+
const getAllBacklinks = (target: string) => getConstellation("/links/all", target);
116
111
117
112
const getRecordBacklinks = (
118
113
target: string,
···
128
123
path: string,
129
124
cursor?: string,
130
125
limit?: number,
131
-
) =>
132
-
getConstellation(
133
-
"/links/distinct-dids",
134
-
target,
135
-
collection,
136
-
path,
137
-
cursor,
138
-
limit || 100,
139
-
);
126
+
) => getConstellation("/links/distinct-dids", target, collection, path, cursor, limit || 100);
140
127
141
128
export {
142
129
didDocCache,
+1
-3
src/utils/firehose.ts
+1
-3
src/utils/firehose.ts
···
196
196
return this;
197
197
}
198
198
199
-
private parseMessage(
200
-
data: ArrayBuffer,
201
-
): ParsedCommit | { $type: string; seq?: number } {
199
+
private parseMessage(data: ArrayBuffer): ParsedCommit | { $type: string; seq?: number } {
202
200
const [header, remainder] = decodeFirst(new Uint8Array(data));
203
201
const [body, remainder2] = decodeFirst(remainder);
204
202
if (remainder2.length > 0) {
+2
-8
src/utils/types/at-uri.ts
+2
-8
src/utils/types/at-uri.ts
···
16
16
17
17
const isDid = (input: unknown): input is Did => {
18
18
return (
19
-
typeof input === "string" &&
20
-
input.length >= 7 &&
21
-
input.length <= 2048 &&
22
-
DID_RE.test(input)
19
+
typeof input === "string" && input.length >= 7 && input.length <= 2048 && DID_RE.test(input)
23
20
);
24
21
};
25
22
26
23
const isNsid = (input: unknown): input is Nsid => {
27
24
return (
28
-
typeof input === "string" &&
29
-
input.length >= 5 &&
30
-
input.length <= 317 &&
31
-
NSID_RE.test(input)
25
+
typeof input === "string" && input.length >= 5 && input.length <= 317 && NSID_RE.test(input)
32
26
);
33
27
};
34
28
+4
-15
src/utils/verify.ts
+4
-15
src/utils/verify.ts
···
4
4
import * as CAR from "@atcute/car";
5
5
import * as CBOR from "@atcute/cbor";
6
6
import * as CID from "@atcute/cid";
7
-
import {
8
-
type FoundPublicKey,
9
-
getPublicKeyFromDidController,
10
-
verifySig,
11
-
} from "@atcute/crypto";
12
-
import {
13
-
type DidDocument,
14
-
getAtprotoVerificationMaterial,
15
-
} from "@atcute/identity";
7
+
import { type FoundPublicKey, getPublicKeyFromDidController, verifySig } from "@atcute/crypto";
8
+
import { type DidDocument, getAtprotoVerificationMaterial } from "@atcute/identity";
16
9
import { toSha256 } from "@atcute/uint8array";
17
10
18
11
import { type AddressedAtUri, parseAddressedAtUri } from "./types/at-uri";
···
34
27
didDoc: DidDocument;
35
28
}
36
29
37
-
export const verifyRecord = async (
38
-
opts: VerifyOptions,
39
-
): Promise<VerifyResult> => {
30
+
export const verifyRecord = async (opts: VerifyOptions): Promise<VerifyResult> => {
40
31
const errors: VerifyError[] = [];
41
32
42
33
// verify cid can be parsed
···
125
116
const cidString = CID.toString(entry.cid);
126
117
127
118
// Verify that `bytes` matches its associated CID
128
-
const expectedCid = CID.toString(
129
-
await CID.create(entry.cid.codec as 85 | 113, entry.bytes),
130
-
);
119
+
const expectedCid = CID.toString(await CID.create(entry.cid.codec as 85 | 113, entry.bytes));
131
120
if (cidString !== expectedCid) {
132
121
errors.push({
133
122
message: `cid does not match bytes`,
+1
-2
src/views/blob.tsx
+1
-2
src/views/blob.tsx
···
25
25
const fetchBlobs = async (): Promise<string[]> => {
26
26
if (!did.startsWith("did:")) did = await resolveHandle(params.repo);
27
27
if (!pds) pds = await resolvePDS(did);
28
-
if (!rpc)
29
-
rpc = new XRPC({ handler: new CredentialManager({ service: pds }) });
28
+
if (!rpc) rpc = new XRPC({ handler: new CredentialManager({ service: pds }) });
30
29
const res = await listBlobs(did, cursor());
31
30
setCursor(res.data.cids.length < 1000 ? undefined : res.data.cursor);
32
31
setBlobs(blobs()?.concat(res.data.cids) ?? res.data.cids);
+7
-18
src/views/collection.tsx
+7
-18
src/views/collection.tsx
···
50
50
onmouseleave={() => setHoverRk(undefined)}
51
51
>
52
52
<span class="text-lightblue-500">{props.record.rkey}</span>
53
-
<Show
54
-
when={props.record.timestamp && props.record.timestamp <= Date.now()}
55
-
>
53
+
<Show when={props.record.timestamp && props.record.timestamp <= Date.now()}>
56
54
<span class="ml-2 text-xs text-neutral-500 dark:text-neutral-400">
57
55
{localDateFromTimestamp(props.record.timestamp!)}
58
56
</span>
···
121
119
const fetchRecords = async () => {
122
120
if (!did.startsWith("did:")) did = await resolveHandle(params.repo);
123
121
if (!pds) pds = await resolvePDS(did);
124
-
if (!rpc)
125
-
rpc = new XRPC({ handler: new CredentialManager({ service: pds }) });
122
+
if (!rpc) rpc = new XRPC({ handler: new CredentialManager({ service: pds }) });
126
123
const res = await listRecords(did, params.collection, cursor());
127
124
setCursor(res.data.records.length < 100 ? undefined : res.data.cursor);
128
125
const tmpRecords: AtprotoRecord[] = [];
···
131
128
tmpRecords.push({
132
129
rkey: rkey,
133
130
record: record,
134
-
timestamp:
135
-
TID.validate(rkey) ? TID.parse(rkey).timestamp / 1000 : undefined,
131
+
timestamp: TID.validate(rkey) ? TID.parse(rkey).timestamp / 1000 : undefined,
136
132
toDelete: false,
137
133
});
138
134
});
···
183
179
setRecords(
184
180
records
185
181
.map((record, index) =>
186
-
JSON.stringify(record.record.value).includes(filter() ?? "") ?
187
-
index
188
-
: undefined,
182
+
JSON.stringify(record.record.value).includes(filter() ?? "") ? index : undefined,
189
183
)
190
184
.filter((i) => i !== undefined),
191
185
"toDelete",
···
271
265
>
272
266
<div class="dark:bg-dark-400 rounded-md border border-neutral-500 bg-slate-100 p-3 text-slate-900 dark:text-slate-100">
273
267
<h3 class="text-lg font-bold">
274
-
Delete {records.filter((rec) => rec.toDelete).length}{" "}
275
-
records?
268
+
Delete {records.filter((rec) => rec.toDelete).length} records?
276
269
</h3>
277
270
<div class="mt-2 inline-flex gap-2">
278
271
<button
···
334
327
<div class="flex flex-col font-mono">
335
328
<For
336
329
each={records.filter((rec) =>
337
-
filter() ?
338
-
JSON.stringify(rec.record.value).includes(filter()!)
339
-
: true,
330
+
filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true,
340
331
)}
341
332
>
342
333
{(record, index) => (
···
349
340
<input
350
341
type="checkbox"
351
342
checked={record.toDelete}
352
-
onchange={(e) =>
353
-
setRecords(index(), "toDelete", e.currentTarget.checked)
354
-
}
343
+
onchange={(e) => setRecords(index(), "toDelete", e.currentTarget.checked)}
355
344
/>
356
345
<RecordLink record={record} index={index()} />
357
346
</label>
+2
-9
src/views/home.tsx
+2
-9
src/views/home.tsx
···
6
6
<div class="mb-2">
7
7
<p>
8
8
Browse the public data on{" "}
9
-
<a
10
-
class="text-lightblue-500 hover:underline"
11
-
href="https://atproto.com"
12
-
target="_blank"
13
-
>
9
+
<a class="text-lightblue-500 hover:underline" href="https://atproto.com" target="_blank">
14
10
AT Protocol
15
11
</a>
16
12
.
···
41
37
<div>
42
38
<span class="font-semibold text-orange-400">PDS</span>
43
39
<div>
44
-
<A
45
-
href="/pyramid-activation.today"
46
-
class="text-lightblue-500 hover:underline"
47
-
>
40
+
<A href="/pyramid-activation.today" class="text-lightblue-500 hover:underline">
48
41
https://pyramid-activation.today
49
42
</A>
50
43
</div>
+5
-15
src/views/labels.tsx
+5
-15
src/views/labels.tsx
···
24
24
});
25
25
26
26
const fetchLabels = async () => {
27
-
const uriPatterns = (
28
-
document.getElementById("patterns") as HTMLInputElement
29
-
).value;
27
+
const uriPatterns = (document.getElementById("patterns") as HTMLInputElement).value;
30
28
if (!uriPatterns) return;
31
29
const res = await rpc.get("com.atproto.label.queryLabels", {
32
30
params: {
···
46
44
setLabels([]);
47
45
setCursor("");
48
46
setSearchParams({
49
-
uriPatterns: (document.getElementById("patterns") as HTMLInputElement)
50
-
.value,
47
+
uriPatterns: (document.getElementById("patterns") as HTMLInputElement).value,
51
48
});
52
49
refetch();
53
50
};
54
51
55
52
const filterLabels = () => {
56
-
const newFilter = labels().filter((label) =>
57
-
filter() ? filter() === label.val : true,
58
-
);
53
+
const newFilter = labels().filter((label) => (filter() ? filter() === label.val : true));
59
54
setLabelCount(newFilter.length);
60
55
return newFilter;
61
56
};
62
57
63
58
return (
64
59
<>
65
-
<form
66
-
class="mt-3 flex flex-col items-center gap-y-1"
67
-
onsubmit={(e) => e.preventDefault()}
68
-
>
60
+
<form class="mt-3 flex flex-col items-center gap-y-1" onsubmit={(e) => e.preventDefault()}>
69
61
<div class="w-full">
70
62
<label for="patterns" class="ml-0.5 text-sm">
71
63
URI Patterns (comma-separated)
···
191
183
</For>
192
184
</div>
193
185
</Show>
194
-
<Show
195
-
when={!labels().length && !response.loading && searchParams.uriPatterns}
196
-
>
186
+
<Show when={!labels().length && !response.loading && searchParams.uriPatterns}>
197
187
<div class="mt-2">No results</div>
198
188
</Show>
199
189
</>
+4
-12
src/views/pds.tsx
+4
-12
src/views/pds.tsx
···
11
11
const [version, setVersion] = createSignal<string>();
12
12
const [cursor, setCursor] = createSignal<string>();
13
13
setPDS(params.pds);
14
-
const pds =
15
-
params.pds.startsWith("localhost") ?
16
-
`http://${params.pds}`
17
-
: `https://${params.pds}`;
14
+
const pds = params.pds.startsWith("localhost") ? `http://${params.pds}` : `https://${params.pds}`;
18
15
const rpc = new XRPC({ handler: new CredentialManager({ service: pds }) });
19
16
20
17
const listRepos = async (cursor: string | undefined) =>
···
44
41
<div class="mt-3 flex flex-col">
45
42
<Show when={version()}>
46
43
<div class="flex max-w-[21rem] gap-1">
47
-
<span class="font-semibold text-stone-600 dark:text-stone-400">
48
-
Version
49
-
</span>
44
+
<span class="font-semibold text-stone-600 dark:text-stone-400">Version</span>
50
45
<span class="break-anywhere">{version()}</span>
51
46
</div>
52
47
</Show>
53
-
<p class="w-full font-semibold text-stone-600 dark:text-stone-400">
54
-
Repositories
55
-
</p>
48
+
<p class="w-full font-semibold text-stone-600 dark:text-stone-400">Repositories</p>
56
49
<For each={repos()}>
57
50
{(repo) => (
58
51
<A
···
60
53
classList={{
61
54
"w-full flex font-mono relative": true,
62
55
"text-lightblue-500": repo.active,
63
-
"text-gray-300 absolute -left-5 dark:text-gray-600":
64
-
!repo.active,
56
+
"text-gray-300 absolute -left-5 dark:text-gray-600": !repo.active,
65
57
}}
66
58
>
67
59
<Show when={!repo.active}>
+12
-42
src/views/record.tsx
+12
-42
src/views/record.tsx
···
13
13
import { setCID, setValidRecord, validRecord } from "../components/navbar.jsx";
14
14
import { theme } from "../components/settings.jsx";
15
15
16
-
import {
17
-
didDocCache,
18
-
getAllBacklinks,
19
-
LinkData,
20
-
resolveHandle,
21
-
resolvePDS,
22
-
} from "../utils/api.js";
16
+
import { didDocCache, getAllBacklinks, LinkData, resolveHandle, resolvePDS } from "../utils/api.js";
23
17
import { AtUri, uriTemplates } from "../utils/templates.js";
24
18
import { verifyRecord } from "../utils/verify.js";
25
19
···
79
73
80
74
if (errors.length > 0) {
81
75
console.warn(errors);
82
-
setNotice(
83
-
`Invalid record: ${errors.map((e) => e.message).join("\n")}`,
84
-
);
76
+
setNotice(`Invalid record: ${errors.map((e) => e.message).join("\n")}`);
85
77
}
86
78
setValidRecord(errors.length === 0);
87
79
} catch (err) {
···
203
195
<div class="i-line-md-loading-twotone-loop mt-3 text-xl" />
204
196
</Show>
205
197
<Show when={validRecord() === false}>
206
-
<div class="w-20rem mb-2 mt-3 break-words text-red-500 dark:text-red-400">
207
-
{notice()}
208
-
</div>
198
+
<div class="w-20rem mb-2 mt-3 break-words text-red-500 dark:text-red-400">{notice()}</div>
209
199
</Show>
210
200
<Show when={record()}>
211
201
<div class="my-4 flex w-full justify-center gap-x-2">
···
221
211
target="_blank"
222
212
href={externalLink()?.link}
223
213
>
224
-
{externalLink()?.label}{" "}
225
-
<div class="i-tabler-external-link text-sm" />
214
+
{externalLink()?.label} <div class="i-tabler-external-link text-sm" />
226
215
</a>
227
216
</Show>
228
-
<Show
229
-
when={loginState() && agent.sub === record()?.uri.split("/")[2]}
230
-
>
217
+
<Show when={loginState() && agent.sub === record()?.uri.split("/")[2]}>
231
218
<Show when={openEdit()}>
232
219
<dialog
233
220
ref={setModal}
···
252
239
</div>
253
240
<Editor theme={theme().color} model={model!} />
254
241
<div class="mt-2 flex flex-col gap-2">
255
-
<div class="text-red-500 dark:text-red-400">
256
-
{editNotice()}
257
-
</div>
242
+
<div class="text-red-500 dark:text-red-400">{editNotice()}</div>
258
243
<div class="flex items-center justify-end gap-2">
259
244
<div class="flex items-center gap-1">
260
-
<input
261
-
id="recreate"
262
-
class="size-4"
263
-
name="recreate"
264
-
type="checkbox"
265
-
/>
245
+
<input id="recreate" class="size-4" name="recreate" type="checkbox" />
266
246
<label for="recreate" class="select-none">
267
247
Recreate record
268
248
</label>
···
288
268
</Show>
289
269
<button
290
270
onclick={() => {
291
-
model = editor.createModel(
292
-
JSON.stringify(record()?.value, null, 2),
293
-
"json",
294
-
);
271
+
model = editor.createModel(JSON.stringify(record()?.value, null, 2), "json");
295
272
setOpenEdit(true);
296
273
}}
297
274
class="dark:bg-dark-700 dark:hover:bg-dark-800 rounded-lg border border-slate-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-slate-100 focus:outline-none focus:ring-1 focus:ring-slate-700 dark:focus:ring-slate-300"
···
335
312
</div>
336
313
<div
337
314
classList={{
338
-
"break-anywhere mb-2 whitespace-pre-wrap pb-3 font-mono text-sm sm:text-base":
339
-
true,
315
+
"break-anywhere mb-2 whitespace-pre-wrap pb-3 font-mono text-sm sm:text-base": true,
340
316
"border-b border-neutral-500": !!backlinks(),
341
317
}}
342
318
>
343
319
<Show when={!JSONSyntax()}>
344
-
<JSONValue
345
-
data={record()?.value as any}
346
-
repo={record()!.uri.split("/")[2]}
347
-
/>
320
+
<JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} />
348
321
</Show>
349
322
<Show when={JSONSyntax()}>
350
323
<span
351
324
innerHTML={syntaxHighlight(
352
325
JSON.stringify(record()?.value, null, 2).replace(
353
326
/[\u007F-\uFFFF]/g,
354
-
(chr) =>
355
-
"\\u" + ("0000" + chr.charCodeAt(0).toString(16)).slice(-4),
327
+
(chr) => "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).slice(-4),
356
328
),
357
329
)}
358
330
></span>
359
331
</Show>
360
332
</div>
361
333
<Show when={backlinks()}>
362
-
{(backlinks) => (
363
-
<Backlinks links={backlinks().links} target={backlinks().target} />
364
-
)}
334
+
{(backlinks) => <Backlinks links={backlinks().links} target={backlinks().target} />}
365
335
</Show>
366
336
</Show>
367
337
</>
+12
-38
src/views/repo.tsx
+12
-38
src/views/repo.tsx
···
1
1
import { createSignal, For, Show, createResource } from "solid-js";
2
2
import { CredentialManager, XRPC } from "@atcute/client";
3
3
import { A, query, useParams } from "@solidjs/router";
4
-
import {
5
-
didDocCache,
6
-
getAllBacklinks,
7
-
LinkData,
8
-
resolveHandle,
9
-
resolvePDS,
10
-
} from "../utils/api.js";
4
+
import { didDocCache, getAllBacklinks, LinkData, resolveHandle, resolvePDS } from "../utils/api.js";
11
5
import { DidDocument } from "@atcute/client/utils/did";
12
6
import { Backlinks } from "../components/backlinks.jsx";
13
7
···
24
18
let did = params.repo;
25
19
26
20
const describeRepo = query(
27
-
(repo: string) =>
28
-
rpc.get("com.atproto.repo.describeRepo", { params: { repo: repo } }),
21
+
(repo: string) => rpc.get("com.atproto.repo.describeRepo", { params: { repo: repo } }),
29
22
"describeRepo",
30
23
);
31
24
···
51
44
const downloadRepo = async () => {
52
45
try {
53
46
setDownloading(true);
54
-
const response = await fetch(
55
-
`${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`,
56
-
);
47
+
const response = await fetch(`${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`);
57
48
if (!response.ok) {
58
49
throw new Error(`HTTP error status: ${response.status}`);
59
50
}
···
78
69
<Show when={repo()}>
79
70
<div class="mt-3 flex w-[21rem] flex-col gap-2 break-words">
80
71
<div class="flex flex-col border-b border-neutral-500 pb-2 font-mono">
81
-
<p class="font-sans font-semibold text-stone-600 dark:text-stone-400">
82
-
Collections
83
-
</p>
72
+
<p class="font-sans font-semibold text-stone-600 dark:text-stone-400">Collections</p>
84
73
<For each={repo()?.collections}>
85
74
{(collection) => (
86
75
<A
···
96
85
{(didDocument) => (
97
86
<div class="flex flex-col gap-y-1">
98
87
<div>
99
-
<span class="font-semibold text-stone-600 dark:text-stone-400">
100
-
ID{" "}
101
-
</span>
88
+
<span class="font-semibold text-stone-600 dark:text-stone-400">ID </span>
102
89
<span>{didDocument().id}</span>
103
90
</div>
104
91
<div>
105
-
<p class="font-semibold text-stone-600 dark:text-stone-400">
106
-
Identities
107
-
</p>
92
+
<p class="font-semibold text-stone-600 dark:text-stone-400">Identities</p>
108
93
<ul class="ml-3">
109
-
<For each={didDocument().alsoKnownAs}>
110
-
{(alias) => <li>{alias}</li>}
111
-
</For>
94
+
<For each={didDocument().alsoKnownAs}>{(alias) => <li>{alias}</li>}</For>
112
95
</ul>
113
96
</div>
114
97
<div>
115
-
<p class="font-semibold text-stone-600 dark:text-stone-400">
116
-
Services
117
-
</p>
98
+
<p class="font-semibold text-stone-600 dark:text-stone-400">Services</p>
118
99
<ul class="ml-3">
119
100
<For each={didDocument().service}>
120
101
{(service) => (
···
133
114
</ul>
134
115
</div>
135
116
<div>
136
-
<p class="font-semibold text-stone-600 dark:text-stone-400">
137
-
Verification methods
138
-
</p>
117
+
<p class="font-semibold text-stone-600 dark:text-stone-400">Verification methods</p>
139
118
<ul class="ml-3">
140
119
<For each={didDocument().verificationMethod}>
141
120
{(verif) => (
···
156
135
}
157
136
target="_blank"
158
137
>
159
-
DID document{" "}
160
-
<div class="i-tabler-external-link ml-0.5 text-xs" />
138
+
DID document <div class="i-tabler-external-link ml-0.5 text-xs" />
161
139
</a>
162
140
<Show when={repo()?.did.startsWith("did:plc")}>
163
141
<a
···
165
143
href={`https://boat.kelinci.net/plc-oplogs?q=${repo()?.did}`}
166
144
target="_blank"
167
145
>
168
-
PLC operation logs{" "}
169
-
<div class="i-tabler-external-link ml-0.5 text-xs" />
146
+
PLC operation logs <div class="i-tabler-external-link ml-0.5 text-xs" />
170
147
</a>
171
148
</Show>
172
149
<div class="flex items-center gap-1">
···
183
160
<Show when={backlinks()}>
184
161
{(backlinks) => (
185
162
<div class="mt-2 border-t border-neutral-500 pt-2">
186
-
<Backlinks
187
-
links={backlinks().links}
188
-
target={backlinks().target}
189
-
/>
163
+
<Backlinks links={backlinks().links} target={backlinks().target} />
190
164
</div>
191
165
)}
192
166
</Show>
+9
-25
src/views/stream.tsx
+9
-25
src/views/stream.tsx
···
14
14
const [searchParams, setSearchParams] = useSearchParams();
15
15
const [parameters, setParameters] = createSignal<Parameter[]>([]);
16
16
const streamType =
17
-
useLocation().pathname === "/firehose" ?
18
-
StreamType.FIREHOSE
19
-
: StreamType.JETSTREAM;
17
+
useLocation().pathname === "/firehose" ? StreamType.FIREHOSE : StreamType.JETSTREAM;
20
18
21
19
const [records, setRecords] = createSignal<Array<any>>([]);
22
20
const [connected, setConnected] = createSignal(false);
···
37
35
let url = "";
38
36
if (streamType === StreamType.JETSTREAM) {
39
37
url =
40
-
formData.get("instance")?.toString() ??
41
-
"wss://jetstream1.us-east.bsky.network/subscribe";
38
+
formData.get("instance")?.toString() ?? "wss://jetstream1.us-east.bsky.network/subscribe";
42
39
url = url.concat("?");
43
40
} else {
44
41
url = formData.get("instance")?.toString() ?? "wss://bsky.network";
···
46
43
47
44
const collections = formData.get("collections")?.toString().split(",");
48
45
collections?.forEach((collection) => {
49
-
if (collection.length)
50
-
url = url.concat(`wantedCollections=${collection}&`);
46
+
if (collection.length) url = url.concat(`wantedCollections=${collection}&`);
51
47
});
52
48
53
49
const dids = formData.get("dids")?.toString().split(",");
···
131
127
132
128
onMount(async () => {
133
129
const formData = new FormData();
134
-
if (searchParams.instance)
135
-
formData.append("instance", searchParams.instance.toString());
130
+
if (searchParams.instance) formData.append("instance", searchParams.instance.toString());
136
131
if (searchParams.collections)
137
132
formData.append("collections", searchParams.collections.toString());
138
-
if (searchParams.dids)
139
-
formData.append("dids", searchParams.dids.toString());
140
-
if (searchParams.cursor)
141
-
formData.append("cursor", searchParams.cursor.toString());
142
-
if (searchParams.allEvents)
143
-
formData.append("allEvents", searchParams.allEvents.toString());
133
+
if (searchParams.dids) formData.append("dids", searchParams.dids.toString());
134
+
if (searchParams.cursor) formData.append("cursor", searchParams.cursor.toString());
135
+
if (searchParams.allEvents) formData.append("allEvents", searchParams.allEvents.toString());
144
136
if (searchParams.instance) connectSocket(formData);
145
137
});
146
138
···
149
141
return (
150
142
<div class="mt-4 flex flex-col items-center gap-y-3">
151
143
<div class="flex divide-x-2 text-lg font-bold">
152
-
<A
153
-
class="pr-2"
154
-
inactiveClass="text-lightblue-500 hover:underline"
155
-
href="/jetstream"
156
-
>
144
+
<A class="pr-2" inactiveClass="text-lightblue-500 hover:underline" href="/jetstream">
157
145
Jetstream
158
146
</A>
159
-
<A
160
-
class="pl-2"
161
-
inactiveClass="text-lightblue-500 hover:underline"
162
-
href="/firehose"
163
-
>
147
+
<A class="pl-2" inactiveClass="text-lightblue-500 hover:underline" href="/firehose">
164
148
Firehose
165
149
</A>
166
150
</div>