atproto explorer pdsls.dev
atproto tool

Compare changes

Choose any two refs to compare.

+7 -7
src/auth/login.tsx
···
<div class="font-semibold">Add account</div>
</div>
</Show>
-
<form onsubmit={(e) => e.preventDefault()}>
+
<form class="flex flex-col gap-2" onsubmit={(e) => e.preventDefault()}>
<label for="username" class="hidden">
Add account
</label>
···
class="grow py-1 select-none placeholder:text-sm focus:outline-none"
onInput={(e) => setLoginInput(e.currentTarget.value)}
/>
-
<button
-
onclick={() => initiateLogin(loginInput())}
-
class="flex items-center rounded-md p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
-
>
-
<span class="iconify lucide--log-in"></span>
-
</button>
</div>
+
<button
+
onclick={() => initiateLogin(loginInput())}
+
class="grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
+
>
+
Continue
+
</button>
</form>
</Show>
+6 -8
src/auth/scope-selector.tsx
···
)}
</For>
</div>
-
<div class="mt-2 flex gap-2">
-
<button
-
onclick={handleConfirm}
-
class="grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-3 py-1.5 hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
-
>
-
Continue
-
</button>
-
</div>
+
<button
+
onclick={handleConfirm}
+
class="mt-2 grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
+
>
+
Continue
+
</button>
</div>
);
};
+2 -2
src/auth/session-manager.ts
···
-
import { Client, CredentialManager } from "@atcute/client";
+
import { Client, simpleFetchHandler } from "@atcute/client";
import { Did } from "@atcute/lexicons";
import {
finalizeAuthorization,
···
export const getAvatar = async (did: Did): Promise<string | undefined> => {
const rpc = new Client({
-
handler: new CredentialManager({ service: "https://public.api.bsky.app" }),
+
handler: simpleFetchHandler({ service: "https://public.api.bsky.app" }),
});
const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } });
if (res.ok) {
+2 -2
src/views/blob.tsx
···
-
import { Client, CredentialManager } from "@atcute/client";
+
import { Client, simpleFetchHandler } from "@atcute/client";
import { createResource, createSignal, For, Show } from "solid-js";
import { Button } from "../components/button";
···
let rpc: Client;
const fetchBlobs = async () => {
-
if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: props.pds }) });
+
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: props.pds }) });
const res = await rpc.get("com.atproto.sync.listBlobs", {
params: {
did: props.repo as `did:${string}:${string}`,
+2 -2
src/views/labels.tsx
···
import { ComAtprotoLabelDefs } from "@atcute/atproto";
-
import { Client, CredentialManager } from "@atcute/client";
+
import { Client, simpleFetchHandler } from "@atcute/client";
import { isAtprotoDid } from "@atcute/identity";
import { Handle } from "@atcute/lexicons";
import { A, useSearchParams } from "@solidjs/router";
···
await resolvePDS(did);
if (!labelerCache[did]) throw new Error("Repository is not a labeler");
rpc = new Client({
-
handler: new CredentialManager({ service: labelerCache[did] }),
+
handler: simpleFetchHandler({ service: labelerCache[did] }),
});
setSearchParams({ did, uriPatterns });
+2 -2
src/views/pds.tsx
···
import { ComAtprotoServerDescribeServer, ComAtprotoSyncListRepos } from "@atcute/atproto";
-
import { Client, CredentialManager } from "@atcute/client";
+
import { Client, simpleFetchHandler } from "@atcute/client";
import { InferXRPCBodyOutput } from "@atcute/lexicons";
import * as TID from "@atcute/tid";
import { A, useLocation, useParams } from "@solidjs/router";
···
setPDS(params.pds);
const pds =
params.pds!.startsWith("localhost") ? `http://${params.pds}` : `https://${params.pds}`;
-
const rpc = new Client({ handler: new CredentialManager({ service: pds }) });
+
const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
const getVersion = async () => {
// @ts-expect-error: undocumented endpoint
+2 -2
src/views/repo.tsx
···
-
import { Client, CredentialManager } from "@atcute/client";
+
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";
···
return {};
}
-
rpc = new Client({ handler: new CredentialManager({ service: pds }) });
+
rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
try {
const res = await rpc.get("com.atproto.repo.describeRepo", {
params: { repo: did as ActorIdentifier },
public/favicon.ico

This is a binary file and will not be displayed.

-2
index.html
···
<meta property="description" content="Browse the public data on atproto" />
<link rel="manifest" href="/manifest.json" />
<title>PDSls</title>
-
<link rel="preconnect" href="https://rsms.me/" />
-
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link rel="preconnect" href="https://fonts.bunny.net" />
<link href="https://fonts.bunny.net/css?family=roboto-mono:400" rel="stylesheet" />
<link href="https://fonts.cdnfonts.com/css/pecita" rel="stylesheet" />
public/fonts/Figtree[wght].woff2

This is a binary file and will not be displayed.

+7 -1
src/styles/index.css
···
@custom-variant dark (&:where(.dark, .dark *));
+
@font-face {
+
font-family: "Figtree";
+
src: url("/fonts/Figtree[wght].woff2") format("woff2");
+
font-display: swap;
+
}
+
@theme {
-
--font-sans: "Inter", sans-serif;
+
--font-sans: "Figtree", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-pecita: "Pecita", serif;
+109
src/components/create/file-upload.tsx
···
+
import { Client } from "@atcute/client";
+
import { remove } from "@mary/exif-rm";
+
import { createSignal, onCleanup, Show } from "solid-js";
+
import { agent } from "../../auth/state";
+
import { Button } from "../button.jsx";
+
import { TextInput } from "../text-input.jsx";
+
import { editorInstance } from "./state";
+
+
export const FileUpload = (props: {
+
file: File;
+
blobInput: HTMLInputElement;
+
onClose: () => void;
+
}) => {
+
const [uploading, setUploading] = createSignal(false);
+
const [error, setError] = createSignal("");
+
+
onCleanup(() => (props.blobInput.value = ""));
+
+
const formatFileSize = (bytes: number) => {
+
if (bytes === 0) return "0 Bytes";
+
const k = 1024;
+
const sizes = ["Bytes", "KB", "MB", "GB"];
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
+
};
+
+
const uploadBlob = async () => {
+
let blob: Blob;
+
+
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
+
(document.getElementById("mimetype") as HTMLInputElement).value = "";
+
if (mimetype) blob = new Blob([props.file], { type: mimetype });
+
else blob = props.file;
+
+
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
+
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
+
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
+
}
+
+
const rpc = new Client({ handler: agent()! });
+
setUploading(true);
+
const res = await rpc.post("com.atproto.repo.uploadBlob", {
+
input: blob,
+
});
+
setUploading(false);
+
if (!res.ok) {
+
setError(res.data.error);
+
return;
+
}
+
editorInstance.view.dispatch({
+
changes: {
+
from: editorInstance.view.state.selection.main.head,
+
insert: JSON.stringify(res.data.blob, null, 2),
+
},
+
});
+
props.onClose();
+
};
+
+
return (
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
+
<h2 class="mb-2 font-semibold">Upload blob</h2>
+
<div class="flex flex-col gap-2 text-sm">
+
<div class="flex flex-col gap-1">
+
<p class="flex gap-1">
+
<span class="truncate">{props.file.name}</span>
+
<span class="shrink-0 text-neutral-600 dark:text-neutral-400">
+
({formatFileSize(props.file.size)})
+
</span>
+
</p>
+
</div>
+
<div class="flex items-center gap-x-2">
+
<label for="mimetype" class="shrink-0 select-none">
+
MIME type
+
</label>
+
<TextInput id="mimetype" placeholder={props.file.type} />
+
</div>
+
<div class="flex items-center gap-1">
+
<input id="exif-rm" type="checkbox" checked />
+
<label for="exif-rm" class="select-none">
+
Remove EXIF data
+
</label>
+
</div>
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
+
Metadata will be pasted after the cursor
+
</p>
+
<Show when={error()}>
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
+
</Show>
+
<div class="flex justify-between gap-2">
+
<Button onClick={props.onClose}>Cancel</Button>
+
<Show when={uploading()}>
+
<div class="flex items-center gap-1">
+
<span class="iconify lucide--loader-circle animate-spin"></span>
+
<span>Uploading</span>
+
</div>
+
</Show>
+
<Show when={!uploading()}>
+
<Button
+
onClick={uploadBlob}
+
class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
+
>
+
Upload
+
</Button>
+
</Show>
+
</div>
+
</div>
+
</div>
+
);
+
};
+87
src/components/create/handle-input.tsx
···
+
import { Handle } from "@atcute/lexicons";
+
import { createSignal, Show } from "solid-js";
+
import { resolveHandle } from "../../utils/api";
+
import { Button } from "../button.jsx";
+
import { TextInput } from "../text-input.jsx";
+
import { editorInstance } from "./state";
+
+
export const HandleInput = (props: { onClose: () => void }) => {
+
const [resolving, setResolving] = createSignal(false);
+
const [error, setError] = createSignal("");
+
let handleFormRef!: HTMLFormElement;
+
+
const resolveDid = async (e: SubmitEvent) => {
+
e.preventDefault();
+
const formData = new FormData(handleFormRef);
+
const handleValue = formData.get("handle")?.toString().trim();
+
+
if (!handleValue) {
+
setError("Please enter a handle");
+
return;
+
}
+
+
setResolving(true);
+
setError("");
+
try {
+
const did = await resolveHandle(handleValue as Handle);
+
editorInstance.view.dispatch({
+
changes: {
+
from: editorInstance.view.state.selection.main.head,
+
insert: `"${did}"`,
+
},
+
});
+
props.onClose();
+
handleFormRef.reset();
+
} catch (err: any) {
+
setError(err.message || "Failed to resolve handle");
+
} finally {
+
setResolving(false);
+
}
+
};
+
+
return (
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
+
<h2 class="mb-2 font-semibold">Insert DID from handle</h2>
+
<form ref={handleFormRef} onSubmit={resolveDid} class="flex flex-col gap-2 text-sm">
+
<div class="flex flex-col gap-1">
+
<label for="handle-input" class="select-none">
+
Handle
+
</label>
+
<TextInput id="handle-input" name="handle" placeholder="user.bsky.social" />
+
</div>
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
+
DID will be pasted after the cursor
+
</p>
+
<Show when={error()}>
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
+
</Show>
+
<div class="flex justify-between gap-2">
+
<Button
+
type="button"
+
onClick={() => {
+
props.onClose();
+
handleFormRef.reset();
+
setError("");
+
}}
+
>
+
Cancel
+
</Button>
+
<Show when={resolving()}>
+
<div class="flex items-center gap-1">
+
<span class="iconify lucide--loader-circle animate-spin"></span>
+
<span>Resolving</span>
+
</div>
+
</Show>
+
<Show when={!resolving()}>
+
<Button
+
type="submit"
+
class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
+
>
+
Insert
+
</Button>
+
</Show>
+
</div>
+
</form>
+
</div>
+
);
+
};
+12
src/components/create/menu-item.tsx
···
+
export const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => {
+
return (
+
<button
+
type="button"
+
class="flex items-center gap-2 rounded-md p-2 text-left text-xs hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
+
onClick={props.onClick}
+
>
+
<span class={`iconify ${props.icon}`}></span>
+
<span>{props.label}</span>
+
</button>
+
);
+
};
+4
src/components/create/state.ts
···
+
import { createSignal } from "solid-js";
+
+
export const editorInstance = { view: null as any };
+
export const [placeholder, setPlaceholder] = createSignal<any>();
+1 -1
src/components/editor.tsx
···
import { basicLight } from "@fsegurai/codemirror-theme-basic-light";
import { basicSetup, EditorView } from "codemirror";
import { onCleanup, onMount } from "solid-js";
-
import { editorInstance } from "./create";
+
import { editorInstance } from "./create/state";
const Editor = (props: { content: string }) => {
let editorDiv!: HTMLDivElement;
+1 -1
src/layout.tsx
···
import { AccountManager } from "./auth/account.jsx";
import { hasUserScope } from "./auth/scope-utils";
import { agent } from "./auth/state.js";
-
import { RecordEditor } from "./components/create.jsx";
+
import { RecordEditor } from "./components/create";
import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx";
import { NavBar } from "./components/navbar.jsx";
import { NotificationContainer } from "./components/notification.jsx";