import React, { useEffect, useState } from "react"; import { useStore } from "@nanostores/react"; import { useTranslation } from "react-i18next"; import { $user } from "../../store/auth"; import { checkAdminAccess, getAdminReports, adminTakeAction, adminCreateLabel, adminDeleteLabel, adminGetLabels, adminBanAccount, adminUnbanAccount, adminGetBannedAccounts, type BannedAccount, } from "../../api/client"; import type { ModerationReport, HydratedLabel } from "../../types"; import { Shield, CheckCircle, XCircle, AlertTriangle, Eye, ChevronDown, ChevronUp, Tag, FileText, Plus, Trash2, EyeOff, UserX, UserCheck, } from "lucide-react"; import { Avatar, EmptyState, Skeleton, Button } from "../../components/ui"; const STATUS_COLORS: Record = { pending: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300", resolved: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300", dismissed: "bg-surface-100 text-surface-600 dark:bg-surface-800 dark:text-surface-400", escalated: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", acknowledged: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", }; const REASON_LABEL_KEYS: Record = { spam: "adminModeration.reasons.spam", violation: "adminModeration.reasons.violation", misleading: "adminModeration.reasons.misleading", sexual: "adminModeration.reasons.sexual", rude: "adminModeration.reasons.rude", other: "adminModeration.reasons.other", }; const LABEL_VALS = [ "sexual", "nudity", "violence", "gore", "spam", "misleading", ]; type Tab = "reports" | "labels" | "actions" | "bans"; export default function AdminModeration() { const { t } = useTranslation(); const user = useStore($user); const [isAdmin, setIsAdmin] = useState(false); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState("reports"); const [reports, setReports] = useState([]); const [pendingCount, setPendingCount] = useState(0); const [totalCount, setTotalCount] = useState(0); const [statusFilter, setStatusFilter] = useState("pending"); const [expandedReport, setExpandedReport] = useState(null); const [actionLoading, setActionLoading] = useState(null); const [labels, setLabels] = useState([]); const [labelSrc, setLabelSrc] = useState(""); const [labelUri, setLabelUri] = useState(""); const [labelVal, setLabelVal] = useState(""); const [labelSubmitting, setLabelSubmitting] = useState(false); const [labelSuccess, setLabelSuccess] = useState(false); const [bans, setBans] = useState([]); const [banDid, setBanDid] = useState(""); const [banReason, setBanReason] = useState(""); const [banSubmitting, setBanSubmitting] = useState(false); const [banSuccess, setBanSuccess] = useState(false); const [unbanLoading, setUnbanLoading] = useState(null); const loadReports = async (status: string) => { const data = await getAdminReports(status || undefined); setReports(data.items); setPendingCount(data.pendingCount); setTotalCount(data.totalItems); }; const loadLabels = async () => { const data = await adminGetLabels(); setLabels(data.items || []); }; const loadBans = async () => { const data = await adminGetBannedAccounts(); setBans(data.items || []); }; useEffect(() => { const init = async () => { const admin = await checkAdminAccess(); setIsAdmin(admin); if (admin) await loadReports("pending"); setLoading(false); }; init(); }, []); const handleTabChange = async (tab: Tab) => { setActiveTab(tab); if (tab === "labels") await loadLabels(); if (tab === "bans") await loadBans(); }; const handleFilterChange = async (status: string) => { setStatusFilter(status); await loadReports(status); }; const handleAction = async (reportId: number, action: string) => { setActionLoading(reportId); const success = await adminTakeAction({ reportId, action }); if (success) { await loadReports(statusFilter); setExpandedReport(null); } setActionLoading(null); }; const handleBanFromReport = async (did: string, reportId: number) => { setActionLoading(reportId); await adminBanAccount({ did }); setActionLoading(null); }; const handleBanAccount = async () => { if (!banDid.trim()) return; setBanSubmitting(true); const success = await adminBanAccount({ did: banDid.trim(), reason: banReason.trim() || undefined, }); if (success) { setBanDid(""); setBanReason(""); setBanSuccess(true); setTimeout(() => setBanSuccess(false), 2000); await loadBans(); } setBanSubmitting(false); }; const handleUnban = async (did: string) => { setUnbanLoading(did); const success = await adminUnbanAccount(did); if (success) setBans((prev) => prev.filter((b) => b.did !== did)); setUnbanLoading(null); }; const handleCreateLabel = async () => { if (!labelVal || (!labelSrc && !labelUri)) return; setLabelSubmitting(true); const success = await adminCreateLabel({ src: labelSrc || labelUri, uri: labelUri || undefined, val: labelVal, }); if (success) { setLabelSrc(""); setLabelUri(""); setLabelVal(""); setLabelSuccess(true); setTimeout(() => setLabelSuccess(false), 2000); if (activeTab === "labels") await loadLabels(); } setLabelSubmitting(false); }; const handleDeleteLabel = async (id: number) => { if (!window.confirm(t("adminModeration.labels.removeConfirm"))) return; const success = await adminDeleteLabel(id); if (success) setLabels((prev) => prev.filter((l) => l.id !== id)); }; if (loading) { return (
); } if (!user || !isAdmin) { return ( } title={t("adminModeration.accessDenied")} message={t("adminModeration.accessDeniedMessage")} /> ); } return (

{t("adminModeration.title")}

{t("adminModeration.stats", { pending: pendingCount, total: totalCount, })}

{[ { id: "reports" as Tab, label: t("adminModeration.tabs.reports"), icon: , }, { id: "actions" as Tab, label: t("adminModeration.tabs.actions"), icon: , }, { id: "labels" as Tab, label: t("adminModeration.tabs.labels"), icon: , }, { id: "bans" as Tab, label: "Bans", icon: , }, ].map((tab) => ( ))}
{activeTab === "reports" && ( <>
{["pending", "resolved", "dismissed", "escalated", ""].map( (status) => ( ), )}
{reports.length === 0 ? ( } title={t("adminModeration.reports.empty")} message={ statusFilter === "pending" ? t("adminModeration.reports.emptyPending") : t("adminModeration.reports.emptyFiltered", { status: statusFilter || "", }) } /> ) : (
{reports.map((report) => (
{expandedReport === report.id && (
{t("adminModeration.reports.reportedUser")} @{report.subject.handle || report.subject.did}
{t("adminModeration.reports.reporter")} @{report.reporter.handle || report.reporter.did}
{report.reasonText && (
{t("adminModeration.reports.details")}

{report.reasonText}

)} {report.subjectUri && (
{t("adminModeration.reports.contentUri")}

{report.subjectUri}

)} {report.status === "pending" && (
)}
)}
))}
)} )} {activeTab === "actions" && (

{t("adminModeration.actions.applyWarning")}

{t("adminModeration.actions.applyWarningDesc")}

setLabelSrc(e.target.value)} placeholder="did:plc:..." className="w-full px-3 py-2 text-sm rounded-lg border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 text-surface-900 dark:text-white placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500" />
setLabelUri(e.target.value)} placeholder="at://did:plc:.../at.margin.annotation/..." className="w-full px-3 py-2 text-sm rounded-lg border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 text-surface-900 dark:text-white placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500" />
{LABEL_VALS.map((val) => ( ))}
{labelSuccess && ( {" "} {t("adminModeration.actions.labelApplied")} )}
)} {activeTab === "labels" && (
{labels.length === 0 ? ( } title={t("adminModeration.labels.empty")} message={t("adminModeration.labels.emptyMessage")} /> ) : (
{labels.map((label) => (
{label.subject && ( )}
{label.val} {label.subject && ( @{label.subject.handle || label.subject.did} )}

{label.uri !== label.src ? label.uri : t("adminModeration.labels.accountLevel")}{" "} · {new Date(label.createdAt).toLocaleDateString()} · by @ {label.createdBy.handle || label.createdBy.did}

))}
)}
)} {activeTab === "bans" && (

Ban account

Banned users cannot sign in and their content is hidden everywhere on Margin.

setBanDid(e.target.value)} placeholder="did:plc:..." className="w-full px-3 py-2 text-sm rounded-lg border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 text-surface-900 dark:text-white placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500" />
setBanReason(e.target.value)} placeholder="Reason for ban..." className="w-full px-3 py-2 text-sm rounded-lg border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 text-surface-900 dark:text-white placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500" />
{banSuccess && ( Account banned )}
{bans.length === 0 ? ( } title="No banned accounts" message="Banned accounts will appear here." /> ) : (
{bans.map((ban) => (
{ban.profile?.displayName || (ban.profile?.handle && `@${ban.profile.handle}`) || ban.did} banned

{ban.reason ? `${ban.reason} · ` : ""} {new Date(ban.bannedAt).toLocaleDateString()}

))}
)}
)}
); }