import { ChevronRight, RefreshCw, ShieldAlert } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; import { formatDate } from "../lib/format"; import type { Navigate } from "../routing"; import type { ModerationCaseRow } from "../types"; const defaultStatuses = "open,in_review,action_pending,action_failed,appeal_review"; const allStatuses = "open,in_review,action_pending,action_failed,resolved,dismissed,appeal_review"; const statusFilterOptions = [ { value: defaultStatuses, label: "Active queue" }, { value: allStatuses, label: "All statuses" }, { value: "open", label: "Open" }, { value: "in_review", label: "In review" }, { value: "action_pending", label: "Action pending" }, { value: "action_failed", label: "Action failed" }, { value: "appeal_review", label: "Appeal review" }, { value: "resolved", label: "Resolved" }, { value: "dismissed", label: "Dismissed" } ]; export function ModerationCasesPage({ navigate }: { navigate: Navigate }) { const [statuses, setStatuses] = useState(defaultStatuses); const [assignedTo, setAssignedTo] = useState(""); const [rows, setRows] = useState([]); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); async function load() { setBusy(true); setError(""); try { const params = new URLSearchParams({ statuses, limit: "100" }); if (assignedTo.trim()) params.set("assigned_to", assignedTo.trim()); setRows((await api.moderationCases(params)).cases); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } useEffect(() => { void load(); }, []); const pendingActions = rows.filter((row) => row.Status === "action_pending" || row.Status === "action_failed").length; const critical = rows.filter((row) => row.Severity === 4).length; return ( {"Refresh"} } > {error && {error}}
{ event.preventDefault(); void load(); }}>
{rows.map((row) => ( ))} {rows.length === 0 && }
{"Case"} {"Target"} {"Status"} {"Severity"} {"Reports / Reporters"} {"Reviewer"} {"Latest report"}
#{row.ID} {moderationTargetLabel(row.Target.Type, row.Target.ID)} {row.ReportCount} / {row.DistinctReporterCount} {row.AssignedTo || "-"} {formatDate(row.LastReportAt)}
); } export function CaseStatus({ status }: { status: string }) { const tone = status === "resolved" || status === "dismissed" ? "good" : status === "action_failed" ? "danger" : status === "action_pending" ? "warn" : "neutral"; return {moderationEnumLabel("status", status)}; } const severityLabels: Record = { low: "Low", medium: "Medium", high: "High", critical: "Critical" }; export function CaseSeverity({ value }: { value: number }) { const keys = ["", "low", "medium", "high", "critical"]; const key = keys[value]; return ( = 4 ? "danger" : value >= 3 ? "warn" : "neutral"}> {key ? severityLabels[key] : value} ); } const moderationLabels: Record> = { status: { open: "Open", in_review: "In review", action_pending: "Action pending", action_failed: "Action failed", appeal_review: "Appeal review", resolved: "Resolved", dismissed: "Dismissed" }, targetType: { channel: "Channel", chat: "Group", user: "Account" }, source: { account_peer: "Account / peer", antispam_false_positive: "Anti-spam false positive", channel_spam: "Channel spam", encrypted_spam: "Encrypted-chat spam", ephemeral: "Ephemeral media", messages: "Messages", messages_spam: "Message spam", profile_photo: "Profile photo", reaction: "Reaction", sponsored: "Sponsored message", story: "Story" }, reason: { child_abuse: "Child abuse", copyright: "Copyright", fake: "Fake", geo_irrelevant: "Location-irrelevant", illegal_drugs: "Illegal drugs", other: "Other", personal_details: "Personal details", pornography: "Pornography", spam: "Spam", violence: "Violence" } }; export function moderationEnumLabel(group: string, value: string): string { return moderationLabels[group]?.[value] ?? value; } export function moderationTargetLabel(type: string, id: number): string { return `${moderationEnumLabel("targetType", type)} #${id}`; }