:3
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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")}>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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…",
|
||||||
|
|||||||
Reference in New Issue
Block a user