import type { BobbinContext } from "$lib/api/client"; import { INVALID_HANDLE } from "$lib/api/identity"; import { listIssues } from "$lib/api/issue"; import { listPulls } from "$lib/api/records"; import { searchActorsTypeahead } from "$lib/api/search"; import { rkeyFromUri } from "$lib/api/uri"; import type { TicketState } from "$lib/components/repo/tickets/TicketStatePill.svelte"; import type { MentionPerson } from "$lib/components/ui/mentionPeople"; 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: string; value: { title: string; createdAt: string } } ): TicketSuggestion => { const rkey = rkeyFromUri(item.uri); const state: TicketState = item.state === "closed" || (isPull && item.state === "merged") ? item.state : "open"; return { kind: "ticket", key: item.uri, isPull, state, title: item.value.title, 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, 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); };