import type { Did } from "@atcute/lexicons/syntax"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { IdentityCache } from "$lib/api/identity"; import { collect } from "$lib/api/pagination"; export type CollaboratorStanding = "owner" | "collaborator" | "invited"; export interface CollaboratorView { did: string; handle: string; standing: CollaboratorStanding; since?: string; } const MAX_COLLABORATORS = 200; export const listCollaborators = async ( ctx: BobbinContext, repo: { ownerDid: string; repoDid?: string }, init?: XrpcRequestInit ): Promise => { const identities = new IdentityCache(ctx); const rows: Omit[] = [{ did: repo.ownerDid, standing: "owner" }]; if (repo.repoDid) { const subject = repo.repoDid as Did; const page = { ...init, max: MAX_COLLABORATORS }; const [accepted, invited] = await Promise.all([ collect(ctx, "sh.tangled.repo.listCollaborators", { subject, limit: 100 }, page), collect(ctx, "sh.tangled.repo.listCollaboratorInvites", { subject, limit: 100 }, page) // fail open; knots and viewers without invite support reject the lookup .catch(() => []) ]); const seen = new Set([repo.ownerDid]); for (const item of accepted) { if (seen.has(item.subject)) continue; seen.add(item.subject); rows.push({ did: item.subject, standing: "collaborator", since: item.effectiveSince }); } for (const item of invited) { if (seen.has(item.subject)) continue; seen.add(item.subject); rows.push({ did: item.subject, standing: "invited", since: item.createdAt }); } } const named: CollaboratorView[] = []; for (const row of rows) { const handle = await identities .resolve(row.did, init) .then((doc) => doc.handle) .catch(() => row.did); named.push({ ...row, handle }); } return named; };