import { ComAtprotoLabelDefs } from "@atcute/atproto"; import { Client, simpleFetchHandler } from "@atcute/client"; import { isAtprotoDid } from "@atcute/identity"; import { Handle } from "@atcute/lexicons"; import { A, useSearchParams } from "@solidjs/router"; import { createMemo, createSignal, For, onMount, Show } from "solid-js"; import { Button } from "../components/button.jsx"; import { StickyOverlay } from "../components/sticky.jsx"; import { TextInput } from "../components/text-input.jsx"; import { labelerCache, resolveHandle, resolvePDS } from "../utils/api.js"; import { localDateFromTimestamp } from "../utils/date.js"; const LABELS_PER_PAGE = 50; const LabelCard = (props: { label: ComAtprotoLabelDefs.Label }) => { const label = props.label; return (
{label.val} negated
{localDateFromTimestamp(new Date(label.cts).getTime())} {(exp) => (
{localDateFromTimestamp(new Date(exp()).getTime())}
)}
{label.uri}
{label.cid}
); }; export const LabelView = () => { const [searchParams, setSearchParams] = useSearchParams(); const [cursor, setCursor] = createSignal(); const [labels, setLabels] = createSignal([]); const [filter, setFilter] = createSignal(""); const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(); const [didInput, setDidInput] = createSignal(searchParams.did ?? ""); let rpc: Client | undefined; let formRef!: HTMLFormElement; const filteredLabels = createMemo(() => { const filterValue = filter().trim(); if (!filterValue) return labels(); const filters = filterValue .split(/[\s,]+/) .map((f) => f.trim()) .filter((f) => f.length > 0); const exclusions: { pattern: string; hasWildcard: boolean }[] = []; const inclusions: { pattern: string; hasWildcard: boolean }[] = []; filters.forEach((f) => { if (f.startsWith("-")) { const lower = f.slice(1).toLowerCase(); exclusions.push({ pattern: lower, hasWildcard: lower.includes("*"), }); } else { const lower = f.toLowerCase(); inclusions.push({ pattern: lower, hasWildcard: lower.includes("*"), }); } }); const matchesPattern = (value: string, filter: { pattern: string; hasWildcard: boolean }) => { if (filter.hasWildcard) { // Convert wildcard pattern to regex const regexPattern = filter.pattern .replace(/[.+?^${}()|[\]\\]/g, "\\$&") // Escape special regex chars except * .replace(/\*/g, ".*"); // Replace * with .* const regex = new RegExp(`^${regexPattern}$`); return regex.test(value); } else { return value === filter.pattern; } }; return labels().filter((label) => { const labelValue = label.val.toLowerCase(); if (exclusions.some((exc) => matchesPattern(labelValue, exc))) { return false; } // If there are inclusions, at least one must match if (inclusions.length > 0) { return inclusions.some((inc) => matchesPattern(labelValue, inc)); } // If only exclusions were specified, include everything not excluded return true; }); }); const hasSearched = createMemo(() => Boolean(searchParams.uriPatterns)); onMount(async () => { if (searchParams.did && searchParams.uriPatterns) { const formData = new FormData(); formData.append("did", searchParams.did.toString()); formData.append("uriPatterns", searchParams.uriPatterns.toString()); await fetchLabels(formData); } }); const fetchLabels = async (formData: FormData, reset?: boolean) => { let did = formData.get("did")?.toString()?.trim(); const uriPatterns = formData.get("uriPatterns")?.toString()?.trim(); if (!did || !uriPatterns) { setError("Please provide both DID and URI patterns"); return; } if (reset) { setLabels([]); setCursor(undefined); setError(undefined); } try { setLoading(true); setError(undefined); if (!isAtprotoDid(did)) did = await resolveHandle(did as Handle); await resolvePDS(did); if (!labelerCache[did]) throw new Error("Repository is not a labeler"); rpc = new Client({ handler: simpleFetchHandler({ service: labelerCache[did] }), }); setSearchParams({ did, uriPatterns }); setDidInput(did); const res = await rpc.get("com.atproto.label.queryLabels", { params: { uriPatterns: uriPatterns.split(",").map((p) => p.trim()), sources: [did as `did:${string}:${string}`], cursor: cursor(), }, }); if (!res.ok) throw new Error(res.data.error || "Failed to fetch labels"); const newLabels = res.data.labels || []; setCursor(newLabels.length < LABELS_PER_PAGE ? undefined : res.data.cursor); setLabels(reset ? newLabels : [...labels(), ...newLabels]); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); console.error("Failed to fetch labels:", err); } finally { setLoading(false); } }; const handleSearch = () => { fetchLabels(new FormData(formRef), true); }; const handleLoadMore = () => { fetchLabels(new FormData(formRef)); }; return (
{ e.preventDefault(); handleSearch(); }} >