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<UserSuggestion[]> => {
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<MentionPerson[]>,
exclude: string | undefined
): Promise<UserSuggestion[]> => {
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"}/${rkey}) `
};
};
export const loadTickets = async (
ctx: BobbinContext,
repo: TicketRepo
): Promise<TicketSuggestion[]> => {
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);
};