import ArchiveIcon from "@lucide/svelte/icons/archive"; import ArrowRightIcon from "@lucide/svelte/icons/arrow-right"; import BookCheckIcon from "@lucide/svelte/icons/book-check"; import BrainIcon from "@lucide/svelte/icons/brain"; import CircleXIcon from "@lucide/svelte/icons/circle-x"; import CopyIcon from "@lucide/svelte/icons/copy"; import EyeOffIcon from "@lucide/svelte/icons/eye-off"; import FileTextIcon from "@lucide/svelte/icons/file-text"; import GitBranchIcon from "@lucide/svelte/icons/git-branch"; import GitMergeIcon from "@lucide/svelte/icons/git-merge"; import PencilIcon from "@lucide/svelte/icons/pencil"; import RotateCcwIcon from "@lucide/svelte/icons/rotate-ccw"; import SendIcon from "@lucide/svelte/icons/send"; import type { Component } from "svelte"; import { toast } from "svelte-sonner"; import type { ApiClient } from "$lib/api/client"; import type { ConversationInfo } from "$lib/api/types"; import type { PanelHost } from "./host"; export interface MenuAction { // Present: rendered as a checkbox item with this state. checked?: boolean; danger?: boolean; disabled?: boolean; // A separator sits above this action. gap?: boolean; icon?: Component<{ class?: string }>; label: string; run: () => void | Promise; } export interface ActionScope { client: ApiClient; // The conversation is the one on screen, so "Open" is pointless. current: boolean; host: PanelHost; onBranch: () => void; onChanged: () => void; onOpen: (id: string) => void; onRename: () => void; } const TELEGRAM = "telegram"; const MARKDOWN = "markdown"; function describe(error: unknown): string { return error instanceof Error ? error.message : String(error); } async function attempt( action: () => Promise, done: string | null ): Promise { try { await action(); if (done) { toast.success(done); } } catch (error) { toast.error(describe(error)); } } function copyId(id: string): void { navigator.clipboard .writeText(id) .then(() => toast.success("Id copied")) .catch(() => toast.error("Clipboard is not available")); } // The one list of things you can do to a conversation, in the order the // header menu and the row's context menu both show them. export function conversationActions( info: ConversationInfo, scope: ActionScope ): MenuAction[] { const { client, host } = scope; const open = info.status === "open"; const telegram = info.bindings.find((b) => b.frontend === TELEGRAM); const note = info.bindings.find((b) => b.frontend === MARKDOWN && b.visible); const remembers = info.flags.memory !== false; const changed = () => scope.onChanged(); const actions: MenuAction[] = []; if (!scope.current) { actions.push({ icon: ArrowRightIcon, label: "Open", run: () => scope.onOpen(info.id), }); } if (note && host.openNote) { actions.push({ icon: FileTextIcon, label: "Open note", run: () => host.openNote?.(note.external_id), }); } actions.push({ disabled: !open, gap: actions.length > 0, icon: GitBranchIcon, label: "Branch…", run: scope.onBranch, }); actions.push({ checked: remembers, gap: true, icon: BrainIcon, label: "Remember on close", run: () => attempt( async () => { await client.setFlags(info.id, { memory: !remembers }); changed(); }, remembers ? "Memory off: closing only merges" : "Memory on: closing writes a digest" ), }); if (info.kind === "branch") { if (telegram?.visible) { actions.push({ icon: EyeOffIcon, label: "Hide from Telegram", run: () => attempt(async () => { await client.bind(info.id, TELEGRAM, telegram.external_id, false); changed(); }, "Hidden from Telegram"), }); } else { actions.push({ icon: SendIcon, label: "Return to Telegram", run: () => attempt(async () => { await client.bind( info.id, TELEGRAM, telegram?.external_id ?? null, true ); changed(); }, "Back in Telegram"), }); } } actions.push({ gap: true, icon: PencilIcon, label: "Rename…", run: scope.onRename, }); actions.push({ icon: CopyIcon, label: "Copy id", run: () => copyId(info.id), }); if (info.parent && open) { actions.push({ gap: true, icon: GitMergeIcon, label: "Merge into parent", run: () => attempt(async () => { await client.merge(info.id); changed(); }, "Merged into the parent"), }); } if (info.kind === "deep" && open) { actions.push({ gap: !(info.parent && open), icon: BookCheckIcon, label: "Close with digest", run: () => attempt(async () => { const result = await client.close(info.id); changed(); if (result.error) { toast.warning(`Digest: ${result.error}`); } else if (result.digest) { toast.message(`Digest: ${result.digest}`); } }, "Chat closed"), }); } if (open) { actions.push({ gap: !(info.parent || info.kind === "deep"), icon: CircleXIcon, label: "Close", run: () => attempt(async () => { await client.update(info.id, { status: "closed" }); changed(); }, "Closed"), }); actions.push({ danger: true, icon: ArchiveIcon, label: "Archive", run: () => attempt(async () => { await client.update(info.id, { status: "archived" }); changed(); }, "Archived"), }); } else { actions.push({ gap: true, icon: RotateCcwIcon, label: "Reopen", run: () => attempt(async () => { await client.update(info.id, { status: "open" }); changed(); }, "Reopened"), }); } return actions; }