/** * A link's card: what a row cannot hold. The header is the row's line (icon * and title as the link; a ring or faces in the corner), then the key and * what it is in, its summary, the items inside it, the newest thing said in * it, and a footer: the pickers and the conversation count, or who and when. * Words a person wrote draw as themselves, with their face and the time. * * While it loads it holds the card's own height, so what is around it does * not jump; not available is one line. */ import { Avatar } from './Avatar' import { AvatarGroup } from './AvatarGroup' import { Card } from './Card' import { Link } from './Link' import type { Linked, LinkedKind, LinkState, LoadingShape } from './linked' import { CommentCount, LinkedIcon, LinkKey, LinkPickers, ProgressRing, StatusMark } from './LinkParts' import { LinkUnavailable } from './LinkRow' import { SkeletonBar } from './Skeleton' export interface LinkCardProps { link: LinkState /** The values read as words, never pickers: the hover preview. */ readOnly?: boolean /** No frame of its own: inside a box that draws one, as the hover preview's. */ bare?: boolean } export function LinkCard({ link, readOnly = false, bare = false }: LinkCardProps) { if (link.state === 'loading') return if (link.state === 'unavailable') return const { linked } = link const body = linked.kind === 'message' && linked.author ? : if (bare) return
{body}
return ( {body} ) } function CardBody({ linked, readOnly }: { linked: Linked; readOnly: boolean }) { return ( <>
{/* The icon opens it too, so the target is larger than the words. */} {linked.title} {linked.done && } {linked.members && }
{(linked.key || linked.subtitle) &&

{[linked.key, linked.subtitle].filter(Boolean).join(' ยท ')}

} {linked.summary &&

{linked.summary}

} {linked.children && linked.children.length > 0 && (
{linked.children.map((child) => (
{/* The status is the row's icon: a list needs no kind icon on every row. */} {child.key && {child.key}} {/* Each opens on its own; the link covers its row. */} {child.title} {child.owner ? : }
))}
)} {linked.latest && (
{/* "Latest" in words: the card follows it, so this is not necessarily what the link was sent about. */} Latest conversation
{linked.latest.author.name} {linked.latest.at}

{linked.latest.body}

)}
{(linked.status || linked.owner) && } {/* Faces only: names would not fit once several people wrote it. */} {linked.authors ? ( {linked.updated && {linked.updated}} ) : ( !linked.status && !linked.owner && linked.updated && Updated {linked.updated} )} {linked.conversation && ( )}
) } /** Words a person wrote, linked: their face, their name, the time as the link, and the words. */ function SaidBody({ linked }: { linked: Linked }) { const author = linked.author! return ( <>
{author.name} {linked.updated ?? 'Open'}

{linked.body ?? linked.title}

) } /** Each kind's usual card, for a placeholder when the app remembers nothing of it. */ const KIND_SHAPE: Record = { issue: { subtitle: true, summary: true, footer: 'pickers' }, project: { subtitle: false, summary: true, rows: 3, ring: true, footer: 'pickers' }, topic: { summary: true, latest: true, footer: 'line' }, page: { subtitle: true, summary: true, footer: 'people' }, file: { summary: false, footer: 'line' }, message: { summary: true, footer: 'none' }, } export interface LinkCardLoadingProps { kind: LinkedKind /** What the card will hold (`loadingShapeOf` from the last time it drew); the kind's usual card otherwise. */ shape?: LoadingShape } /** The card it will become, drawn in bars at the card's own height. */ export function LinkCardLoading({ kind, shape }: LinkCardLoadingProps) { const { subtitle = false, summary = true, rows = 0, latest = false, ring = false, footer = 'line' } = shape ?? KIND_SHAPE[kind] return (
{ring && }
{subtitle && (
)} {summary && (
)} {rows > 0 && (
{Array.from({ length: rows }, (_, i) => (
))}
)} {latest && (
)} {footer !== 'none' && (
{footer === 'pickers' ? ( <> ) : footer === 'people' ? ( <> ) : ( )}
)}
) }