atmosphere explorer pdsls.dev
atproto tool typescript

Compare changes

Choose any two refs to compare.

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