225 lines
6.0 KiB
TypeScript
225 lines
6.0 KiB
TypeScript
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<void>;
|
|
}
|
|
|
|
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<unknown>,
|
|
done: string | null
|
|
): Promise<void> {
|
|
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;
|
|
}
|