| 1 | import { type ChatSidebar, type Principal, type User, shownUsername } from "@g1t/contracts"; |
| 2 | |
| 3 | import type { Mentionable } from "./chat"; |
| 4 | import { 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 | */ |
| 11 | export 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. */ |
| 26 | export 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 | */ |
| 40 | export 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. */ |
| 53 | export const LAST_CHAT_COOKIE = "g1t_chat"; |
| 54 | |
| 55 | export function rememberChat(path: string, secure: boolean): string { |
| 56 | return `${LAST_CHAT_COOKIE}=${encodeURIComponent(path)}; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly${secure ? "; Secure" : ""}`; |
| 57 | } |