diff --git a/src/components/AccountWarning/CannedMessages.ts b/src/components/AccountWarning/CannedMessages.ts
index 92f62a68f6..56727fb08e 100644
--- a/src/components/AccountWarning/CannedMessages.ts
+++ b/src/components/AccountWarning/CannedMessages.ts
@@ -90,8 +90,7 @@ Thank you for your report, '{{reported}}' has been given a formal warning about
llm_pgettext(
"Acknowledgement message to a user",
`
-Thank you for bringing the possible instance of '{{reported}}' abandoning the game to
-our attention.
+Thank you for bringing the possible instance of '{{reported}}' abandoning the game to our attention.
We looked into the game and did not see them failing to finish the game properly.
diff --git a/src/components/IncidentReportTracker/IncidentReportCard.tsx b/src/components/IncidentReportTracker/IncidentReportCard.tsx
index a21545b362..a5057ace7c 100644
--- a/src/components/IncidentReportTracker/IncidentReportCard.tsx
+++ b/src/components/IncidentReportTracker/IncidentReportCard.tsx
@@ -33,12 +33,22 @@ import { Player } from "@/components/Player";
import { errorAlerter } from "@/lib/misc";
import { AutoTranslate } from "@/components/AutoTranslate";
-import { Report } from "@/lib/report_util";
+import { ReportNotification } from "@/lib/report_util";
import { useUser } from "@/lib/hooks";
import { report_categories } from "@/components/Report";
import { openReportedConversationModal } from "@/components/ReportedConversationModal";
-function getReportType(report: Report): string {
+export type ActionableReport = ReportNotification & {
+ unclaim: () => void;
+ good_report: () => void;
+ bad_report: () => void;
+ steal: () => void;
+ claim: () => void;
+ cancel: () => void;
+ set_note: () => void;
+};
+
+function getReportType(report: ActionableReport): string {
if (report.report_type === "appeal") {
return "Ban Appeal";
}
@@ -49,7 +59,7 @@ function getReportType(report: Report): string {
}
interface IncidentReportCardProps {
- report: Report;
+ report: ActionableReport;
}
export function IncidentReportCard({ report }: IncidentReportCardProps): React.ReactElement {
@@ -178,13 +188,13 @@ export function IncidentReportCard({ report }: IncidentReportCardProps): React.R
)}
- {report.reported_conversation && (
+ {report.reported_conversation && report.reported_user && (
{
openReportedConversationModal(
- report.reported_user?.id,
- report.reported_conversation,
+ report.reported_user!.id,
+ report.reported_conversation!,
);
}}
>
diff --git a/src/components/IncidentReportTracker/IncidentReportList.tsx b/src/components/IncidentReportTracker/IncidentReportList.tsx
index c1f3d46c9f..7b96bf8a98 100644
--- a/src/components/IncidentReportTracker/IncidentReportList.tsx
+++ b/src/components/IncidentReportTracker/IncidentReportList.tsx
@@ -21,16 +21,17 @@ import { alert } from "@/lib/swal_config";
import { post } from "@/lib/requests";
import { ignore, errorAlerter } from "@/lib/misc";
-import { Report } from "@/lib/report_util";
+import { ReportNotification } from "@/lib/report_util";
-import { IncidentReportCard } from "./IncidentReportCard";
+import { IncidentReportCard, ActionableReport } from "./IncidentReportCard";
-// Define a type for the props
type IncidentReportListProps = {
- reports: Report[];
+ reports: ReportNotification[];
modal?: boolean;
};
+// This presents a list of incident reports with a summary and actions.
+// It's intended to be built from Report Notifications that we get from the term server via report_manager
export function IncidentReportList({
reports,
modal = true,
@@ -40,14 +41,16 @@ export function IncidentReportList({
}
// Attach appropriate actions to each report
- reports.forEach((report) => {
- if (report.state !== "resolved") {
- report.unclaim = () => {
+ const actionableReports: ActionableReport[] = reports.map((report) => {
+ const actionableReport = report as ActionableReport;
+
+ if (actionableReport.state !== "resolved") {
+ actionableReport.unclaim = () => {
post(`moderation/incident/${report.id}`, { id: report.id, action: "unclaim" })
.then(ignore)
.catch(errorAlerter);
};
- report.good_report = () => {
+ actionableReport.good_report = () => {
post(`moderation/incident/${report.id}`, {
id: report.id,
action: "resolve",
@@ -56,7 +59,7 @@ export function IncidentReportList({
.then(ignore)
.catch(errorAlerter);
};
- report.bad_report = () => {
+ actionableReport.bad_report = () => {
post(`moderation/incident/${report.id}`, {
id: report.id,
action: "resolve",
@@ -65,7 +68,7 @@ export function IncidentReportList({
.then(ignore)
.catch(errorAlerter);
};
- report.steal = () => {
+ actionableReport.steal = () => {
post(`moderation/incident/${report.id}`, { id: report.id, action: "steal" })
.then((res) => {
if (res.vanished) {
@@ -74,7 +77,7 @@ export function IncidentReportList({
})
.catch(errorAlerter);
};
- report.claim = () => {
+ actionableReport.claim = () => {
post(`moderation/incident/${report.id}`, { id: report.id, action: "claim" })
.then((res) => {
if (res.vanished) {
@@ -86,17 +89,17 @@ export function IncidentReportList({
})
.catch(errorAlerter);
};
- report.cancel = () => {
+ actionableReport.cancel = () => {
post(`moderation/incident/${report.id}`, { id: report.id, action: "cancel" })
.then(ignore)
.catch(errorAlerter);
};
- report.set_note = () => {
+ actionableReport.set_note = () => {
void alert
.fire({
input: "text",
- inputValue: report.moderator_note,
+ inputValue: report.moderator_note || "",
showCancelButton: true,
})
.then(({ value: txt, isConfirmed }) => {
@@ -112,13 +115,14 @@ export function IncidentReportList({
});
};
}
+ return actionableReport;
});
return (
diff --git a/src/lib/report_manager.tsx b/src/lib/report_manager.tsx
index 73e999796a..108c650a8f 100644
--- a/src/lib/report_manager.tsx
+++ b/src/lib/report_manager.tsx
@@ -27,20 +27,22 @@ import { toast } from "@/lib/toast";
import { alert } from "@/lib/swal_config";
import { socket } from "@/lib/sockets";
import { pgettext } from "@/lib/translate";
-import { Report, community_mod_can_handle } from "@/lib/report_util";
+import { ReportNotification, community_mod_can_handle } from "@/lib/report_util";
import { EventEmitter } from "eventemitter3";
import { emitNotification } from "@/components/Notifications";
import { browserHistory } from "@/lib/ogsHistory";
import { get, post } from "@/lib/requests";
import { MODERATOR_POWERS } from "./moderation";
+type ReportDetail = rest_api.moderation.ReportDetail;
+
export interface ReportRelation {
relationship: string;
- report: Report;
+ report: ReportNotification;
}
interface Events {
- "incident-report": (report: Report) => void;
+ "incident-report": (report: ReportNotification) => void;
"active-count": (count: number) => void;
update: () => void;
}
@@ -50,8 +52,8 @@ interface Events {
let post_connect_notification_squelch = true;
class ReportManager extends EventEmitter {
- active_incident_reports: { [id: string]: Report } = {};
- sorted_active_incident_reports: Report[] = [];
+ active_incident_reports: { [id: string]: ReportNotification } = {};
+ sorted_active_incident_reports: ReportNotification[] = [];
this_user_reported_games: number[] = [];
constructor() {
@@ -72,7 +74,7 @@ class ReportManager extends EventEmitter {
}
socket.on("incident-report", (report) =>
- this.updateIncidentReport(report as any as Report),
+ this.updateIncidentReport(report as any as ReportNotification),
);
preferences.watch("moderator.report-settings", () => {
@@ -84,11 +86,10 @@ class ReportManager extends EventEmitter {
});
}
- public updateIncidentReport(report: Report) {
+ public updateIncidentReport(report: ReportNotification) {
const user = data.get("user");
report.id = parseInt(report.id as unknown as string);
- //console.log("updateIncidentReport", report);
if (!(report.id in this.active_incident_reports)) {
if (
data.get("user").is_moderator &&
@@ -143,7 +144,7 @@ class ReportManager extends EventEmitter {
const prefs = preferences.get("moderator.report-settings");
const user = data.get("user");
- const reports: Report[] = [];
+ const reports: ReportNotification[] = [];
let normal_ct = 0;
for (const id in this.active_incident_reports) {
const report = this.active_incident_reports[id];
@@ -163,18 +164,18 @@ class ReportManager extends EventEmitter {
this.emit("update");
}
- public getEligibleReports(): Report[] {
+ public getEligibleReports(): ReportNotification[] {
const quota = preferences.get("moderator.report-quota");
return !quota || this.getHandledTodayCount() < preferences.get("moderator.report-quota")
? this.getAvailableReports()
: // Always show the user their own reports
this.getAvailableReports().filter(
- (report) => report.reporting_user.id === data.get("user").id,
+ (report) => report.reporting_user?.id === data.get("user").id,
);
}
// Clients should use getEligibleReports
- private getAvailableReports(): Report[] {
+ private getAvailableReports(): ReportNotification[] {
const user = data.get("user");
return this.sorted_active_incident_reports.filter((report) => {
if (!report) {
@@ -286,11 +287,7 @@ class ReportManager extends EventEmitter {
this.update();
}
- public async getReport(id: number): Promise {
- if (id in this.active_incident_reports) {
- return this.active_incident_reports[id];
- }
-
+ public async getReportDetails(id: number): Promise {
const res = await get(`moderation/incident/${id}`);
if (res) {
@@ -300,7 +297,7 @@ class ReportManager extends EventEmitter {
throw new Error("Report not found");
}
- public async reopen(report_id: number): Promise {
+ public async reopen(report_id: number): Promise {
const res = await post(`moderation/incident/${report_id}`, {
id: report_id,
action: "reopen",
@@ -308,7 +305,7 @@ class ReportManager extends EventEmitter {
this.updateIncidentReport(res);
return res;
}
- public async close(report_id: number, helpful: boolean): Promise {
+ public async close(report_id: number, helpful: boolean): Promise {
delete this.active_incident_reports[report_id];
this.update();
const res = await post(`moderation/incident/${report_id}`, {
@@ -319,17 +316,17 @@ class ReportManager extends EventEmitter {
this.updateIncidentReport(res);
return res;
}
- public async good_report(report_id: number): Promise {
+ public async good_report(report_id: number): Promise {
const res = await this.close(report_id, true);
this.updateIncidentReport(res);
return res;
}
- public async bad_report(report_id: number): Promise {
+ public async bad_report(report_id: number): Promise {
const res = await this.close(report_id, false);
this.updateIncidentReport(res);
return res;
}
- public async unclaim(report_id: number): Promise {
+ public async unclaim(report_id: number): Promise {
const res = await post(`moderation/incident/${report_id}`, {
id: report_id,
action: "unclaim",
@@ -337,7 +334,7 @@ class ReportManager extends EventEmitter {
this.updateIncidentReport(res);
return res;
}
- public async claim(report_id: number): Promise {
+ public async claim(report_id: number): Promise {
const res = await post(`moderation/incident/${report_id}`, {
id: report_id,
action: "claim",
@@ -359,7 +356,7 @@ class ReportManager extends EventEmitter {
voted_action: string,
escalation_note: string,
dissenter_note: string,
- ): Promise {
+ ): Promise {
return post(`moderation/incident/${report_id}`, {
action: "vote",
voted_action: voted_action,
@@ -397,7 +394,7 @@ class ReportManager extends EventEmitter {
}
}
-function compare_reports(a: Report, b: Report): number {
+function compare_reports(a: ReportNotification, b: ReportNotification): number {
const prefs = preferences.get("moderator.report-settings");
const sort_order = preferences.get("moderator.report-sort-order");
const user = data.get("user");
@@ -411,13 +408,13 @@ function compare_reports(a: Report, b: Report): number {
return custom_ordering || (sort_order === "newest-first" ? b.id - a.id : a.id - b.id);
}
if (a.moderator && !b.moderator) {
- if (a.moderator.id === user.id) {
+ if (a.moderator?.id === user.id) {
return A_BEFORE_B;
}
return B_BEFORE_A;
}
if (b.moderator && !a.moderator) {
- if (b.moderator.id === user.id) {
+ if (b.moderator?.id === user.id) {
return B_BEFORE_A;
}
return A_BEFORE_B;
@@ -426,10 +423,10 @@ function compare_reports(a: Report, b: Report): number {
// both have moderators, sort our mod reports first, then other
// mods, then by id
- if (a.moderator.id !== user.id && b.moderator.id === user.id) {
+ if (a.moderator?.id !== user.id && b.moderator?.id === user.id) {
return B_BEFORE_A;
}
- if (a.moderator.id === user.id && b.moderator.id !== user.id) {
+ if (a.moderator?.id === user.id && b.moderator?.id !== user.id) {
return A_BEFORE_B;
}
diff --git a/src/lib/report_util.ts b/src/lib/report_util.ts
index e38079e60b..3bbaefa6bc 100644
--- a/src/lib/report_util.ts
+++ b/src/lib/report_util.ts
@@ -15,68 +15,14 @@
* along with this program. If not, see .
*/
-import { ReportedConversation } from "@/components/Report";
-import { PlayerCacheEntry } from "@/lib/player_cache";
import { MODERATOR_POWERS } from "@/lib/moderation";
import { ReportType } from "@/components/Report";
-interface Vote {
- voter_id: number;
- action: string;
- updated: string;
-}
-
-export interface Report {
- // TBD put this into /models, in a suitable namespace?
- // TBD: relationship between this and SeverToClient['incident-report']
- id: number;
- created: string;
- updated: string;
- state: string;
- escalated: boolean;
- escalated_at: string;
- retyped: boolean;
- source: string;
- report_type: ReportType;
- reporting_user: PlayerCacheEntry;
- reported_user: PlayerCacheEntry;
- reported_game: number;
- reported_game_move?: number;
- reported_review: number;
- reported_conversation: ReportedConversation;
- url: string;
- moderator: PlayerCacheEntry;
- cleared_by_user: boolean;
- was_helpful: boolean;
- reporter_note: string;
- reporter_note_translation: {
- source_language: string;
- target_language: string;
- source_text: string;
- target_text: string;
- };
- moderator_note: string;
- system_note: string;
- detected_ai_games: Array
);
diff --git a/src/views/ReportsCenter/ReportsViewer.styl b/src/views/ReportsCenter/ReportsViewer.styl
new file mode 100644
index 0000000000..45dfd126e5
--- /dev/null
+++ b/src/views/ReportsCenter/ReportsViewer.styl
@@ -0,0 +1,29 @@
+.ReportsViewer {
+ display: block;
+ width: 100%;
+ height: 100%;
+ max-width: 100%;
+ clear: both;
+
+ .hide {
+ visibility: hidden;
+ }
+
+ .view-report-header {
+ margin-left: 1rem;
+ margin-bottom: 1rem;
+
+ .report-id {
+ margin-right: 1rem;
+ }
+
+ button {
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+ }
+
+ .reports-center-selected-report {
+ padding-left: 0.5rem;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/views/ReportsCenter/ReportsViewer.tsx b/src/views/ReportsCenter/ReportsViewer.tsx
new file mode 100644
index 0000000000..287ee759ea
--- /dev/null
+++ b/src/views/ReportsCenter/ReportsViewer.tsx
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) Online-Go.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ */
+
+import * as React from "react";
+import Select from "react-select";
+import { useUser } from "@/lib/hooks";
+import { ReportNotification } from "@/lib/report_util";
+import { report_manager } from "@/lib/report_manager";
+import { _ } from "@/lib/translate";
+import * as DynamicHelp from "react-dynamic-help";
+import { PlayerCacheEntry } from "@/lib/player_cache";
+import { get } from "@/lib/requests";
+import { errorAlerter } from "@/lib/misc";
+import { ViewReport } from "@/views/ReportsCenter/ViewReport";
+
+interface ViewReportHeaderProps {
+ reports: ReportNotification[];
+ report_id: number;
+ selectReport: (report_id: number) => void;
+}
+
+let cached_moderators: PlayerCacheEntry[] = [];
+
+// Provides navigation around a set of report-notifications, with a full view of the current report
+export function ReportsViewer({
+ reports,
+ report_id,
+ selectReport,
+}: ViewReportHeaderProps): React.ReactElement {
+ const user = useUser();
+ const { registerTargetItem } = React.useContext(DynamicHelp.Api);
+ const { ref: ignore_button } = registerTargetItem("ignore-button");
+
+ const current_report = reports.find((r) => r.id === report_id);
+ const claimed_by_me = current_report?.moderator?.id === user.id;
+
+ React.useEffect(() => {
+ if (cached_moderators.length === 0) {
+ get("players/?is_moderator=true&page_size=100")
+ .then((res) => {
+ cached_moderators = res.results.sort(
+ (a: PlayerCacheEntry, b: PlayerCacheEntry) => {
+ if (a.id === user.id) {
+ return -1;
+ }
+ if (b.id === user.id) {
+ return 1;
+ }
+ return a.username!.localeCompare(b.username as string);
+ },
+ );
+ })
+ .catch(errorAlerter);
+ }
+ }, []);
+
+ const claimReport = () => {
+ if (!report_id) {
+ return;
+ }
+ if (user.is_moderator) {
+ void report_manager.claim(report_id);
+ }
+ };
+
+ const next = () => {
+ const currentIndex = reports.findIndex((r) => r.id === report_id);
+ if (currentIndex + 1 < reports.length) {
+ selectReport(reports[currentIndex + 1].id);
+ } else {
+ selectReport(0);
+ }
+ };
+
+ const prev = () => {
+ const currentIndex = reports.findIndex((r) => r.id === report_id);
+ if (currentIndex > 0) {
+ selectReport(reports[currentIndex - 1].id);
+ }
+ };
+
+ const currentIndex = reports.findIndex((r) => r.id === report_id);
+ const hasPrev = currentIndex > 0;
+ const hasNext = currentIndex + 1 < reports.length;
+
+ if (report_id === 0) {
+ return (
+
+
All done!
+
+ );
+ }
+
+ return (
+
+
+ {current_report ? (
+
+
+
+ );
+}
+
+function R(id: number): string {
+ return "R" + `${id}`.slice(-3);
+}
diff --git a/src/views/ReportsCenter/ViewReport.styl b/src/views/ReportsCenter/ViewReport.styl
index 035067493b..e74a2f03ef 100644
--- a/src/views/ReportsCenter/ViewReport.styl
+++ b/src/views/ReportsCenter/ViewReport.styl
@@ -33,27 +33,42 @@
}
}
- .reporting-user {
- float: right;
- font-size: 0.8rem;
- font-weight: 400;
- }
- }
-
- .view-report-header {
- margin-bottom: 1 rem;
+ .users-header-right {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
- .report-id {
- margin-right: 1rem;
- }
+ .moderator-selector > span {
+ padding-right: 0.5rem;
+ }
- button {
- margin-left: 0.5rem;
- margin-right: 0.5rem;
- }
+ #ViewReportSelectModerator {
+ font-size: 0.8rem;
+ font-weight: 400;
+ display: inline-block;
+ width: 10rem;
+
+ .reports-center-assigned-moderator-container {
+ display: inline-flex;
+ align-items: center;
+ padding-left: 0.5rem;
+ }
+
+ // Force moderator selector to be vertically small:
+ // get rid of padding and min-height of internals
+ .ogs-react-select__control {
+ min-height: 0;
+
+ .ogs-react-select__dropdown-indicator {
+ padding: 0.2rem 0.5rem 0.2rem 0.5rem;
+ }
+ }
+ }
- .reports-center-selected-report {
- padding-left: 0.5rem;
+ .reporting-user {
+ font-size: 0.8rem;
+ font-weight: 400;
+ }
}
}
@@ -172,6 +187,12 @@
.notes {
flex-basis: 50%;
white-space: pre-wrap;
+
+ h4>span {
+ padding-left: 2rem;
+ font-style: italic;
+ font-size: smaller;
+ }
}
.voting {
diff --git a/src/views/ReportsCenter/ViewReport.tsx b/src/views/ReportsCenter/ViewReport.tsx
index fac80110e4..4d203e95fa 100644
--- a/src/views/ReportsCenter/ViewReport.tsx
+++ b/src/views/ReportsCenter/ViewReport.tsx
@@ -17,201 +17,162 @@
import * as React from "react";
import moment from "moment";
-import * as ReactSelect from "react-select";
import Select from "react-select";
+
import { useUser } from "@/lib/hooks";
import { report_categories, ReportType } from "@/components/Report";
import { report_manager } from "@/lib/report_manager";
-import { Report } from "@/lib/report_util";
+import { ReportNotification } from "@/lib/report_util";
import { AutoTranslate } from "@/components/AutoTranslate";
import { interpolate, _, pgettext, llm_pgettext } from "@/lib/translate";
import { Player } from "@/components/Player";
import { Link } from "react-router-dom";
-import { post } from "@/lib/requests";
-import { PlayerCacheEntry } from "@/lib/player_cache";
+import { post, get } from "@/lib/requests";
import { errorAlerter, ignore } from "@/lib/misc";
import { UserHistory } from "./UserHistory";
import { ReportedGame } from "./ReportedGame";
import { AppealView } from "./AppealView";
-import { get } from "@/lib/requests";
import { MessageTemplate, WARNING_TEMPLATES, REPORTER_RESPONSE_TEMPLATES } from "./MessageTemplate";
import { ModerationActionSelector } from "./ModerationActionSelector";
import { openAnnulQueueModal, AnnulQueueModal } from "@/components/AnnulQueueModal";
import { ReportTypeSelector } from "./ReportTypeSelector";
import { alert } from "@/lib/swal_config";
import { ErrorBoundary } from "@/components/ErrorBoundary";
-import * as DynamicHelp from "react-dynamic-help";
+
import { MODERATOR_POWERS } from "@/lib/moderation";
import { KBShortcut } from "@/components/KBShortcut";
import { GobanRenderer } from "goban";
import { ReportContext } from "@/contexts/ReportContext";
+import { PlayerCacheEntry } from "@/lib/player_cache";
+import { useEffect } from "react";
+
+type ReportDetail = rest_api.moderation.ReportDetail;
interface ViewReportProps {
- reports: Report[];
- onChange: (report_id: number) => void;
report_id: number;
+ advanceToNextReport: () => void;
}
-let cached_moderators: PlayerCacheEntry[] = [];
-
// Used for saving updates to the report
let report_note_id = 0;
let report_note_text = "";
let report_note_update_timeout: ReturnType | null = null;
-export function ViewReport({ report_id, reports, onChange }: ViewReportProps): React.ReactElement {
+export function ViewReport({
+ report_id,
+ advanceToNextReport,
+}: ViewReportProps): React.ReactElement {
const user = useUser();
- const [moderatorNote, setModeratorNote] = React.useState("");
- const [moderators, setModerators] = React.useState(cached_moderators);
- const [report, setReport] = React.useState(null);
- const [error, setError] = React.useState(null);
- const [moderator_id, setModeratorId] = React.useState(
- report?.moderator?.id,
- );
- const [reportState, setReportState] = React.useState(report?.state);
+ const [report, setReport] = React.useState(null);
+ const [usersVote, setUsersVote] = React.useState(null);
const [isAnnulQueueModalOpen, setIsAnnulQueueModalOpen] = React.useState(false);
const [annulQueue, setAnnulQueue] = React.useState(
report?.detected_ai_games,
);
- const [availableActions, setAvailableActions] = React.useState(null);
- const [voteCounts, setVoteCounts] = React.useState<{ [action: string]: number }>({});
- const [usersVote, setUsersVote] = React.useState(null);
const [currentGoban, setCurrentGoban] = React.useState(null);
+ const [moderators, setModerators] = React.useState([]);
+
+ // Although moderator_id is a field on the report, we control the value we're using
+ // to display separately so we can update it without having to wait for the report to update
+ // when the user changes it.
+ const [moderator_id, setModeratorId] = React.useState(null);
const related = report_manager.getRelatedReports(report_id);
- const { registerTargetItem } = React.useContext(DynamicHelp.Api);
- const { ref: ignore_button } = registerTargetItem("ignore-button");
+ const [modNoteNeedsSave, setHasUnsavedChanges] = React.useState(false);
- const captureReport = (report: Report) => {
+ const updateReportState = (report: ReportDetail) => {
setReport(report);
- setModeratorId(report?.moderator?.id);
- setReportState(report?.state);
- setAnnulQueue(report?.detected_ai_games);
- if (report?.available_actions !== null) {
- setAvailableActions(report.available_actions);
- }
- setVoteCounts(report?.vote_counts);
setUsersVote(report?.voters?.find((v) => v.voter_id === user.id)?.action ?? null);
+ setModeratorId(report?.moderator?.id ?? null);
};
- React.useEffect(() => {
- if (report_id) {
- // For some reason we have to capture the state of the report at the time that report_id goes valid
- // It's not clear why, but there are subsequent renders where the report state goes away, so ...
- // capture what you want to use here! ...
- report_manager
- .getReport(report_id)
- .then((report) => {
- setError(null);
- captureReport(report);
- })
- .catch((err) => {
- console.error(err);
- setError(err + "");
- });
- } else {
- setReport(null);
- setError(null);
- setModeratorId(null);
- setReportState(null);
+ const fetchAndUpdateReport = async (reportId: number) => {
+ try {
+ const report = await report_manager.getReportDetails(reportId);
+ console.log("got report", report.moderator_note);
+ updateReportState(report);
+ } catch (error) {
+ errorAlerter(error);
}
- }, [report_id]);
+ };
- React.useEffect(() => {
- const onUpdate = (r: Report) => {
- if (r.id === report?.id) {
- captureReport(r);
+ useEffect(() => {
+ console.log("report_id", report_id);
+ if (report_id === 0) {
+ return;
+ }
+
+ void fetchAndUpdateReport(report_id);
+
+ const onUpdate = async (r: ReportNotification) => {
+ if (r.id === report_id) {
+ await fetchAndUpdateReport(report_id);
}
};
+
report_manager.on("incident-report", onUpdate);
+
return () => {
report_manager.off("incident-report", onUpdate);
};
- }, [report]);
+ }, [report_id]);
React.useEffect(() => {
- if (cached_moderators.length === 0 || moderators.length === 0) {
+ if (moderators.length === 0) {
get("players/?is_moderator=true&page_size=100")
.then((res) => {
- cached_moderators = res.results.sort(
- (a: PlayerCacheEntry, b: PlayerCacheEntry) => {
- if (a.id === user.id) {
- return -1;
- }
- if (b.id === user.id) {
- return 1;
- }
- return a.username!.localeCompare(b.username as string);
- },
+ setModerators(
+ res.results.sort((a: PlayerCacheEntry, b: PlayerCacheEntry) =>
+ a.username!.localeCompare(b.username as string),
+ ),
);
- setModerators(cached_moderators);
})
.catch(errorAlerter);
}
}, []);
- React.useEffect(() => {
- setModeratorId(report?.moderator?.id);
- }, [report?.moderator?.id]);
-
- React.useEffect(() => {
- if (document.activeElement?.nodeName !== "TEXTAREA") {
- setModeratorNote(report?.moderator_note || "");
- }
- }, [report?.moderator_note]);
-
- React.useEffect(() => {
- setReportState(report?.state);
- }, [report?.state]);
-
const setAndSaveModeratorNote = React.useCallback(
(event: React.ChangeEvent) => {
if (!report) {
return;
}
- setModeratorNote(event.target.value);
-
- if (report_note_id !== 0 && report_note_id !== report.id) {
- window.alert(
- "Hold your horses, already saving an update, though you should never see this contact anoek",
- );
- } else {
- report_note_id = report.id;
- report_note_text = event.target.value;
-
- if (!report_note_update_timeout) {
- report_note_update_timeout = setTimeout(() => {
- post(`moderation/incident/${report.id}`, {
- id: report.id,
- action: "note",
- note: report_note_text,
- })
- .then(ignore)
- .catch(errorAlerter);
- report_note_id = 0;
- report_note_text = "";
- report_note_update_timeout = null;
- }, 250);
- }
- }
- },
- [report],
- );
- const assignToModerator = React.useCallback(
- (id: number) => {
- if (!report) {
- return;
+ const prevNote = report.moderator_note || "";
+ const newlineCount = (str: string) => (str.match(/\n/g) || []).length;
+
+ if (newlineCount(event.target.value) > newlineCount(prevNote)) {
+ if (report_note_id !== 0 && report_note_id !== report.id) {
+ window.alert(
+ "Hold your horses, already saving an update, though you should never see this contact anoek",
+ );
+ } else {
+ console.log("saving note", event.target.value);
+ report_note_id = report.id;
+ report_note_text = event.target.value;
+
+ if (!report_note_update_timeout) {
+ report_note_update_timeout = setTimeout(() => {
+ post(`moderation/incident/${report.id}`, {
+ id: report.id,
+ action: "note",
+ note: report_note_text,
+ })
+ .then(() => {
+ setHasUnsavedChanges(false);
+ })
+ .catch(errorAlerter);
+ report_note_id = 0;
+ report_note_text = "";
+ report_note_update_timeout = null;
+ }, 250);
+ }
+ }
}
- setModeratorId(id);
- post(`moderation/incident/${report.id}`, {
- id: report.id,
- action: "assign",
- moderator_id: id,
- })
- .then(ignore)
- .catch(errorAlerter);
+ setHasUnsavedChanges(event.target.value !== prevNote);
+ setReport((prevReport) =>
+ prevReport ? { ...prevReport, moderator_note: event.target.value } : null,
+ );
},
[report],
);
@@ -221,8 +182,13 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): R
return;
}
if (report.moderator?.id !== user.id && user.is_moderator) {
- setReportState("claimed");
- void report_manager.claim(report.id);
+ report.state = "claimed";
+ report_manager
+ .claim(report.id)
+ .then(() => {
+ setModeratorId(user.id ?? null);
+ })
+ .catch(errorAlerter);
}
};
@@ -250,44 +216,8 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): R
);
}
- if (error) {
- return (
-
-
{error}
-
- );
- }
-
const category = report_categories.find((c) => c.type === report.report_type);
const claimed_by_me = report.moderator?.id === user.id;
- const report_in_reports = reports.find((r) => r.id === report.id);
- let next_report: Report | null = null;
- let prev_report: Report | null = null;
- for (let i = 0; i < reports.length; i++) {
- if (reports[i].id === report.id) {
- if (i + 1 < reports.length) {
- next_report = reports[i + 1];
- }
- if (i - 1 >= 0) {
- prev_report = reports[i - 1];
- }
- break;
- }
- }
-
- const next = () => {
- if (next_report) {
- onChange(next_report.id);
- } else {
- onChange(0);
- }
- };
-
- const prev = () => {
- if (prev_report) {
- onChange(prev_report.id);
- }
- };
const handleCloseAnnulQueueModal = () => {
setIsAnnulQueueModalOpen(false);
@@ -317,9 +247,7 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): R
new_type: new_type,
})
.then((_res) => {
- // We need to move on to the next report, because this one is getting updated in the
- // back end, and we'll get it back via that route, not by directly manipulating it here.
- next();
+ advanceToNextReport();
})
.catch(errorAlerter);
}
@@ -349,164 +277,9 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): R
player={report.reported_user}
/>
)}
-
- {report_in_reports ? (
-
-
-
-
+
+
+
-
-
-
- {pgettext(
- "A label for the user name that reported an incident (followed by colon and the username)",
- "Reported by",
- )}
- :
- {report.reporting_user ? (
-
- ) : (
- "System"
- )}
- {moment(report.created).fromNow()}
-
-
+ )}
+
+
+ {pgettext(
+ "A label for the user name that reported an incident (followed by colon and the username)",
+ "Reported by",
+ )}
+ :
+ {report.reporting_user ? (
+
+ ) : (
+ "System"
+ )}
+ {moment(report.created).fromNow()}
+
+
@@ -584,9 +423,12 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): R
{(user.is_moderator || null) && (
-
Moderator Notes
+
+ Moderator Notes{" "}
+ {modNoteNeedsSave && (hit enter to save)}
+
@@ -606,13 +448,13 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): R
{!user.is_moderator && user.moderator_powers && (