forked from administration/panel
331 lines
11 KiB
TypeScript
331 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { Report } from "revolt-api";
|
|
import { Textarea } from "../../ui/textarea";
|
|
import { Button } from "../../ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "../../ui/dropdown-menu";
|
|
import { useState } from "react";
|
|
import { useToast } from "../../ui/use-toast";
|
|
import {
|
|
assignReportToCase,
|
|
rejectReport,
|
|
reopenReport,
|
|
resolveReport,
|
|
sendAlert,
|
|
updateReportNotes,
|
|
} from "@/lib/actions";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "../../ui/alert-dialog";
|
|
import { ReportCard } from "../../cards/ReportCard";
|
|
import { Popover } from "@radix-ui/react-popover";
|
|
import { PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { CaseDocument, ReportDocument, fetchOpenCases } from "@/lib/db";
|
|
import { CaseCard } from "@/components/cards/CaseCard";
|
|
|
|
const template: Record<string, (ref: string) => string> = {
|
|
resolved: (ref) =>
|
|
`Your report (${ref}) has been actioned and appropriate action has been taken.`,
|
|
invalid: (ref) => `Your report (${ref}) has been marked as invalid.`,
|
|
false: (ref) =>
|
|
`Your report (${ref}) has been marked as false or spam. False reports may lead to additional action against your account.`,
|
|
duplicate: (ref) => `Your report (${ref}) has been marked as a duplicate.`,
|
|
"not enough evidence": (ref) =>
|
|
`Your report (${ref}) has not been actioned at this time due to a lack of supporting evidence, if you have additional information to support your report, please either report individual relevant messages or send an email to contact@revolt.chat.`,
|
|
clarify: (ref) =>
|
|
`Your report (${ref}) needs clarification, please provide additional information.`,
|
|
acknowledged: (ref) =>
|
|
`Your report (${ref}) has been acknowledged, we will be monitoring the situation.`,
|
|
default: (ref) =>
|
|
`Report (${ref})\n\nNo template found for rejection reason, please specify.`,
|
|
};
|
|
|
|
export function ReportActions({
|
|
report,
|
|
reference,
|
|
}: {
|
|
report: ReportDocument;
|
|
reference: string;
|
|
}) {
|
|
const { toast } = useToast();
|
|
const [reportDraft, setDraft] = useState(report);
|
|
const [availableCases, setAvailableCases] = useState<CaseDocument[]>([]);
|
|
|
|
function rejectHandler(reason: string) {
|
|
return async () => {
|
|
try {
|
|
const $set = await rejectReport(report._id, reason);
|
|
setDraft((report) => ({ ...report, ...$set }));
|
|
toast({
|
|
title: "Rejected report",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to reject report",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ReportCard report={reportDraft} />
|
|
|
|
<Textarea
|
|
cols={8}
|
|
placeholder="Enter notes here... (save on unfocus)"
|
|
className="!min-h-0 !h-[76px]"
|
|
defaultValue={report.notes}
|
|
onBlur={async (e) => {
|
|
const notes = e.currentTarget.value;
|
|
if (notes === reportDraft.notes ?? "") return;
|
|
|
|
try {
|
|
await updateReportNotes(report._id, notes);
|
|
setDraft((report) => ({ ...report, notes }));
|
|
toast({
|
|
title: "Updated report notes",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to update report notes",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{reportDraft.case_id ? (
|
|
<Button
|
|
variant="destructive"
|
|
onClick={async () => {
|
|
try {
|
|
const $set = await assignReportToCase(report._id);
|
|
setDraft((report) => ({ ...report, ...$set }));
|
|
toast({
|
|
title: "Removed report from case",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to resolve report",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Remove from case
|
|
</Button>
|
|
) : (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
fetchOpenCases().then(setAvailableCases);
|
|
}}
|
|
>
|
|
Add to case
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80">
|
|
<div className="grid gap-4">
|
|
<div className="space-y-2">
|
|
<h4 className="font-medium leading-none">Open Cases</h4>
|
|
{availableCases.map((entry) => (
|
|
<a
|
|
key={entry._id}
|
|
onClick={async () => {
|
|
try {
|
|
const $set = await assignReportToCase(
|
|
report._id,
|
|
entry._id
|
|
);
|
|
setDraft((report) => ({ ...report, ...$set }));
|
|
toast({
|
|
title: "Assigned report to case",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to resolve report",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<CaseCard entry={entry} />
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
|
|
<div className="flex gap-2">
|
|
{reportDraft.status === "Created" ? (
|
|
<>
|
|
<Button
|
|
className="flex-1 bg-green-400 hover:bg-green-300"
|
|
onClick={async () => {
|
|
try {
|
|
const $set = await resolveReport(report._id);
|
|
setDraft((report) => ({ ...report, ...$set }));
|
|
toast({
|
|
title: "Resolved report",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to resolve report",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Resolve
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button className="flex-1 bg-red-400 hover:bg-red-300">
|
|
Reject
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
const reason = prompt("Enter a custom reason:");
|
|
// TODO: modal
|
|
reason && rejectHandler(reason)();
|
|
}}
|
|
>
|
|
Custom Reason
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuLabel>Presets</DropdownMenuLabel>
|
|
<DropdownMenuItem onClick={rejectHandler("invalid")}>
|
|
Invalid
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={rejectHandler("false")}>
|
|
False or Spam
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={rejectHandler("duplicate")}>
|
|
Duplicate
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={rejectHandler("not enough evidence")}
|
|
>
|
|
Not Enough Evidence
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={rejectHandler("clarify")}>
|
|
Request Clarification
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={rejectHandler("acknowledged")}>
|
|
Acknowledge Only
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={rejectHandler("ignore")}>
|
|
Ignore
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Button
|
|
className="flex-1"
|
|
onClick={async () => {
|
|
try {
|
|
const $set = await reopenReport(report._id);
|
|
setDraft((report) => ({ ...report, ...$set }));
|
|
toast({
|
|
title: "Opened report again",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to re-open report",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Re-open
|
|
</Button>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button className="flex-1">Send resolution notification</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Send Notification</AlertDialogTitle>
|
|
<AlertDialogDescription className="flex flex-col gap-2">
|
|
<span>
|
|
This will send a message from the Platform Moderation
|
|
account.
|
|
</span>
|
|
<Textarea
|
|
id="notification-content"
|
|
defaultValue={(
|
|
template[
|
|
reportDraft.status === "Rejected"
|
|
? reportDraft.rejection_reason
|
|
: "resolved"
|
|
] ?? template["default"]
|
|
)(reference)}
|
|
/>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => {
|
|
const msg = (
|
|
document.getElementById(
|
|
"notification-content"
|
|
) as HTMLTextAreaElement
|
|
).value;
|
|
if (!msg) return;
|
|
|
|
sendAlert(report.author_id, msg)
|
|
.then(() => toast({ title: "Sent Alert" }))
|
|
.catch((err) =>
|
|
toast({
|
|
title: "Failed to send alert!",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
})
|
|
);
|
|
}}
|
|
>
|
|
Send
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|