atproto explorer pdsls.dev
atproto tool

add label query

Changed files
+204 -9
src
+19
src/components/navbar.tsx
···
export const [pds, setPDS] = createSignal<string>();
export const [cid, setCID] = createSignal<string>();
export const [validRecord, setValidRecord] = createSignal<boolean | undefined>(
undefined,
);
···
inactiveClass="text-lightblue-500 w-full hover:underline"
>
blobs
</A>
</div>
</Show>
···
export const [pds, setPDS] = createSignal<string>();
export const [cid, setCID] = createSignal<string>();
+
export const [isLabeler, setIsLabeler] = createSignal(false);
export const [validRecord, setValidRecord] = createSignal<boolean | undefined>(
undefined,
);
···
inactiveClass="text-lightblue-500 w-full hover:underline"
>
blobs
+
</A>
+
</div>
+
</Show>
+
<Show
+
when={
+
isLabeler() && !props.params.collection && !props.params.rkey
+
}
+
>
+
<div class="mt-1 flex items-center">
+
<Tooltip text="Labels">
+
<div class="i-mdi-tag-outline mr-1 text-sm" />
+
</Tooltip>
+
<A
+
end
+
href={`at/${props.params.repo}/labels`}
+
inactiveClass="text-lightblue-500 w-full hover:underline"
+
>
+
labels
</A>
</div>
</Show>
+2
src/index.tsx
···
import { BlobView } from "./views/blob.tsx";
import { CollectionView } from "./views/collection.tsx";
import { RecordView } from "./views/record.tsx";
render(
() => (
···
<Route path="/:pds" component={PdsView} />
<Route path="/:pds/:repo" component={RepoView} />
<Route path="/:pds/:repo/blobs" component={BlobView} />
<Route path="/:pds/:repo/:collection" component={CollectionView} />
<Route path="/:pds/:repo/:collection/:rkey" component={RecordView} />
</Router>
···
import { BlobView } from "./views/blob.tsx";
import { CollectionView } from "./views/collection.tsx";
import { RecordView } from "./views/record.tsx";
+
import { LabelView } from "./views/labels.tsx";
render(
() => (
···
<Route path="/:pds" component={PdsView} />
<Route path="/:pds/:repo" component={RepoView} />
<Route path="/:pds/:repo/blobs" component={BlobView} />
+
<Route path="/:pds/:repo/labels" component={LabelView} />
<Route path="/:pds/:repo/:collection" component={CollectionView} />
<Route path="/:pds/:repo/:collection/:rkey" component={RecordView} />
</Router>
+12
src/styles/icons.css
···
width: 1.2em;
height: 1.2em;
}
···
width: 1.2em;
height: 1.2em;
}
+
+
.i-mdi-tag-outline {
+
--un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m21.41 11.58l-9-9A2 2 0 0 0 11 2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 .59 1.42l9 9A2 2 0 0 0 13 22a2 2 0 0 0 1.41-.59l7-7A2 2 0 0 0 22 13a2 2 0 0 0-.59-1.42M13 20l-9-9V4h7l9 9M6.5 5A1.5 1.5 0 1 1 5 6.5A1.5 1.5 0 0 1 6.5 5'/%3E%3C/svg%3E");
+
-webkit-mask: var(--un-icon) no-repeat;
+
mask: var(--un-icon) no-repeat;
+
-webkit-mask-size: 100% 100%;
+
mask-size: 100% 100%;
+
background-color: currentColor;
+
color: inherit;
+
width: 1.2em;
+
height: 1.2em;
+
}
+8 -3
src/utils/api.ts
···
import { CredentialManager, XRPC } from "@atcute/client";
import { query } from "@solidjs/router";
-
import { setPDS } from "../components/navbar";
import { DidDocument } from "@atcute/client/utils/did";
const didPDSCache: Record<string, string> = {};
const didDocCache: Record<string, DidDocument> = {};
const getPDS = query(async (did: string) => {
if (did in didPDSCache) return didPDSCache[did];
···
if (service.id === "#atproto_pds") {
didPDSCache[did] = service.serviceEndpoint.toString();
didDocCache[did] = doc;
-
return service.serviceEndpoint.toString();
}
}
});
}, "getPDS");
···
return pds;
};
-
export { getPDS, didDocCache, resolveHandle, resolvePDS };
···
import { CredentialManager, XRPC } from "@atcute/client";
import { query } from "@solidjs/router";
+
import { setIsLabeler, setPDS } from "../components/navbar";
import { DidDocument } from "@atcute/client/utils/did";
const didPDSCache: Record<string, string> = {};
+
const labelerCache: Record<string, string> = {};
const didDocCache: Record<string, DidDocument> = {};
const getPDS = query(async (did: string) => {
if (did in didPDSCache) return didPDSCache[did];
···
if (service.id === "#atproto_pds") {
didPDSCache[did] = service.serviceEndpoint.toString();
didDocCache[did] = doc;
+
}
+
if (service.id === "#atproto_labeler") {
+
labelerCache[did] = service.serviceEndpoint.toString();
+
setIsLabeler(true);
}
}
+
return didPDSCache[did];
});
}, "getPDS");
···
return pds;
};
+
export { getPDS, labelerCache, didDocCache, resolveHandle, resolvePDS };
+7
src/utils/date.ts
···
···
+
const getDateFromTimestamp = (timestamp: number) =>
+
new Date(timestamp - new Date().getTimezoneOffset() * 60 * 1000)
+
.toISOString()
+
.split(".")[0]
+
.replace("T", " ");
+
+
export { getDateFromTimestamp };
+1 -6
src/views/collection.tsx
···
import { agent, loginState } from "../components/login.jsx";
import { createStore } from "solid-js/store";
import Tooltip from "../components/tooltip.jsx";
interface AtprotoRecord {
rkey: string;
···
const isOverflowing = (elem: HTMLElement, previewHeight: number) =>
elem.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight;
-
-
const getDateFromTimestamp = (timestamp: number) =>
-
new Date(timestamp - new Date().getTimezoneOffset() * 60 * 1000)
-
.toISOString()
-
.split(".")[0]
-
.replace("T", " ");
return (
<span
···
import { agent, loginState } from "../components/login.jsx";
import { createStore } from "solid-js/store";
import Tooltip from "../components/tooltip.jsx";
+
import { getDateFromTimestamp } from "../utils/date.js";
interface AtprotoRecord {
rkey: string;
···
const isOverflowing = (elem: HTMLElement, previewHeight: number) =>
elem.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight;
return (
<span
+155
src/views/labels.tsx
···
···
+
import { createResource, createSignal, For, onMount, Show } from "solid-js";
+
import { CredentialManager, XRPC } from "@atcute/client";
+
import { useParams } from "@solidjs/router";
+
import { labelerCache, resolvePDS } from "../utils/api.js";
+
import { ComAtprotoLabelDefs } from "@atcute/client/lexicons";
+
import { getDateFromTimestamp } from "../utils/date.js";
+
+
const LabelView = () => {
+
const params = useParams();
+
const [cursor, setCursor] = createSignal<string>();
+
const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]);
+
const [uriPatterns, setUriPatterns] = createSignal<string>();
+
const did = params.repo;
+
let rpc: XRPC;
+
+
onMount(async () => {
+
await resolvePDS(did);
+
rpc = new XRPC({
+
handler: new CredentialManager({ service: labelerCache[did] }),
+
});
+
});
+
+
const fetchLabels = async () => {
+
const res = await rpc.get("com.atproto.label.queryLabels", {
+
params: {
+
uriPatterns: uriPatterns()!.split(","),
+
sources: [did as `did:${string}`],
+
cursor: cursor(),
+
},
+
});
+
setCursor(res.data.labels.length < 50 ? undefined : res.data.cursor);
+
setLabels(labels().concat(res.data.labels) ?? res.data.labels);
+
return res.data.labels;
+
};
+
+
const [response, { refetch }] = createResource(uriPatterns, fetchLabels);
+
+
return (
+
<>
+
<div class="z-5 dark:bg-dark-700 sticky top-0 flex w-full flex-col items-center justify-center gap-2 bg-slate-100 py-4">
+
<form
+
class="flex flex-col items-center gap-y-1"
+
onsubmit={(e) => e.preventDefault()}
+
>
+
<div class="w-full">
+
<label for="patterns" class="ml-0.5 text-sm">
+
URI Patterns (comma-separated)
+
</label>
+
</div>
+
<div class="flex items-center gap-x-2">
+
<textarea
+
id="patterns"
+
name="patterns"
+
spellcheck={false}
+
rows={3}
+
cols={25}
+
class="dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300"
+
/>
+
<button
+
onclick={() =>
+
setUriPatterns(
+
(document.getElementById("patterns") as HTMLInputElement)
+
.value,
+
)
+
}
+
type="submit"
+
class="dark:bg-dark-700 dark:hover:bg-dark-800 rounded-lg border border-gray-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-gray-300"
+
>
+
Get
+
</button>
+
</div>
+
</form>
+
<div class="flex items-center gap-x-2">
+
<Show when={labels().length}>
+
<div>
+
<span>
+
{labels().length} label{labels().length > 1 ? "s" : ""}
+
</span>
+
</div>
+
</Show>
+
<Show when={cursor()}>
+
<div class="flex h-[2rem] w-[5.5rem] items-center justify-center text-nowrap">
+
<Show when={!response.loading}>
+
<button
+
type="button"
+
onclick={() => refetch()}
+
class="dark:bg-dark-700 dark:hover:bg-dark-800 rounded-lg border border-gray-400 bg-white px-2 py-1.5 text-sm font-bold hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-gray-300"
+
>
+
Load More
+
</button>
+
</Show>
+
<Show when={response.loading}>
+
<div class="i-line-md-loading-twotone-loop text-xl"></div>
+
</Show>
+
</div>
+
</Show>
+
</div>
+
</div>
+
<div class="break-anywhere flex flex-col gap-2 divide-y divide-neutral-500 whitespace-pre-wrap font-mono">
+
<For each={labels()}>
+
{(label) => (
+
<div class="flex flex-col gap-x-2 pt-2">
+
<div class="flex gap-x-2">
+
<div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400">
+
URI
+
</div>
+
{label.uri}
+
</div>
+
<Show when={label.cid}>
+
<div class="flex gap-x-2">
+
<div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400">
+
CID
+
</div>
+
{label.cid}
+
</div>
+
</Show>
+
<div class="flex gap-x-2">
+
<div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400">
+
Label
+
</div>
+
{label.val}
+
</div>
+
<Show when={label.neg}>
+
<div class="flex gap-x-2">
+
<div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400">
+
Negated
+
</div>
+
{label.neg}
+
</div>
+
</Show>
+
<div class="flex gap-x-2">
+
<div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400">
+
Created
+
</div>
+
{getDateFromTimestamp(new Date(label.cts).getTime())}
+
</div>
+
<Show when={label.exp}>
+
{(exp) => (
+
<div class="flex gap-x-2">
+
<div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400">
+
Expires
+
</div>
+
{getDateFromTimestamp(new Date(exp()).getTime())}
+
</div>
+
)}
+
</Show>
+
</div>
+
)}
+
</For>
+
</div>
+
</>
+
);
+
};
+
+
export { LabelView };