Skip to content
133 linesCodeBlameRaw

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 */
11import type { Contributors, Profile, ProfileWorkspace, RepoPath, Result, Viewer } from "@g1t/contracts";
12
13/** How recently someone committed to the repository the card opened in. */
14export type CommittedWithin = "day" | "week" | "month";
15
16export type UserCard = {
17 kind: "user";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers18 /** Lowercased: what links and lookups use. */
Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016)19 username: string;
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers20 /** The username as its owner wrote it, when that differs: what the card shows. */
21 display_username?: string | null;
Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016)22 name: string | null;
23 pronouns: string | null;
24 bio: string | null;
25 location: string | null;
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)26 /** The IANA time zone they gave, such as `America/Denver`; the card shows their local time from it. */
27 timezone: string | null;
Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016)28 avatar: string | null;
29 /** Workspaces the viewer may know they belong to, at most `MAX_WORKSPACES`. */
30 workspaces: { slug: string; name: string; avatar: string | null }[];
31 /** Workspaces past those listed. */
32 more_workspaces: number;
33 /** Their latest commit to the repository, by how long ago; null when none or not asked. */
34 committed: CommittedWithin | null;
35};
36
37/** g1t itself: no profile, a card of its own. */
38export type G1tCard = { kind: "g1t"; username: "g1t" };
39
40export type Card = UserCard | G1tCard;
41
42export const MAX_WORKSPACES = 3;
43
44const DAY_MS = 86_400_000;
45
46/** `day`, `week` or `month` for a commit at `at` (RFC 3339), else null. */
47export function committedWithin(at: string | null | undefined, now: number): CommittedWithin | null {
48 if (!at) return null;
49 const ago = now - Date.parse(at);
50 if (!Number.isFinite(ago) || ago < -DAY_MS) return null;
51 if (ago <= DAY_MS) return "day";
52 if (ago <= 7 * DAY_MS) return "week";
53 if (ago <= 31 * DAY_MS) return "month";
54 return null;
55}
56
57/** The line the card shows for it. */
58export function committedLabel(within: CommittedWithin): string {
59 return `Committed to this repository in the past ${within}`;
60}
61
62/** `owner/name` from the card's `repo` parameter, or null. */
63export function parseRepo(value: string | null | undefined): RepoPath | null {
64 const match = /^([A-Za-z0-9][A-Za-z0-9._-]{0,99})\/([A-Za-z0-9._-]{1,100})$/.exec(value?.trim() ?? "");
65 return match ? { namespace: match[1]!, name: match[2]! } : null;
66}
67
68/** What building a card reads, so tests can stand in for the services. */
69export type CardSources = {
70 profile(username: string): Promise<Profile | null>;
71 /** The account's id, to find where it has public projects. */
72 accountId(username: string): Promise<string | null>;
73 publicNamespaces(userId: string): Promise<string[]>;
74 profileWorkspaces(username: string, viewer: Viewer, publicIn: string[]): Promise<ProfileWorkspace[]>;
75 contributors(path: RepoPath, viewer: Viewer): Promise<Result<Contributors>>;
76};
77
78/**
79 * The card for `username` as `viewer` sees it, opened in `repo` if any;
80 * null when there is nobody by that name (or for ghost, a deleted
81 * account, which has no card).
82 */
83export async function buildCard(
84 username: string,
85 viewer: Viewer,
86 repo: RepoPath | null,
87 sources: CardSources,
88 now: number,
89): Promise<Card | null> {
90 const name = username.trim().toLowerCase();
91 if (name === "g1t") return { kind: "g1t", username: "g1t" };
92 if (!/^[a-z0-9-]{1,39}$/.test(name) || name === "ghost") return null;
93 const profile = await sources.profile(name).catch(() => null);
94 if (!profile) return null;
95 const workspaces = sources
96 .accountId(profile.username)
97 .then((id) => (id ? sources.publicNamespaces(id) : []))
98 .catch(() => [] as string[])
99 .then((publicIn) => sources.profileWorkspaces(profile.username, viewer, publicIn))
100 .catch(() => [] as ProfileWorkspace[]);
101 // The repository answers for the viewer: one they cannot read says nothing.
102 const committed = repo
103 ? sources
104 .contributors(repo, viewer)
105 .then((found) => {
106 if (!found.ok) return null;
107 const them = found.value.contributors.find((one) => one.username?.toLowerCase() === profile.username.toLowerCase());
108 return committedWithin(them?.lastAt, now);
109 })
110 .catch(() => null)
111 : Promise.resolve(null);
112 const [shown, within] = await Promise.all([workspaces, committed]);
113 return {
114 kind: "user",
115 username: profile.username,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers116 ...(profile.displayUsername ? { display_username: profile.displayUsername } : {}),
Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016)117 name: profile.name,
118 pronouns: profile.pronouns,
119 bio: profile.bio,
120 location: profile.location,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)121 timezone: profile.timezone ?? null,
Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016)122 avatar: profile.avatar,
123 workspaces: shown.slice(0, MAX_WORKSPACES).map((one) => ({ slug: one.slug, name: one.name, avatar: one.avatar })),
124 more_workspaces: Math.max(0, shown.length - MAX_WORKSPACES),
125 committed: within,
126 };
127}
128
129/** Where the card for `username` is read from, opened in `repo` if any. */
130export function cardHref(username: string, repo: string | null): string {
131 const path = `/-/hovercard/user/${encodeURIComponent(username.toLowerCase())}`;
132 return repo ? `${path}?repo=${encodeURIComponent(repo)}` : path;
133}

This file's history is long; its oldest lines are credited to the oldest commit read.