import type { BobbinContext } from "#lib/api/client.ts"; import { INVALID_HANDLE } from "#lib/api/identity.ts"; import { listIssues } from "#lib/api/issue.ts"; import { listPulls } from "#lib/api/records.ts"; import { searchActorsTypeahead } from "#lib/api/search.ts"; import { didOf } from "#lib/api/syntax.ts"; import { rkeyFromUri } from "#lib/api/uri.ts"; import type { TicketState } from "#lib/components/repo/tickets/TicketStatePill.svelte"; import type { MentionPerson } from "#lib/components/ui/mentionPeople.ts"; export interface UserSuggestion { kind: "user"; key: string; did: string; handle: string; insert: string; } export interface TicketSuggestion { kind: "ticket"; key: string; isPull: boolean; state: TicketState; title: string; rkey: string; createdAt: string; insert: string; } export type Suggestion = UserSuggestion | TicketSuggestion; export interface TicketRepo { did: string; url: string; } const SUGGESTION_LIMIT = 8; const RECOMMENDATION_LIMIT = 5; const TICKET_FETCH_LIMIT = 100; export const suggestUsers = async ( ctx: BobbinContext, query: string, signal: AbortSignal ): Promise => { if (query === "") return []; const { actors } = await searchActorsTypeahead( ctx, { q: query, limit: SUGGESTION_LIMIT }, { signal } ); return actors.map((actor) => userSuggestion(actor.did, actor.handle)); }; const userSuggestion = (did: string, handle: string): UserSuggestion => ({ kind: "user", key: did, did, handle, insert: `@${handle} ` }); export const recommendUsers = ( people: MentionPerson[], exclude: string | undefined ): UserSuggestion[] => { const seen = new Set(exclude ? [exclude] : []); const picked: UserSuggestion[] = []; for (const { did, handle } of people) { if (!handle || handle === INVALID_HANDLE || seen.has(did)) continue; seen.add(did); picked.push(userSuggestion(did, handle)); if (picked.length === RECOMMENDATION_LIMIT) break; } return picked; }; export const recommendPeople = async ( known: MentionPerson[], members: () => Promise, exclude: string | undefined ): Promise => { const participants = recommendUsers(known, exclude); if (participants.length >= RECOMMENDATION_LIMIT) return participants; const others = await members().catch(() => []); return recommendUsers([...known, ...others], exclude); }; const ticket = ( repo: TicketRepo, isPull: boolean, item: { uri: string; state: TicketState; value: { title: { text: string }; createdAt: string } } ): TicketSuggestion => { const rkey = rkeyFromUri(item.uri); return { kind: "ticket", key: item.uri, isPull, state: item.state, title: item.value.title.text, rkey, createdAt: item.value.createdAt, insert: `[#${rkey}](${repo.url}/${isPull ? "pulls" : "issues"}/${item.uri}) ` }; }; export const loadTickets = async ( ctx: BobbinContext, repo: TicketRepo ): Promise => { const [issues, pulls] = await Promise.all([ listIssues(ctx, didOf(repo.did), { limit: TICKET_FETCH_LIMIT }), listPulls(ctx, repo.did, { limit: TICKET_FETCH_LIMIT }) ]); return [ ...issues.items.map((item) => ticket(repo, false, item)), ...pulls.items.map((item) => ticket(repo, true, item)) ].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); }; export const filterTickets = (tickets: TicketSuggestion[], query: string): TicketSuggestion[] => { const needle = query.toLowerCase(); return tickets .filter( (ticket) => ticket.rkey.startsWith(needle) || ticket.title.toLowerCase().includes(needle) ) .slice(0, SUGGESTION_LIMIT); };