This commit is contained in:
k
2026-09-21 21:57:36 +02:00
parent e69654430a
commit 43003957eb
4 changed files with 98 additions and 10 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ export function AdminCenter() {
try { try {
const res = await api.adminReportList(); const res = await api.adminReportList();
setOpenReportCount( setOpenReportCount(
res.reports.filter((r) => r.status === "open" || r.status === "reviewing").length, res.reports.filter((r) => (r.status === "open" || r.status === "reviewing") && r.reporter_shadowbanned !== 1).length,
); );
} catch { } catch {
// Transient failure: keep last count. // Transient failure: keep last count.
-9
View File
@@ -126,15 +126,6 @@ function ReportDialog({ open, onClose, conversationId, messageIndex, content, pr
<Button variant="secondary" onClick={onClose}>{t("common.close")}</Button> <Button variant="secondary" onClick={onClose}>{t("common.close")}</Button>
</div> </div>
</div> </div>
) : (
<div className="space-y-4">
<p className="flex items-center gap-2 rounded-2xl bg-emerald-600/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-400">
<Check size={16} /> {t("report.sent")}
</p>
<div className="flex justify-end">
<Button variant="secondary" onClick={onClose}>{t("common.close")}</Button>
</div>
</div>
) : ( ) : (
<form onSubmit={submit} className="space-y-4"> <form onSubmit={submit} className="space-y-4">
<Field label={t("report.reason")}> <Field label={t("report.reason")}>
+43
View File
@@ -136,8 +136,10 @@ function ReportDetail({ report, onChanged }: { report: Report; onChanged: () =>
const [note, setNote] = useState(report.admin_note); const [note, setNote] = useState(report.admin_note);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [confirmBan, setConfirmBan] = useState<boolean | null>(null);
const { t } = useT(); const { t } = useT();
const closed = report.status === "resolved" || report.status === "dismissed"; const closed = report.status === "resolved" || report.status === "dismissed";
const isBanned = report.reporter_shadowbanned === 1;
useEffect(() => { useEffect(() => {
setNote(report.admin_note); setNote(report.admin_note);
@@ -156,8 +158,29 @@ function ReportDetail({ report, onChanged }: { report: Report; onChanged: () =>
} }
} }
async function toggleBan(shadowbanned: boolean) {
setBusy(true);
setError("");
try {
await api.adminShadowban(report.reporter_id, shadowbanned);
onChanged();
} catch (err) {
setError(err instanceof Error ? err.message : t("reports.updateFailed"));
} finally {
setBusy(false);
}
}
return ( return (
<div className="mt-2 space-y-3 rounded-3xl border border-stone-200/70 bg-stone-50 p-4 dark:border-zinc-800 dark:bg-zinc-900/60"> <div className="mt-2 space-y-3 rounded-3xl border border-stone-200/70 bg-stone-50 p-4 dark:border-zinc-800 dark:bg-zinc-900/60">
{report.prompt && (
<div>
<p className="mb-1 text-xs font-medium uppercase tracking-wider opacity-50">{t("reports.prompt")}</p>
<div className="max-h-40 overflow-y-auto whitespace-pre-wrap rounded-2xl border border-stone-200/70 bg-white px-3.5 py-2.5 text-sm dark:border-zinc-800 dark:bg-zinc-900">
{report.prompt}
</div>
</div>
)}
<div> <div>
<p className="mb-1 text-xs font-medium uppercase tracking-wider opacity-50">{t("reports.response")}</p> <p className="mb-1 text-xs font-medium uppercase tracking-wider opacity-50">{t("reports.response")}</p>
<div className="max-h-56 overflow-y-auto whitespace-pre-wrap rounded-2xl border border-stone-200/70 bg-white px-3.5 py-2.5 text-sm dark:border-zinc-800 dark:bg-zinc-900"> <div className="max-h-56 overflow-y-auto whitespace-pre-wrap rounded-2xl border border-stone-200/70 bg-white px-3.5 py-2.5 text-sm dark:border-zinc-800 dark:bg-zinc-900">
@@ -176,6 +199,26 @@ function ReportDetail({ report, onChanged }: { report: Report; onChanged: () =>
)} )}
</div> </div>
<div className="flex flex-wrap items-center gap-2">
<Button size="sm" variant="secondary" disabled={busy} onClick={() => setConfirmBan(!isBanned)}>
{isBanned ? <Eye size={14} /> : <EyeOff size={14} />}
{isBanned ? t("reports.unshadowban") : t("reports.shadowban")}
</Button>
{isBanned && <span className="text-xs opacity-60">{t("reports.unreliable")}</span>}
</div>
<ConfirmDialog
open={confirmBan !== null}
onClose={() => setConfirmBan(null)}
onConfirm={() => {
const v = confirmBan;
setConfirmBan(null);
if (v !== null) void toggleBan(v);
}}
title={t(confirmBan === false ? "reports.unshadowbanTitle" : "reports.shadowbanTitle")}
message={t(confirmBan === false ? "reports.unshadowbanMsg" : "reports.shadowbanMsg", { user: report.reporter_name })}
confirmLabel={t(confirmBan === false ? "reports.unshadowban" : "reports.shadowban")}
/>
{!closed ? ( {!closed ? (
<div className="flex flex-wrap items-end gap-2 rounded-2xl border border-stone-200 bg-white p-3 dark:border-zinc-700 dark:bg-zinc-900"> <div className="flex flex-wrap items-end gap-2 rounded-2xl border border-stone-200 bg-white p-3 dark:border-zinc-700 dark:bg-zinc-900">
<div className="min-w-48 flex-1"> <div className="min-w-48 flex-1">
+54
View File
@@ -467,6 +467,15 @@ const en = {
"reports.btnDismiss": "Dismiss", "reports.btnDismiss": "Dismiss",
"reports.updateFailed": "Update failed", "reports.updateFailed": "Update failed",
"reports.closedNote": "Report {status}.", "reports.closedNote": "Report {status}.",
"report.alreadyReported": "This response was already reported.",
"reports.prompt": "User prompt",
"reports.unreliable": "Unreliable reporter",
"reports.shadowban": "Shadow-ban reporter",
"reports.unshadowban": "Lift shadow-ban",
"reports.shadowbanTitle": "Shadow-ban this reporter?",
"reports.shadowbanMsg": "@{user}'s reports will still be accepted but marked unreliable and hidden from the badge.",
"reports.unshadowbanTitle": "Lift the shadow-ban?",
"reports.unshadowbanMsg": "@{user}'s reports will count toward the badge again.",
"review.title": "Access request", "review.title": "Access request",
"review.loadingTicket": "Loading ticket…", "review.loadingTicket": "Loading ticket…",
@@ -961,6 +970,15 @@ const es: Dict = {
"reports.btnDismiss": "Descartar", "reports.btnDismiss": "Descartar",
"reports.updateFailed": "No se pudo actualizar", "reports.updateFailed": "No se pudo actualizar",
"reports.closedNote": "Denuncia {status}.", "reports.closedNote": "Denuncia {status}.",
"report.alreadyReported": "Esta respuesta ya fue denunciada.",
"reports.prompt": "Pregunta del usuario",
"reports.unreliable": "Denunciante no fiable",
"reports.shadowban": "Silenciar denunciante",
"reports.unshadowban": "Quitar silencio",
"reports.shadowbanTitle": "¿Silenciar a este denunciante?",
"reports.shadowbanMsg": "Las denuncias de @{user} se seguirán aceptando, pero se marcarán como no fiables y no contarán en el contador.",
"reports.unshadowbanTitle": "¿Quitar el silencio?",
"reports.unshadowbanMsg": "Las denuncias de @{user} volverán a contar.",
"review.title": "Solicitud de acceso", "review.title": "Solicitud de acceso",
"review.loadingTicket": "Cargando ticket…", "review.loadingTicket": "Cargando ticket…",
@@ -1452,6 +1470,15 @@ const fr: Dict = {
"reports.btnDismiss": "Rejeter", "reports.btnDismiss": "Rejeter",
"reports.updateFailed": "Mise à jour impossible", "reports.updateFailed": "Mise à jour impossible",
"reports.closedNote": "Signalement {status}.", "reports.closedNote": "Signalement {status}.",
"report.alreadyReported": "Cette réponse a déjà été signalée.",
"reports.prompt": "Demande de l'utilisateur",
"reports.unreliable": "Signaleur non fiable",
"reports.shadowban": "Shadow-ban le signaleur",
"reports.unshadowban": "Lever le shadow-ban",
"reports.shadowbanTitle": "Shadow-ban ce signaleur ?",
"reports.shadowbanMsg": "Les signalements de @{user} seront toujours acceptés mais marqués non fiables et exclus du compteur.",
"reports.unshadowbanTitle": "Lever le shadow-ban ?",
"reports.unshadowbanMsg": "Les signalements de @{user} compteront à nouveau.",
"review.title": "Demande d'accès", "review.title": "Demande d'accès",
"review.loadingTicket": "Chargement du ticket…", "review.loadingTicket": "Chargement du ticket…",
@@ -1931,6 +1958,15 @@ const ru: Dict = {
"reports.btnDismiss": "Отклонить", "reports.btnDismiss": "Отклонить",
"reports.updateFailed": "Не обновилось", "reports.updateFailed": "Не обновилось",
"reports.closedNote": "Жалоба {status}.", "reports.closedNote": "Жалоба {status}.",
"report.alreadyReported": "На этот ответ уже пожаловались.",
"reports.prompt": "Запрос пользователя",
"reports.unreliable": "Ненадёжный автор",
"reports.shadowban": "Теневой бан",
"reports.unshadowban": "Снять теневой бан",
"reports.shadowbanTitle": "Теневой бан для этого автора?",
"reports.shadowbanMsg": "Жалобы @{user} будут приниматься, но помечаться как ненадёжные и не учитываться в счётчике.",
"reports.unshadowbanTitle": "Снять теневой бан?",
"reports.unshadowbanMsg": "Жалобы @{user} снова будут учитываться.",
"review.title": "Заявка на доступ", "review.title": "Заявка на доступ",
"review.loadingTicket": "Загружаем тикет…", "review.loadingTicket": "Загружаем тикет…",
@@ -2415,6 +2451,15 @@ const it: Dict = {
"reports.btnDismiss": "Respingi", "reports.btnDismiss": "Respingi",
"reports.updateFailed": "Aggiornamento fallito", "reports.updateFailed": "Aggiornamento fallito",
"reports.closedNote": "Segnalazione {status}.", "reports.closedNote": "Segnalazione {status}.",
"report.alreadyReported": "Questa risposta è già stata segnalata.",
"reports.prompt": "Richiesta dell'utente",
"reports.unreliable": "Autore inaffidabile",
"reports.shadowban": "Silenzia autore",
"reports.unshadowban": "Togli silenzio",
"reports.shadowbanTitle": "Silenziare questo autore?",
"reports.shadowbanMsg": "Le segnalazioni di @{user} saranno comunque accettate ma marcate come inaffidabili ed escluse dal conteggio.",
"reports.unshadowbanTitle": "Togliere il silenzio?",
"reports.unshadowbanMsg": "Le segnalazioni di @{user} conteranno di nuovo.",
"review.title": "Richiesta di accesso", "review.title": "Richiesta di accesso",
"review.loadingTicket": "Carico ticket…", "review.loadingTicket": "Carico ticket…",
@@ -2890,6 +2935,15 @@ const pt: Dict = {
"reports.btnDismiss": "Rejeitar", "reports.btnDismiss": "Rejeitar",
"reports.updateFailed": "Falha ao atualizar", "reports.updateFailed": "Falha ao atualizar",
"reports.closedNote": "Denúncia {status}.", "reports.closedNote": "Denúncia {status}.",
"report.alreadyReported": "Esta resposta já foi denunciada.",
"reports.prompt": "Pergunta do utilizador",
"reports.unreliable": "Autor pouco fiável",
"reports.shadowban": "Silenciar autor",
"reports.unshadowban": "Tirar silêncio",
"reports.shadowbanTitle": "Silenciar este autor?",
"reports.shadowbanMsg": "As denúncias de @{user} continuarão a ser aceites, mas marcadas como pouco fiáveis e excluídas da contagem.",
"reports.unshadowbanTitle": "Tirar o silêncio?",
"reports.unshadowbanMsg": "As denúncias de @{user} voltarão a contar.",
"review.title": "Pedido de acesso", "review.title": "Pedido de acesso",
"review.loadingTicket": "A carregar ticket…", "review.loadingTicket": "A carregar ticket…",