import { Client, simpleFetchHandler } from "@atcute/client"; import { DidDocument } from "@atcute/identity"; import { ActorIdentifier, Did, Handle, Nsid } from "@atcute/lexicons"; import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; import { createEffect, createResource, createSignal, ErrorBoundary, For, onMount, Show, Suspense, } from "solid-js"; import { createStore } from "solid-js/store"; import { Backlinks } from "../components/backlinks.jsx"; import { ActionMenu, CopyMenu, DropdownMenu, MenuProvider, MenuSeparator, NavMenu, } from "../components/dropdown.jsx"; import { setPDS } from "../components/navbar.jsx"; import { addNotification, removeNotification, updateNotification, } from "../components/notification.jsx"; import { TextInput } from "../components/text-input.jsx"; import Tooltip from "../components/tooltip.jsx"; import { didDocCache, labelerCache, resolveHandle, resolveLexiconAuthority, resolvePDS, validateHandle, } from "../utils/api.js"; import { detectDidKeyType, detectKeyType } from "../utils/key.js"; import { BlobView } from "./blob.jsx"; import { PlcLogView } from "./logs.jsx"; export const RepoView = () => { const params = useParams(); const location = useLocation(); const navigate = useNavigate(); const [error, setError] = createSignal(); const [downloading, setDownloading] = createSignal(false); const [didDoc, setDidDoc] = createSignal(); const [nsids, setNsids] = createSignal>(); const [filter, setFilter] = createSignal(); const [showFilter, setShowFilter] = createSignal(false); const [validHandles, setValidHandles] = createStore>({}); const [rotationKeys, setRotationKeys] = createSignal>([]); let rpc: Client; let pds: string; const did = params.repo!; // Handle scrolling to a collection group when hash is like #collections:app.bsky createEffect(() => { const hash = location.hash; if (hash.startsWith("#collections:")) { const authority = hash.slice(13); requestAnimationFrame(() => { const element = document.getElementById(`collection-${authority}`); if (element) element.scrollIntoView({ behavior: "instant", block: "start" }); }); } }); const RepoTab = (props: { tab: "collections" | "backlinks" | "identity" | "blobs" | "logs"; label: string; }) => { const isActive = () => { if (!location.hash) { if (!error() && props.tab === "collections") return true; if (!!error() && props.tab === "identity") return true; return false; } if (props.tab === "collections") return location.hash === "#collections" || location.hash.startsWith("#collections:"); return location.hash === `#${props.tab}`; }; return ( {props.label} ); }; const getRotationKeys = async () => { const res = await fetch( `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/last`, ); const json = await res.json(); setRotationKeys(json.rotationKeys ?? []); }; const fetchRepo = async () => { try { pds = await resolvePDS(did); } catch { if (!did.startsWith("did:")) { try { const did = await resolveHandle(params.repo as Handle); navigate(location.pathname.replace(params.repo!, did), { replace: true }); return; } catch { try { const nsid = params.repo as Nsid; const res = await resolveLexiconAuthority(nsid); navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`, { replace: true }); return; } catch { navigate(`/${did}`, { replace: true }); return; } } } } setDidDoc(didDocCache[did] as DidDocument); getRotationKeys(); validateHandles(); if (!pds) { setError("Missing PDS"); setPDS("Missing PDS"); return {}; } rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); try { const res = await rpc.get("com.atproto.repo.describeRepo", { params: { repo: did as ActorIdentifier }, }); if (res.ok) { const collections: Record = {}; res.data.collections.forEach((c) => { const nsid = c.split("."); if (nsid.length > 2) { const authority = `${nsid[0]}.${nsid[1]}`; collections[authority] = { nsids: (collections[authority]?.nsids ?? []).concat(nsid.slice(2).join(".")), hidden: false, }; } }); setNsids(collections); } else { console.error(res.data.error); switch (res.data.error) { case "RepoDeactivated": setError("Deactivated"); break; case "RepoTakendown": setError("Takendown"); break; default: setError("Unreachable"); } } return res.data; } catch { return {}; } }; const [repo] = createResource(fetchRepo); const validateHandles = async () => { for (const alias of didDoc()?.alsoKnownAs ?? []) { if (alias.startsWith("at://")) setValidHandles( alias, await validateHandle(alias.replace("at://", "") as Handle, did as Did), ); } }; const downloadRepo = async () => { let notificationId: string | null = null; try { setDownloading(true); notificationId = addNotification({ message: "Downloading repository...", progress: 0, total: 0, type: "info", }); const response = await fetch(`${pds}/xrpc/com.atproto.sync.getRepo?did=${did}`); if (!response.ok) { throw new Error(`HTTP error status: ${response.status}`); } const contentLength = response.headers.get("content-length"); const total = contentLength ? parseInt(contentLength, 10) : 0; let loaded = 0; const reader = response.body?.getReader(); const chunks: Uint8Array[] = []; if (reader) { while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; if (total > 0) { const progress = Math.round((loaded / total) * 100); updateNotification(notificationId, { progress, total, }); } else { const progressMB = Math.round((loaded / (1024 * 1024)) * 10) / 10; updateNotification(notificationId, { progress: progressMB, total: 0, }); } } } const blob = new Blob(chunks); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${did}-${new Date().toISOString()}.car`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); updateNotification(notificationId, { message: "Repository downloaded successfully", type: "success", progress: undefined, }); setTimeout(() => { if (notificationId) removeNotification(notificationId); }, 3000); } catch (error) { console.error("Download failed:", error); if (notificationId) { updateNotification(notificationId, { message: "Download failed", type: "error", progress: undefined, }); setTimeout(() => { if (notificationId) removeNotification(notificationId); }, 5000); } } setDownloading(false); }; return (
{error()}
downloadRepo()} />
Error: {err.message}
} > } >
Error: {err.message}
} > } >
Error: {err.message}
} > } >
setFilter(e.currentTarget.value.toLowerCase())} class="grow" ref={(node) => { onMount(() => node.focus()); }} />
filter() ? authority.includes(filter()!) || nsids()?.[authority].nsids.some((nsid) => `${authority}.${nsid}`.includes(filter()!), ) : true, )} > {(authority) => { const reversedDomain = authority.split(".").reverse().join("."); const [faviconLoaded, setFaviconLoaded] = createSignal(false); const isHighlighted = () => location.hash === `#collections:${authority}`; return (
{`${reversedDomain} setFaviconLoaded(true)} onError={() => setFaviconLoaded(false)} />
filter() ? `${authority}.${nsid}`.includes(filter()!) : true, )} > {(nsid) => ( {authority} .{nsid} )}
); }}
{(didDocument) => (
{/* ID Section */}

ID

{didDocument().id}
{/* Aliases Section */}

Aliases

{(alias) => (
{alias}
)}
{/* Services Section */}

Services

{(service) => (
#{service.id.split("#")[1]}
{service.serviceEndpoint.toString()}
)}
{/* Verification Methods Section */}

Verification Methods

{(verif) => ( {(key) => (
#{verif.id.split("#")[1]} {detectKeyType(key())}
{key()}
)}
)}
{/* Rotation Keys Section */} 0}>

Rotation Keys

{(key) => (
{detectDidKeyType(key)}
{key.replace("did:key:", "")}
)}
)}
); };