RegistryAccount

tcmts.registry.pages.RegistryAccount · #infwf2nu · term · runs in the browser

uses useState

Who's signed in, in the registry's header. Nobody: "Sign in", which opens a dialog asking for their handle (React Aria's Form and TextField, as the forge's sign-in is) and sends the browser to sign in. Someone: their picture, which opens a menu (as the forge's is): who it is, their page, what's theirs in the feed, and signing out. Coming back from signing in, it finishes it.

import { handleOfDid, said, whoHref, whoIs } from "#codebase/tcmts/registry";
import type { Someone } from "#codebase/tcmts/registry";
import { Initial } from "#codebase/tcmts/registry/pages";
import { finishSignIn, registrySession, registrySignOut, startSignIn } from "#codebase/tcmts/registry/session";
import { at } from "#codebase/tcmts/share";
import { useEffect, useState } from "react";
import {
  Button, Dialog, DialogTrigger, FieldError, Form, Header, Heading, Input, Label, Menu, MenuItem, MenuSection,
  MenuTrigger, Popover, TextField,
} from "react-aria-components";

export function RegistryAccount() {
  const [state, setState] = useState<{ at: "checking" | "out" | "in"; me?: Someone }>({
    at: "checking",
  });
  const [open, setOpen] = useState(false);
  const [handle, setHandle] = useState("");
  const [problem, setProblem] = useState("");
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    void (async () => {
      try {
        if (await finishSignIn()) return;
        const s = await registrySession();
        if (!s) return setState({ at: "out" });
        // (their picture and name, if the registry knows them; their handle, from their DID document if not)
        const me = (await whoIs(s.did).catch(() => undefined)) ?? { did: s.did };
        const named = me.handle ?? (await handleOfDid(s.did));
        setState({ at: "in", me: named === s.did ? me : { ...me, handle: named } });
      } catch (e) {
        setState({ at: "out" });
        setProblem(`Signing in didn't work: ${(e as Error).message}`);
        setOpen(true);
      }
    })();
  }, []);
  if (state.at === "checking") return <span className="account" />;
  const me = state.me;
  if (state.at === "in" && me)
    return (
      <span className="account">
        <MenuTrigger>
          <Button className="avatar-btn" aria-label={`Signed in as ${said(me)}`}>
            <Initial who={me} />
          </Button>
          <Popover className="menu-pop" placement="bottom end" offset={6}>
            <Menu
              className="menu"
              aria-label="Account"
              onAction={(k) => {
                if (k === "out") void registrySignOut();
              }}
            >
              <MenuSection>
                <Header className="menu-head">
                  Signed in as <b>{said(me)}</b>
                </Header>
                <MenuItem id="profile" href={whoHref(me)} className="opt">
                  Your page
                </MenuItem>
                <MenuItem id="following" href={at("/?show=following")} className="opt">
                  People I follow
                </MenuItem>
                <MenuItem id="mine" href={at("/?show=mine")} className="opt">
                  Libraries I use
                </MenuItem>
              </MenuSection>
              <MenuSection className="menu-end">
                <MenuItem id="out" className="opt signout">
                  Sign out
                </MenuItem>
              </MenuSection>
            </Menu>
          </Popover>
        </MenuTrigger>
      </span>
    );
  return (
    <span className="account">
      <DialogTrigger isOpen={open} onOpenChange={setOpen}>
        <Button className="btn sky">Sign in</Button>
        <Popover className="menu-pop signin-pop" placement="bottom end" offset={6}>
          <Dialog className="getit-body">
            <Heading slot="title" className="getit-title">
              Sign in with your handle
            </Heading>
            <p className="muted">
              Your Bluesky handle, or any AT Protocol account's. Your own server signs you in; the
              registry asks only to write your stars.
            </p>
            <Form
              className="signin-form"
              onSubmit={(e) => {
                e.preventDefault();
                setBusy(true);
                startSignIn(handle).catch((err: Error) => {
                  setProblem(err.message);
                  setBusy(false);
                });
              }}
            >
              <TextField
                className="textfield"
                name="handle"
                autoComplete="username"
                isRequired
                value={handle}
                onChange={(v) => (setHandle(v), setProblem(""))}
                isInvalid={!!problem}
              >
                <Label>Handle</Label>
                <Input className="field" placeholder="you.bsky.social" />
                <FieldError className="field-error">{problem}</FieldError>
              </TextField>
              <Button type="submit" className="btn sky" isPending={busy}>
                {busy ? "Signing in…" : "Continue"}
              </Button>
            </Form>
          </Dialog>
        </Popover>
      </DialogTrigger>
    </span>
  );
}

uses

used by