atmosphere explorer pdsls.dev
atproto tool typescript

Compare changes

Choose any two refs to compare.

+1 -1
src/components/theme.tsx
··· 43 44 return ( 45 <div class="flex flex-col gap-1"> 46 - <label class="select-none">Theme</label> 47 <div class="flex gap-2"> 48 <ThemeOption theme="auto" label="Auto" /> 49 <ThemeOption theme="light" label="Light" />
··· 43 44 return ( 45 <div class="flex flex-col gap-1"> 46 + <label class="font-medium select-none">Theme</label> 47 <div class="flex gap-2"> 48 <ThemeOption theme="auto" label="Auto" /> 49 <ThemeOption theme="light" label="Light" />
public/fonts/Figtree[wght].woff2 public/fonts/Figtree.woff2
+1 -1
src/styles/index.css
··· 8 9 @font-face { 10 font-family: "Figtree"; 11 - src: url("/fonts/Figtree[wght].woff2") format("woff2"); 12 font-display: swap; 13 } 14
··· 8 9 @font-face { 10 font-family: "Figtree"; 11 + src: url("/fonts/Figtree.woff2") format("woff2"); 12 font-display: swap; 13 } 14
+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
··· 12 13 export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 14 export const [sessions, setSessions] = createStore<Sessions>();
··· 12 13 export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>(); 14 export const [sessions, setSessions] = createStore<Sessions>(); 15 + export const [openManager, setOpenManager] = createSignal(false);
+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
··· 5 cursor: string | undefined; 6 scrollY: number; 7 reverse: boolean; 8 } 9 10 type RouteCache = Record<string, CollectionCacheEntry>;
··· 5 cursor: string | undefined; 6 scrollY: number; 7 reverse: boolean; 8 + limit: number; 9 } 10 11 type RouteCache = Record<string, CollectionCacheEntry>;
+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
··· 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
··· 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
··· 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
··· 57 const schemaPromise = (async () => { 58 let didDocPromise = documentCache.get(authority); 59 if (!didDocPromise) { 60 - didDocPromise = didDocumentResolver.resolve(authority); 61 documentCache.set(authority, didDocPromise); 62 } 63
··· 57 const schemaPromise = (async () => { 58 let didDocPromise = documentCache.get(authority); 59 if (!didDocPromise) { 60 + didDocPromise = didDocumentResolver().resolve(authority); 61 documentCache.set(authority, didDocPromise); 62 } 63
+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
··· 92 const handlePaste = (e: ClipboardEvent) => { 93 if (e.target === searchInput) return; 94 if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; 95 96 const pastedText = e.clipboardData?.getData("text"); 97 if (pastedText) processInput(pastedText);
··· 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 - Copyright (c) 2024-2025 Juliet Philippe <m@juli.ee> 2 3 Permission to use, copy, modify, and/or distribute this software for any 4 purpose with or without fee is hereby granted.
··· 1 + Copyright (c) 2024-2026 Juliet Philippe <m@juli.ee> 2 3 Permission to use, copy, modify, and/or distribute this software for any 4 purpose with or without fee is hereby granted.
+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
··· 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
··· 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>