~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
120 lines · 3.5 KB Raw History
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 || '');
}