Skip to content
57 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
46 .filter((name) => known.has(name) && name !== viewer.username.toLowerCase())
47 .map((name) => identity.userByUsername(name).catch(() => null)),
48 );
49 return people.filter((user): user is User => user != null).map((user) => ({ kind: "user" as const, id: user.id }));
50}
51
52/** The cookie that remembers the last conversation open, so Chat opens there again. */
53export const LAST_CHAT_COOKIE = "g1t_chat";
54
55export function rememberChat(path: string, secure: boolean): string {
56 return `${LAST_CHAT_COOKIE}=${encodeURIComponent(path)}; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly${secure ? "; Secure" : ""}`;
57}