+1
-1
src/components/theme.tsx
+1
-1
src/components/theme.tsx
public/fonts/Figtree[wght].woff2
public/fonts/Figtree.woff2
public/fonts/Figtree[wght].woff2
public/fonts/Figtree.woff2
+1
-1
src/styles/index.css
+1
-1
src/styles/index.css
+1
-2
src/auth/account.tsx
+1
-2
src/auth/account.tsx
···
17
retrieveSession,
18
saveSessionToStorage,
19
} from "./session-manager.js";
20
-
import { agent, sessions, setAgent, setSessions } from "./state.js";
21
22
const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => void }) => {
23
const removeSession = async (did: Did) => {
···
62
};
63
64
export const AccountManager = () => {
65
-
const [openManager, setOpenManager] = createSignal(false);
66
const [avatars, setAvatars] = createStore<Record<Did, string>>();
67
const [showingAddAccount, setShowingAddAccount] = createSignal(false);
68
···
17
retrieveSession,
18
saveSessionToStorage,
19
} from "./session-manager.js";
20
+
import { agent, openManager, sessions, setAgent, setOpenManager, setSessions } from "./state.js";
21
22
const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => void }) => {
23
const removeSession = async (did: Did) => {
···
62
};
63
64
export const AccountManager = () => {
65
const [avatars, setAvatars] = createStore<Record<Did, string>>();
66
const [showingAddAccount, setShowingAddAccount] = createSignal(false);
67
+1
src/auth/state.ts
+1
src/auth/state.ts
+33
src/components/favicon.tsx
+33
src/components/favicon.tsx
···
···
1
+
import { createSignal, JSX, Show } from "solid-js";
2
+
3
+
export const Favicon = (props: {
4
+
authority: string;
5
+
wrapper?: (children: JSX.Element) => JSX.Element;
6
+
}) => {
7
+
const [loaded, setLoaded] = createSignal(false);
8
+
const domain = () => props.authority.split(".").reverse().join(".");
9
+
10
+
const content = (
11
+
<>
12
+
<Show when={!loaded()}>
13
+
<span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" />
14
+
</Show>
15
+
<img
16
+
src={
17
+
["bsky.app", "bsky.chat"].includes(domain()) ?
18
+
"https://web-cdn.bsky.app/static/apple-touch-icon.png"
19
+
: `https://${domain()}/favicon.ico`
20
+
}
21
+
alt=""
22
+
class="h-4 w-4"
23
+
classList={{ hidden: !loaded() }}
24
+
onLoad={() => setLoaded(true)}
25
+
onError={() => setLoaded(false)}
26
+
/>
27
+
</>
28
+
);
29
+
30
+
return props.wrapper ?
31
+
props.wrapper(content)
32
+
: <div class="flex h-5 w-4 shrink-0 items-center justify-center">{content}</div>;
33
+
};
+1
src/utils/route-cache.ts
+1
src/utils/route-cache.ts
+8
-3
src/auth/oauth-config.ts
+8
-3
src/auth/oauth-config.ts
···
1
-
import { configureOAuth, defaultIdentityResolver } from "@atcute/oauth-browser-client";
2
import { didDocumentResolver, handleResolver } from "../utils/api";
3
4
configureOAuth({
5
metadata: {
6
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
7
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
8
},
9
-
identityResolver: defaultIdentityResolver({
10
handleResolver: handleResolver,
11
-
didDocumentResolver: didDocumentResolver,
12
}),
13
});
···
1
+
import { LocalActorResolver } from "@atcute/identity-resolver";
2
+
import { configureOAuth } from "@atcute/oauth-browser-client";
3
import { didDocumentResolver, handleResolver } from "../utils/api";
4
5
+
const reactiveDidDocumentResolver = {
6
+
resolve: async (did: string) => didDocumentResolver().resolve(did as any),
7
+
};
8
+
9
configureOAuth({
10
metadata: {
11
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
12
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
13
},
14
+
identityResolver: new LocalActorResolver({
15
handleResolver: handleResolver,
16
+
didDocumentResolver: reactiveDidDocumentResolver,
17
}),
18
});
+3
-4
src/layout.tsx
+3
-4
src/layout.tsx
···
12
import { Search, SearchButton, showSearch } from "./components/search.jsx";
13
import { themeEvent } from "./components/theme.jsx";
14
import { resolveHandle } from "./utils/api.js";
15
16
export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1;
17
···
187
</Show>
188
</div>
189
<NotificationContainer />
190
-
<Show
191
-
when={localStorage.plcDirectory && localStorage.plcDirectory !== "https://plc.directory"}
192
-
>
193
<div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs">
194
<span>
195
-
PLC directory: <span class="font-medium">{localStorage.plcDirectory}</span>
196
</span>
197
</div>
198
</Show>
···
12
import { Search, SearchButton, showSearch } from "./components/search.jsx";
13
import { themeEvent } from "./components/theme.jsx";
14
import { resolveHandle } from "./utils/api.js";
15
+
import { plcDirectory } from "./views/settings.jsx";
16
17
export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1;
18
···
188
</Show>
189
</div>
190
<NotificationContainer />
191
+
<Show when={plcDirectory() !== "https://plc.directory"}>
192
<div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs">
193
<span>
194
+
PLC directory: <span class="font-medium">{plcDirectory()}</span>
195
</span>
196
</div>
197
</Show>
+22
-14
src/utils/api.ts
+22
-14
src/utils/api.ts
···
16
import { DohJsonLexiconAuthorityResolver, LexiconSchemaResolver } from "@atcute/lexicon-resolver";
17
import { Did, Handle } from "@atcute/lexicons";
18
import { AtprotoDid, isHandle, Nsid } from "@atcute/lexicons/syntax";
19
import { createStore } from "solid-js/store";
20
import { setPDS } from "../components/navbar";
21
-
22
-
export const didDocumentResolver = new CompositeDidDocumentResolver({
23
-
methods: {
24
-
plc: new PlcDidDocumentResolver({
25
-
apiUrl: localStorage.getItem("plcDirectory") ?? "https://plc.directory",
26
}),
27
-
web: new AtprotoWebDidDocumentResolver(),
28
-
},
29
-
});
30
31
export const handleResolver = new CompositeHandleResolver({
32
strategy: "dns-first",
···
40
dohUrl: "https://dns.google/resolve?",
41
});
42
43
-
const schemaResolver = new LexiconSchemaResolver({
44
-
didDocumentResolver: didDocumentResolver,
45
-
});
46
47
const didPDSCache: Record<string, string> = {};
48
const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({});
···
54
throw new Error("Not a valid DID identifier");
55
}
56
57
-
const doc = await didDocumentResolver.resolve(did);
58
didDocCache[did] = doc;
59
60
const pds = getPdsEndpoint(doc);
···
83
if (!isAtprotoDid(did)) {
84
throw new Error("Not a valid DID identifier");
85
}
86
-
return await didDocumentResolver.resolve(did);
87
};
88
89
const validateHandle = async (handle: Handle, did: Did) => {
···
145
};
146
147
const resolveLexiconSchema = async (authority: AtprotoDid, nsid: Nsid) => {
148
-
return await schemaResolver.resolve(authority, nsid);
149
};
150
151
interface LinkData {
···
16
import { DohJsonLexiconAuthorityResolver, LexiconSchemaResolver } from "@atcute/lexicon-resolver";
17
import { Did, Handle } from "@atcute/lexicons";
18
import { AtprotoDid, isHandle, Nsid } from "@atcute/lexicons/syntax";
19
+
import { createMemo } from "solid-js";
20
import { createStore } from "solid-js/store";
21
import { setPDS } from "../components/navbar";
22
+
import { plcDirectory } from "../views/settings";
23
+
24
+
export const didDocumentResolver = createMemo(
25
+
() =>
26
+
new CompositeDidDocumentResolver({
27
+
methods: {
28
+
plc: new PlcDidDocumentResolver({
29
+
apiUrl: plcDirectory(),
30
+
}),
31
+
web: new AtprotoWebDidDocumentResolver(),
32
+
},
33
}),
34
+
);
35
36
export const handleResolver = new CompositeHandleResolver({
37
strategy: "dns-first",
···
45
dohUrl: "https://dns.google/resolve?",
46
});
47
48
+
const schemaResolver = createMemo(
49
+
() =>
50
+
new LexiconSchemaResolver({
51
+
didDocumentResolver: didDocumentResolver(),
52
+
}),
53
+
);
54
55
const didPDSCache: Record<string, string> = {};
56
const [labelerCache, setLabelerCache] = createStore<Record<string, string>>({});
···
62
throw new Error("Not a valid DID identifier");
63
}
64
65
+
const doc = await didDocumentResolver().resolve(did);
66
didDocCache[did] = doc;
67
68
const pds = getPdsEndpoint(doc);
···
91
if (!isAtprotoDid(did)) {
92
throw new Error("Not a valid DID identifier");
93
}
94
+
return await didDocumentResolver().resolve(did);
95
};
96
97
const validateHandle = async (handle: Handle, did: Did) => {
···
153
};
154
155
const resolveLexiconSchema = async (authority: AtprotoDid, nsid: Nsid) => {
156
+
return await schemaResolver().resolve(authority, nsid);
157
};
158
159
interface LinkData {
+2
-3
src/views/logs.tsx
+2
-3
src/views/logs.tsx
···
8
import { createEffect, createResource, createSignal, For, Show } from "solid-js";
9
import { localDateFromTimestamp } from "../utils/date.js";
10
import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js";
11
12
type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method";
13
···
23
!activePlcEvent() || diffs.some((d) => d.type.startsWith(activePlcEvent()!));
24
25
const fetchPlcLogs = async () => {
26
-
const res = await fetch(
27
-
`${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`,
28
-
);
29
const json = await res.json();
30
const logs = defs.indexedEntryLog.parse(json);
31
setRawLogs(logs);
···
8
import { createEffect, createResource, createSignal, For, Show } from "solid-js";
9
import { localDateFromTimestamp } from "../utils/date.js";
10
import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js";
11
+
import { plcDirectory } from "./settings.jsx";
12
13
type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method";
14
···
24
!activePlcEvent() || diffs.some((d) => d.type.startsWith(activePlcEvent()!));
25
26
const fetchPlcLogs = async () => {
27
+
const res = await fetch(`${plcDirectory()}/${props.did}/log/audit`);
28
const json = await res.json();
29
const logs = defs.indexedEntryLog.parse(json);
30
setRawLogs(logs);
+1
-1
src/views/record.tsx
+1
-1
src/views/record.tsx
+4
-5
src/views/repo.tsx
+4
-5
src/views/repo.tsx
···
42
import { detectDidKeyType, detectKeyType } from "../utils/key.js";
43
import { BlobView } from "./blob.jsx";
44
import { PlcLogView } from "./logs.jsx";
45
46
export const RepoView = () => {
47
const params = useParams();
···
101
};
102
103
const getRotationKeys = async () => {
104
-
const res = await fetch(
105
-
`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/last`,
106
-
);
107
const json = await res.json();
108
setRotationKeys(json.rotationKeys ?? []);
109
};
···
364
<NavMenu
365
href={
366
did.startsWith("did:plc") ?
367
-
`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}`
368
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
369
}
370
newTab
···
373
/>
374
<Show when={did.startsWith("did:plc")}>
375
<NavMenu
376
-
href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`}
377
newTab
378
label="Audit log"
379
icon="lucide--external-link"
···
42
import { detectDidKeyType, detectKeyType } from "../utils/key.js";
43
import { BlobView } from "./blob.jsx";
44
import { PlcLogView } from "./logs.jsx";
45
+
import { plcDirectory } from "./settings.jsx";
46
47
export const RepoView = () => {
48
const params = useParams();
···
102
};
103
104
const getRotationKeys = async () => {
105
+
const res = await fetch(`${plcDirectory()}/${did}/log/last`);
106
const json = await res.json();
107
setRotationKeys(json.rotationKeys ?? []);
108
};
···
363
<NavMenu
364
href={
365
did.startsWith("did:plc") ?
366
+
`${plcDirectory()}/${did}`
367
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
368
}
369
newTab
···
372
/>
373
<Show when={did.startsWith("did:plc")}>
374
<NavMenu
375
+
href={`${plcDirectory()}/${did}/log/audit`}
376
newTab
377
label="Audit log"
378
icon="lucide--external-link"
+1
src/components/search.tsx
+1
src/components/search.tsx
···
92
const handlePaste = (e: ClipboardEvent) => {
93
if (e.target === searchInput) return;
94
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
95
+
if (document.querySelector("[data-modal]")) return;
96
97
const pastedText = e.clipboardData?.getData("text");
98
if (pastedText) processInput(pastedText);
+1
-1
LICENSE
+1
-1
LICENSE
+2
-1
README.md
+2
-1
README.md
···
1
-
# PDSls - AT Protocol Explorer
2
3
Lightweight and client-side web app to navigate [atproto](https://atproto.com/).
4
···
9
- Jetstream and firehose (com.atproto.sync.subscribeRepos) streaming.
10
- Backlinks support with [constellation](https://constellation.microcosm.blue/).
11
- Query moderation labels.
12
13
## Hacking
14
···
1
+
# PDSls - Atmosphere Explorer
2
3
Lightweight and client-side web app to navigate [atproto](https://atproto.com/).
4
···
9
- Jetstream and firehose (com.atproto.sync.subscribeRepos) streaming.
10
- Backlinks support with [constellation](https://constellation.microcosm.blue/).
11
- Query moderation labels.
12
+
- Explore and unpack repository archives (CAR).
13
14
## Hacking
15
+1
-1
src/views/blob.tsx
+1
-1
src/views/blob.tsx
···
51
{blobs()?.length} blob{(blobs()?.length ?? 0 > 1) ? "s" : ""}
52
</p>
53
<Show when={!response.loading && cursor()}>
54
-
<Button onClick={() => refetch()}>Load More</Button>
55
</Show>
56
<Show when={response.loading}>
57
<span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span>
···
51
{blobs()?.length} blob{(blobs()?.length ?? 0 > 1) ? "s" : ""}
52
</p>
53
<Show when={!response.loading && cursor()}>
54
+
<Button onClick={() => refetch()}>Load more</Button>
55
</Show>
56
<Show when={response.loading}>
57
<span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span>
+1
-1
src/views/pds.tsx
+1
-1
src/views/pds.tsx
···
260
<div class="flex flex-col items-center gap-1 pb-2">
261
<p>{repos()?.length} loaded</p>
262
<Show when={!response.loading && cursor()}>
263
-
<Button onClick={() => refetch()}>Load More</Button>
264
</Show>
265
<Show when={response.loading}>
266
<span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span>
···
260
<div class="flex flex-col items-center gap-1 pb-2">
261
<p>{repos()?.length} loaded</p>
262
<Show when={!response.loading && cursor()}>
263
+
<Button onClick={() => refetch()}>Load more</Button>
264
</Show>
265
<Show when={response.loading}>
266
<span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span>