Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 1 | /** |
| 2 | * The card that opens over a person's name or avatar: who they are, as | |
| 3 | * their public profile says, and how they relate to what the viewer is | |
| 4 | * looking at. Only what the viewer may see: workspaces through | |
| 5 | * `profile_workspaces` (those the viewer shares with them, and those they | |
| 6 | * made a public project in), and commits to a repository only when the | |
| 7 | * viewer may read it. Never an address. | |
| 8 | * | |
| 9 | * The body is snake_case, like every API body g1t answers with. | |
| 10 | */ | |
| 11 | import type { Contributors, Profile, ProfileWorkspace, RepoPath, Result, Viewer } from "@g1t/contracts"; | |
| 12 | ||
| 13 | /** How recently someone committed to the repository the card opened in. */ | |
| 14 | export type CommittedWithin = "day" | "week" | "month"; | |
| 15 | ||
| 16 | export type UserCard = { | |
| 17 | kind: "user"; | |
| 18 | username: string; | |
| 19 | name: string | null; | |
| 20 | pronouns: string | null; | |
| 21 | bio: string | null; | |
| 22 | location: string | null; | |
| 23 | avatar: string | null; | |
| 24 | /** Workspaces the viewer may know they belong to, at most `MAX_WORKSPACES`. */ | |
| 25 | workspaces: { slug: string; name: string; avatar: string | null }[]; | |
| 26 | /** Workspaces past those listed. */ | |
| 27 | more_workspaces: number; | |
| 28 | /** Their latest commit to the repository, by how long ago; null when none or not asked. */ | |
| 29 | committed: CommittedWithin | null; | |
| 30 | }; | |
| 31 | ||
| 32 | /** g1t itself: no profile, a card of its own. */ | |
| 33 | export type G1tCard = { kind: "g1t"; username: "g1t" }; | |
| 34 | ||
| 35 | export type Card = UserCard | G1tCard; | |
| 36 | ||
| 37 | export const MAX_WORKSPACES = 3; | |
| 38 | ||
| 39 | const DAY_MS = 86_400_000; | |
| 40 | ||
| 41 | /** `day`, `week` or `month` for a commit at `at` (RFC 3339), else null. */ | |
| 42 | export function committedWithin(at: string | null | undefined, now: number): CommittedWithin | null { | |
| 43 | if (!at) return null; | |
| 44 | const ago = now - Date.parse(at); | |
| 45 | if (!Number.isFinite(ago) || ago < -DAY_MS) return null; | |
| 46 | if (ago <= DAY_MS) return "day"; | |
| 47 | if (ago <= 7 * DAY_MS) return "week"; | |
| 48 | if (ago <= 31 * DAY_MS) return "month"; | |
| 49 | return null; | |
| 50 | } | |
| 51 | ||
| 52 | /** The line the card shows for it. */ | |
| 53 | export function committedLabel(within: CommittedWithin): string { | |
| 54 | return `Committed to this repository in the past ${within}`; | |
| 55 | } | |
| 56 | ||
| 57 | /** `owner/name` from the card's `repo` parameter, or null. */ | |
| 58 | export function parseRepo(value: string | null | undefined): RepoPath | null { | |
| 59 | const match = /^([A-Za-z0-9][A-Za-z0-9._-]{0,99})\/([A-Za-z0-9._-]{1,100})$/.exec(value?.trim() ?? ""); | |
| 60 | return match ? { namespace: match[1]!, name: match[2]! } : null; | |
| 61 | } | |
| 62 | ||
| 63 | /** What building a card reads, so tests can stand in for the services. */ | |
| 64 | export type CardSources = { | |
| 65 | profile(username: string): Promise<Profile | null>; | |
| 66 | /** The account's id, to find where it has public projects. */ | |
| 67 | accountId(username: string): Promise<string | null>; | |
| 68 | publicNamespaces(userId: string): Promise<string[]>; | |
| 69 | profileWorkspaces(username: string, viewer: Viewer, publicIn: string[]): Promise<ProfileWorkspace[]>; | |
| 70 | contributors(path: RepoPath, viewer: Viewer): Promise<Result<Contributors>>; | |
| 71 | }; | |
| 72 | ||
| 73 | /** | |
| 74 | * The card for `username` as `viewer` sees it, opened in `repo` if any; | |
| 75 | * null when there is nobody by that name (or for ghost, a deleted | |
| 76 | * account, which has no card). | |
| 77 | */ | |
| 78 | export async function buildCard( | |
| 79 | username: string, | |
| 80 | viewer: Viewer, | |
| 81 | repo: RepoPath | null, | |
| 82 | sources: CardSources, | |
| 83 | now: number, | |
| 84 | ): Promise<Card | null> { | |
| 85 | const name = username.trim().toLowerCase(); | |
| 86 | if (name === "g1t") return { kind: "g1t", username: "g1t" }; | |
| 87 | if (!/^[a-z0-9-]{1,39}$/.test(name) || name === "ghost") return null; | |
| 88 | const profile = await sources.profile(name).catch(() => null); | |
| 89 | if (!profile) return null; | |
| 90 | const workspaces = sources | |
| 91 | .accountId(profile.username) | |
| 92 | .then((id) => (id ? sources.publicNamespaces(id) : [])) | |
| 93 | .catch(() => [] as string[]) | |
| 94 | .then((publicIn) => sources.profileWorkspaces(profile.username, viewer, publicIn)) | |
| 95 | .catch(() => [] as ProfileWorkspace[]); | |
| 96 | // The repository answers for the viewer: one they cannot read says nothing. | |
| 97 | const committed = repo | |
| 98 | ? sources | |
| 99 | .contributors(repo, viewer) | |
| 100 | .then((found) => { | |
| 101 | if (!found.ok) return null; | |
| 102 | const them = found.value.contributors.find((one) => one.username?.toLowerCase() === profile.username.toLowerCase()); | |
| 103 | return committedWithin(them?.lastAt, now); | |
| 104 | }) | |
| 105 | .catch(() => null) | |
| 106 | : Promise.resolve(null); | |
| 107 | const [shown, within] = await Promise.all([workspaces, committed]); | |
| 108 | return { | |
| 109 | kind: "user", | |
| 110 | username: profile.username, | |
| 111 | name: profile.name, | |
| 112 | pronouns: profile.pronouns, | |
| 113 | bio: profile.bio, | |
| 114 | location: profile.location, | |
| 115 | avatar: profile.avatar, | |
| 116 | workspaces: shown.slice(0, MAX_WORKSPACES).map((one) => ({ slug: one.slug, name: one.name, avatar: one.avatar })), | |
| 117 | more_workspaces: Math.max(0, shown.length - MAX_WORKSPACES), | |
| 118 | committed: within, | |
| 119 | }; | |
| 120 | } | |
| 121 | ||
| 122 | /** Where the card for `username` is read from, opened in `repo` if any. */ | |
| 123 | export function cardHref(username: string, repo: string | null): string { | |
| 124 | const path = `/-/hovercard/user/${encodeURIComponent(username.toLowerCase())}`; | |
| 125 | return repo ? `${path}?repo=${encodeURIComponent(repo)}` : path; | |
| 126 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.