| 1 | /** |
| 2 | * People: a workspace's directory of the people in it, and each member's |
| 3 | * place in it: a title and what they own. Kept by identity beside |
| 4 | * membership; mirrors `crates/contracts/src/people.rs`. |
| 5 | */ |
| 6 | import type { User } from "./identity"; |
| 7 | import type { Result } from "./result"; |
| 8 | import type { TeamChannel, TeamLead, TeamRef, TeamRole, TeamVisibility } from "./teams"; |
| 9 | |
| 10 | /** The longest title. */ |
| 11 | export const MAX_TITLE_LENGTH = 80; |
| 12 | /** The most things one person owns, as their profile lists them, and the longest of them. */ |
| 13 | export const MAX_OWNS = 8; |
| 14 | export const MAX_OWNS_LENGTH = 60; |
| 15 | |
| 16 | /** One member, as the directory and their profile show them. */ |
| 17 | export type DirectoryPerson = { |
| 18 | user_id: string; |
| 19 | username: string; |
| 20 | display_username?: string; |
| 21 | name: string | null; |
| 22 | avatar: string | null; |
| 23 | bio: string | null; |
| 24 | location: string | null; |
| 25 | pronouns: string | null; |
| 26 | /** An IANA time zone, for their local time. */ |
| 27 | timezone: string | null; |
| 28 | role: "owner" | "member"; |
| 29 | /** Their title in this workspace. */ |
| 30 | title: string | null; |
| 31 | /** What they own: a few short phrases. */ |
| 32 | owns: string[]; |
| 33 | joined_at: string; |
| 34 | }; |
| 35 | |
| 36 | export type TeamPersonRef = { username: string; role: TeamRole }; |
| 37 | |
| 38 | /** A team as the directory needs it. */ |
| 39 | export type DirectoryTeam = { |
| 40 | slug: string; |
| 41 | name: string; |
| 42 | description: string | null; |
| 43 | visibility: TeamVisibility; |
| 44 | parent: TeamRef | null; |
| 45 | lead: TeamLead | null; |
| 46 | channel: TeamChannel | null; |
| 47 | /** Its own people. */ |
| 48 | people: TeamPersonRef[]; |
| 49 | repos_count: number; |
| 50 | }; |
| 51 | |
| 52 | export type PeopleDirectory = { |
| 53 | people: DirectoryPerson[]; |
| 54 | teams: DirectoryTeam[]; |
| 55 | /** What members get on every repository. */ |
| 56 | base_permission: "none" | "read" | "write" | "admin"; |
| 57 | /** Whether the viewer owns the workspace: they set anyone's title and what they own. */ |
| 58 | can_manage: boolean; |
| 59 | }; |
| 60 | |
| 61 | /** What changes on a member's profile; the rest stays. */ |
| 62 | export type MemberProfileChanges = { |
| 63 | title?: string; |
| 64 | owns?: string[]; |
| 65 | }; |
| 66 | |
| 67 | export interface PeopleClient { |
| 68 | /** Everyone in a workspace and its teams, as the viewer may see them. Members only. */ |
| 69 | peopleDirectory(viewer: User | null, workspace: string): Promise<Result<PeopleDirectory>>; |
| 70 | /** A member's title and what they own: themselves, or an owner. */ |
| 71 | setMemberProfile(actor: User, workspace: string, username: string, changes: MemberProfileChanges): Promise<Result<DirectoryPerson>>; |
| 72 | } |
| 73 | |
| 74 | /** What someone owns, as identity keeps it: trimmed, each once (ignoring case), at most eight. */ |
| 75 | export function cleanOwns(owns: readonly string[]): string[] { |
| 76 | const out: string[] = []; |
| 77 | for (const raw of owns) { |
| 78 | const phrase = raw.split(/\s+/).filter(Boolean).join(" ").slice(0, MAX_OWNS_LENGTH).replace(/[.,;]+$/, "").trim(); |
| 79 | if (!phrase || out.some((kept) => kept.toLowerCase() === phrase.toLowerCase())) continue; |
| 80 | out.push(phrase); |
| 81 | if (out.length === MAX_OWNS) break; |
| 82 | } |
| 83 | return out; |
| 84 | } |