chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQuery } from '@tanstack/react-query';
import { ShieldAlert } from 'lucide-react';
import { Copyable } from '#/components/molecules/copyable.jsx';
import { shortDid } from '#/lib/format.js';
import { identityOf } from '#/lib/identity.js';
import { cn } from '#/lib/utils.js';
/**
* Who a DID is, resolved once per DID for as long as the tab is open. Several
* screens name the same few accounts, and one query key serves all of them.
* @param {string} did
*/
function useIdentity(did) {
return useQuery({
queryKey: ['identity', did],
queryFn: () => identityOf(did),
retry: false,
staleTime: Number.POSITIVE_INFINITY,
enabled: Boolean(did),
});
}
/**
* What a hover says about a handle: the DID it stands for, and whether the two
* agree.
* @param {string} did @param {boolean} verified
* @returns {string}
*/
function verificationNote(did, verified) {
return `${did}\n${
verified
? 'This handle resolves back to this DID.'
: 'Claimed by the DID document but not confirmed.'
}`;
}
/**
* A DID, shown as whoever holds it.
*
* The DID is what the protocol addresses and the handle is what a reader knows,
* so the handle is the label and the DID is what the click copies. A handle the
* document claims but which does not resolve back is marked: anybody may write
* any handle into their own DID document, and an unconfirmed one shown plainly
* is somebody else's name on this page.
*/
export function Identity({ did, className, inline = false }) {
const { data, isPending } = useIdentity(did);
if (!did) return null;
// The DID until the lookup settles, so the line does not reflow under a
// reader who is already looking at it.
if (isPending || !data?.handle) {
return inline ? (
<span className={cn('font-mono text-[0.95em]', className)}>
{shortDid(did)}
</span>
) : (
<Copyable value={did} className={className}>
{shortDid(did)}
</Copyable>
);
}
// Inside a sentence a handle is a name, so it is set in the surrounding face
// and carries no copy affordance: that control keeps its width whether or not
// it is showing, which in running text is a hole between the name and the
// word after it.
if (inline) {
return (
<span
title={verificationNote(did, data.verified)}
className={cn(
'font-semibold',
!data.verified && 'text-muted-foreground italic',
className,
)}
>
{data.handle}
{!data.verified && (
<ShieldAlert
className="ml-1 inline size-3 shrink-0 align-[-0.1em] text-warning"
aria-label="unconfirmed handle"
/>
)}
</span>
);
}
return (
<span className="inline-flex min-w-0 items-baseline gap-1.5">
<Copyable
value={did}
title={verificationNote(did, data.verified)}
className={cn(
!data.verified && 'text-muted-foreground italic',
className,
)}
>
{data.handle}
</Copyable>
{!data.verified && (
<ShieldAlert
className="size-3 shrink-0 self-center text-warning"
aria-label="unconfirmed handle"
/>
)}
</span>
);
}
/**
* The same identity as a bare string, for a heading that reads as a sentence.
* Falls back to the shortened DID, so it never renders empty.
* @param {string} did
* @returns {string}
*/
export function useIdentityLabel(did) {
const { data } = useIdentity(did);
return data?.handle || shortDid(did || '');
}