Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

71 linesCodeBlameRaw
1import { type ChatSidebar, type Principal, type User, shownUsername } from "@g1t/contracts";
2
3import type { Mentionable } from "./chat";
4import { chat, identity } from "./services.server";
5
6/**
7 * Who can be talked to in a workspace: its people, for mentions and for
8 * starting a direct message. Empty, not an error, when identity does not
9 * answer.
10 */
11export async function workspacePeople(slug: string, viewer: User): Promise<Mentionable[]> {
12 const members = await identity.listMembers(slug, viewer).catch(() => null);
13 return members?.ok
14 ? members.value.map((member) => ({
15 kind: "user" as const,
16 name: member.username,
17 display_username: member.display_username ?? null,
18 // As chat names them (`memberName`): display name, else the username as they wrote it.
19 display_name: member.name?.trim() || shownUsername(member),
20 avatar: member.avatar ?? null,
21 }))
22 : [];
23}
24
25/** The sidebar, or null when chat does not answer. */
26export async function sidebarOrNull(slug: string, viewer: User): Promise<ChatSidebar | null> {
27 try {
28 const result = await chat.sidebar(slug, viewer);
29 return result.ok ? result.value : null;
30 } catch {
31 return null;
32 }
33}
34
35/**
36 * Members as a form sends them, `user:<username>`, as the chat service
37 * names them: a person by their account id. Anyone who is not a member of
38 * the workspace is left out, and so is anything that is not a person.
39 */
40export async function principalsFrom(slug: string, viewer: User, fields: string[]): Promise<Principal[]> {
41 const usernames = fields.filter((f) => f.startsWith("user:")).map((f) => f.slice(5).toLowerCase());
42 const members = usernames.length > 0 ? await identity.listMembers(slug, viewer).catch(() => null) : null;
43 const known = new Set(members?.ok ? members.value.map((m) => m.username.toLowerCase()) : []);
44 const people = await Promise.all(
45 usernames.filter((name) => known.has(name) && name !== viewer.username.toLowerCase()).map((name) => identity.userByUsername(name).catch(() => null)),
46 );
47 return people.filter((user): user is User => user != null).map((user) => ({ kind: "user" as const, id: user.id }));
48}
49
50/**
51 * A person anywhere on g1t, by `user:<username>` (or a bare username), as
52 * the chat service names them: by their account id. Null for no one, and
53 * for anything that is not a person's account.
54 */
55export async function personFrom(field: string): Promise<Principal | null> {
56 const name = field
57 .replace(/^user:/, "")
58 .replace(/^@/, "")
59 .trim()
60 .toLowerCase();
61 if (!name || name.startsWith("usr_")) return null;
62 const user = await identity.userByUsername(name).catch(() => null);
63 return user && (user.kind ?? "user") === "user" ? { kind: "user", id: user.id } : null;
64}
65
66/** The cookie that remembers the last conversation open, so Chat opens there again. */
67export const LAST_CHAT_COOKIE = "g1t_chat";
68
69export function rememberChat(path: string, secure: boolean): string {
70 return `${LAST_CHAT_COOKIE}=${encodeURIComponent(path)}; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly${secure ? "; Secure" : ""}`;
71}