import type { OAuthSession } from "@atproto/oauth-client-browser"; import { useEffect, useState } from "react"; import { Avatar } from "./Avatar"; import { SCOPES } from "./config"; import { Modal } from "./Modal"; import { bare, wear } from "./oauth/profile"; import { type Repo, read } from "./oauth/records"; import type { Session } from "./oauth/useSession"; // Who you are, and nothing else: under a sign-in form an index version and a // license read as its terms. The rest is in `About`. export function Menu({ account }: { account: Session }) { const { state, signIn, signOut } = account; const [repo, setRepo] = useState(null); const [failed, setFailed] = useState(null); const [worn, setWorn] = useState(0); useEffect(() => { if (state.status !== "in") { setRepo(null); return; } let live = true; read(state.session).then( (found) => { if (live) setRepo(found); }, (error: unknown) => { if (live) setFailed(error instanceof Error ? error.message : "failed"); }, ); return () => { live = false; }; }, [state]); // Only the page-load read takes the menu away. Signing in keeps it, or the // dialog holding the form is torn down with the failure still to report. if (state.status === "restoring") return …; const session = state.status === "in" ? state.session : null; return ( {repo?.handle ?? state.session.did} {/* Keyed so wearing a new one reads it again: the trigger and the control that changed it are separate readers. */} ) : ( "Sign in" ) } actions={ session && ( ) } > {state.status === "in" ? ( setWorn((n) => n + 1)} /> ) : ( )} ); } function Account({ session, repo, failed, onWorn, }: { session: OAuthSession; repo: Repo | null; failed: string | null; onWorn: () => void; }) { const missing = repo ? SCOPES.filter((s) => !repo.granted.includes(s)) : []; return ( <>
DID
{session.did}
Server
{session.serverMetadata.issuer}
Granted
{repo ? repo.granted.join(" ") : "…"}
{missing.length > 0 && ( <>
Not granted
{missing.join(" ")}
)}
Records
{repo ? repo.counts.length > 0 ? repo.counts .map( (c) => `${c.collection.slice(12)} ${c.records}${c.more ? "+" : ""}`, ) .join(", ") : "none yet" : "…"}
{failed &&

{failed}

} ); } // Shrunk before it goes up, so a phone camera's output does not become what // every page load reads back. function Picture({ session, onWorn, }: { session: OAuthSession; onWorn: () => void; }) { const [busy, setBusy] = useState(false); const [failed, setFailed] = useState(null); const run = (what: () => Promise) => { setBusy(true); setFailed(null); what().then( () => { setBusy(false); onWorn(); }, (error: unknown) => { setBusy(false); setFailed(error instanceof Error ? error.message : "failed"); }, ); }; return ( <>

{failed &&

{failed}

} ); } function SignIn({ signIn, error, going, }: { signIn: (handle: string) => Promise; error?: string; going: boolean; }) { const [handle, setHandle] = useState(""); return ( <>
{ event.preventDefault(); void signIn(handle); }} > setHandle(event.target.value)} autoComplete="username" spellCheck={false} />
{error &&

{error}

} {/* A new tab, unlike every other link here: this one is mid-flow, and a handle has to be carried back to the form behind it. */}

No account? Sign up with{" "} Bluesky {" "} or{" "} Tangled .

Records are written to your own PDS, never ours.

); }