/** * The mark for whoever said something. * * Shape carries kind: a person is a rounded square, the agent a circle. That * has to hold everywhere at once, because the same person appears in a chat * entry, in the presence stack and in the header — drawn separately per surface * it becomes one person with two shapes on one screen, which is exactly how the * two copies this replaces came to disagree. So there is one of each here and * the size is the only thing a caller chooses, set by the row it sits in. * * The photograph takes the same shape as the fallback. If a loaded face were * round and a failed one square, the shape would report whether github.com * answered rather than who is speaking — and a person with a photograph would * be the same shape as the agent. * * Until the photograph arrives, and for good if it never does, the mark is a * square tinted with the person's cursor colour and carrying their initial, so a * dead URL never shows the browser's broken-image glyph. */ import { useState } from "react"; import { color } from "./cursor"; /** Retina-sharp at the rendered size. */ function photograph(handle: string, size: number): string { return `https://github.com/${encodeURIComponent(handle)}.png?size=${size * 2}`; } export type FaceProps = { /** Unverified, so the photograph may not exist. */ handle: string; size?: number; /** Compact chrome can choose the smaller fallback initial without changing the face size. */ textSize?: "xs" | "2xs"; /** The surface behind overlapping faces, so their cover ring does not show. */ ring?: "ground" | "page"; /** Set false where a design-system tooltip already names the face. */ titled?: boolean; /** Set true where the name is written beside the face, so it is not read twice. */ decorative?: boolean; }; export const FACE_RING_CLASS = { ground: "ring-2 ring-ground", page: "ring-2 ring-page", } as const; export const FACE_RADIUS_CLASS = { small: "rounded-sm", regular: "rounded-md", } as const; /** Small faces need a smaller corner than the default. */ export function faceCorner(size: number): keyof typeof FACE_RADIUS_CLASS { return size <= 18 ? "small" : "regular"; } /** Keyed by handle so a reused mount never carries one person's load state to another. */ export function Face({ decorative, handle, ring, size, textSize, titled }: FaceProps) { return ( ); } function Portrait( { decorative, handle, ring, size = 20, textSize = "xs", titled = true }: FaceProps, ) { let [failed, setFailed] = useState(false); let [loaded, setLoaded] = useState(false); let tone = color(handle); let edge = `${FACE_RADIUS_CLASS[faceCorner(size)]} ${ring ? FACE_RING_CLASS[ring] : ""}`; return ( {!loaded && } {!failed && ( setFailed(true)} onLoad={() => setLoaded(true)} referrerPolicy="no-referrer" src={photograph(handle, size)} /> )} ); } /** The agent, which has no photograph and never will. */ export function AgentFace({ ring, size = 20 }: { ring?: boolean; size?: number }) { return ( ); }