feat(frontend): capture policy shows what each rule changes, rule card opens from folder and chat menus and can trash downloaded files, trash is paged

This commit is contained in:
hh committed 2026-10-14 04:20:38 +02:00
1 parent f646f29ff9
commit cb01d7dff8
16 files changed
+1536 -369

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
import type { CaptureToggles, PolicyScopeType } from "#lib/api/types.js";
import { plural } from "#lib/format/shares.js";
export type ToggleKey = keyof CaptureToggles;
export const TOGGLE_LABELS: Record<ToggleKey, string> = {
backfill: "Бэкфилл",
media: "Медиа",
messages: "Сообщения",
presence: "Присутствие",
profile_history: "История профиля",
reactions: "Реакции",
self_destruct_media: "Исчезающие",
stories: "Сторис",
stt: "Расшифровка",
track_edits_deletes: "Правки, удаления",
};
export const TOGGLE_KEYS: ToggleKey[] = [
"messages",
"media",
"self_destruct_media",
"stt",
"reactions",
"track_edits_deletes",
"profile_history",
"stories",
"presence",
"backfill",
];
export const DEFAULT_LABELS: Partial<Record<PolicyScopeType, string>> = {
default_channel: "Каналы",
default_dm: "Личные чаты",
default_group: "Группы",
};
const PARENT_LABELS: Partial<Record<PolicyScopeType, string>> = {
default_channel: "у каналов",
default_dm: "у личных чатов",
default_group: "у групп",
};
const SHOWN_CHIPS = 3;
const SHORT_LIST = 2;
export interface Chip {
label: string;
sign: "plus" | "minus";
}
export function pickToggles(source: CaptureToggles): CaptureToggles {
return {
backfill: source.backfill,
media: source.media,
messages: source.messages,
presence: source.presence,
profile_history: source.profile_history,
reactions: source.reactions,
self_destruct_media: source.self_destruct_media,
stories: source.stories,
stt: source.stt,
track_edits_deletes: source.track_edits_deletes,
};
}
export function preset(only: ToggleKey[] | "all"): CaptureToggles {
const toggles = {} as CaptureToggles;
for (const key of TOGGLE_KEYS) {
toggles[key] = only === "all" || only.includes(key);
}
return toggles;
}
export function sameToggles(a: CaptureToggles, b: CaptureToggles): boolean {
return TOGGLE_KEYS.every((key) => a[key] === b[key]);
}
export function summary(toggles: CaptureToggles): string {
const on = TOGGLE_KEYS.filter((key) => toggles[key]);
const off = TOGGLE_KEYS.filter((key) => !toggles[key]);
if (off.length === 0) {
return "Всё";
}
if (on.length === 0) {
return "Ничего";
}
if (on.length === 1 && on[0] === "messages") {
return "Только сообщения";
}
if (off.length <= SHORT_LIST) {
const names = off.map((key) => TOGGLE_LABELS[key].toLowerCase());
return `Всё, кроме: ${names.join(", ")}`;
}
const names = on.slice(0, SHORT_LIST).map((key) => TOGGLE_LABELS[key]);
const rest = on.length - names.length;
return rest > 0 ? `${names.join(", ")} и ещё ${rest}` : names.join(", ");
}
export function parentLabel(
scopeType: PolicyScopeType,
title: string | null
): string {
if (scopeType === "folder") {
return `у папки «${title ?? "без названия"}»`;
}
return PARENT_LABELS[scopeType] ?? "по умолчанию";
}
function settings(count: number): string {
return `${count} ${plural(count, "настройка", "настройки", "настроек")}`;
}
function collapse(
keys: ToggleKey[],
sign: Chip["sign"],
whole: string
): Chip[] {
if (keys.length === 0) {
return [];
}
if (keys.length > SHOWN_CHIPS) {
return [{ label: whole, sign }];
}
return keys.map((key) => ({ label: TOGGLE_LABELS[key], sign }));
}
export function diffChips(
rule: CaptureToggles,
bases: CaptureToggles[]
): Chip[] {
const plus = TOGGLE_KEYS.filter(
(key) => rule[key] && bases.some((base) => !base[key])
);
const minus = TOGGLE_KEYS.filter(
(key) => !rule[key] && bases.some((base) => base[key])
);
const allOn = TOGGLE_KEYS.every((key) => rule[key]);
const allOff = TOGGLE_KEYS.every((key) => !rule[key]);
return [
...collapse(plus, "plus", allOn ? "Всё включено" : settings(plus.length)),
...collapse(
minus,
"minus",
allOff ? "Всё выключено" : settings(minus.length)
),
];
}