Inappropriate nickname report help

This commit is contained in:
Kalle
2026-08-22 16:45:02 +03:00
parent ab2c323abf
commit da26be23e2
40 changed files with 221 additions and 8 deletions

View File

@@ -0,0 +1,7 @@
.instructions {
margin: var(--s-1) 0 0;
padding-inline-start: var(--s-4);
display: flex;
flex-direction: column;
gap: var(--s-1);
}

View File

@@ -4,11 +4,19 @@ import { SendouDialog } from "~/components/elements/Dialog";
import { toastQueue } from "~/components/elements/Toast";
import { FormMessage } from "~/components/FormMessage";
import { SendouForm } from "~/form";
import type { FormRenderProps } from "~/form/SendouForm";
import { useFormValue } from "~/form/SendouForm";
import { userReportPage } from "~/utils/urls";
import { INAPPROPRIATE_NICKNAME_CATEGORY } from "../user-report-constants";
import { reportUserSchema } from "../user-report-schemas";
import styles from "./ReportUserDialog.module.css";
const SENDOUQ_MATCH_ROUTE_ID = "features/sendouq-match/routes/q.match.$id";
type ReportFormFieldComponent = FormRenderProps<
typeof reportUserSchema.entries
>["FormField"];
/**
* Modal for reporting a user to the staff, posting to the `/user-report/:id` resource
* route. Re-reporting the same user overwrites the previous report. Rendered wherever
@@ -35,6 +43,9 @@ export function ReportUserDialog({
schema={reportUserSchema}
action={userReportPage(userId)}
defaultValues={{ matchId: prefilledMatchId }}
hideSubmitButtonWhen={(values) =>
values.category === INAPPROPRIATE_NICKNAME_CATEGORY
}
onSuccess={() => {
toastQueue.add(
{ message: "Report sent to the staff", variant: "success" },
@@ -46,11 +57,7 @@ export function ReportUserDialog({
{({ FormField }) => (
<>
<FormField name="category" />
<FormField name="description" />
<FormField name="matchId" />
<FormMessage type="info">
{t("user:card.report.falseReportsWarning")}
</FormMessage>
<ReportFields FormField={FormField} />
</>
)}
</SendouForm>
@@ -58,6 +65,46 @@ export function ReportUserDialog({
);
}
/**
* The report itself, replaced by instructions when the selected category is one the
* staff can't act on.
*/
function ReportFields({ FormField }: { FormField: ReportFormFieldComponent }) {
const { t } = useTranslation(["user"]);
const category = useFormValue("category");
if (category === INAPPROPRIATE_NICKNAME_CATEGORY) {
return <InappropriateNicknameInstructions />;
}
return (
<>
<FormField name="description" />
<FormField name="matchId" />
<FormMessage type="info">
{t("user:card.report.falseReportsWarning")}
</FormMessage>
</>
);
}
function InappropriateNicknameInstructions() {
const { t } = useTranslation(["user"]);
return (
<FormMessage type="info">
{t("user:card.report.nickname.explanation")}
<ul
className={styles.instructions}
data-testid="nickname-report-instructions"
>
<li>{t("user:card.report.nickname.inGame")}</li>
<li>{t("user:card.report.nickname.discord")}</li>
</ul>
</FormMessage>
);
}
/**
* Reads the SendouQ match id from the current route so a report opened from a match page
* prefills its "Match ID" field. Returns `undefined` on any other page.

View File

@@ -3,6 +3,13 @@ export const USER_REPORT = {
MATCH_ID_MAX_LENGTH: 10,
};
/**
* Category offered in the report dialog but never stored: nicknames are not set on
* sendou.ink, so picking it points the user to Splatoon 3 / Discord instead of
* letting them send a report.
*/
export const INAPPROPRIATE_NICKNAME_CATEGORY = "INAPPROPRIATE_NICKNAME";
export const USER_REPORT_CATEGORIES = [
"INAPPROPRIATE_CONTENT",
"ALTING",

View File

@@ -1,9 +1,12 @@
import * as v from "valibot";
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
import { USER_REPORT_CATEGORIES } from "./user-report-constants";
import { reportUserSchema } from "./user-report-schemas";
export const reportUserSchemaServer = v.objectAsync({
...reportUserSchema.entries,
// the dialog also offers a category that only shows guidance, never a report
category: v.picklist(USER_REPORT_CATEGORIES),
// cast to the concrete value type: the field's nullability makes its inferred
// type a union the async pipe can't resolve
matchId: v.pipeAsync(

View File

@@ -1,7 +1,10 @@
import * as v from "valibot";
import { select, textArea, textFieldOptional } from "~/form/fields";
import { id } from "~/utils/schema";
import { USER_REPORT } from "./user-report-constants";
import {
INAPPROPRIATE_NICKNAME_CATEGORY,
USER_REPORT,
} from "./user-report-constants";
export const reportUserSchema = v.object({
category: select({
@@ -11,6 +14,10 @@ export const reportUserSchema = v.object({
label: "options.userReportCategory.INAPPROPRIATE_CONTENT",
value: "INAPPROPRIATE_CONTENT",
},
{
label: "options.userReportCategory.INAPPROPRIATE_NICKNAME",
value: INAPPROPRIATE_NICKNAME_CATEGORY,
},
{ label: "options.userReportCategory.ALTING", value: "ALTING" },
{ label: "options.userReportCategory.HARASSMENT", value: "HARASSMENT" },
{ label: "options.userReportCategory.CHEATING", value: "CHEATING" },

View File

@@ -79,6 +79,9 @@ const FormContext = React.createContext<FormFieldContextValue | null>(null);
export const EMPTY_FORM_STORE = createFormStore({}, {});
const SUBMIT_ROW_CLASS_NAME =
"mt-4 stack horizontal md mx-auto justify-center items-center";
export interface FormRenderProps<T extends v.ObjectEntries> {
FormField: TypedFormFieldComponent<T>;
}
@@ -113,6 +116,13 @@ type BaseFormProps<T extends v.ObjectEntries> = {
*/
readOnly?: boolean;
secondarySubmit?: React.ReactNode;
/**
* Hides the submit button while the current values match, for forms with a
* branch that has nothing to submit (e.g. a choice that only shows guidance).
*/
hideSubmitButtonWhen?: (
values: Partial<v.InferInput<v.ObjectSchema<T, undefined>>>,
) => boolean;
/**
* Called once after a server submission completes successfully (the action
* returned without field errors). Useful for collapsing an inline edit form
@@ -196,6 +206,7 @@ function SendouFormInner<T extends v.ObjectEntries>({
mode = "submit",
onApply,
secondarySubmit,
hideSubmitButtonWhen,
onSuccess,
}: SendouFormProps<T>) {
const { t } = useTranslation(["forms"]);
@@ -330,7 +341,11 @@ function SendouFormInner<T extends v.ObjectEntries>({
{title ? <h2 className={styles.title}>{title}</h2> : null}
{resolvedChildren}
{mode !== "submit" || readOnly ? null : (
<div className="mt-4 stack horizontal md mx-auto justify-center items-center">
<SubmitRow
hideWhen={
hideSubmitButtonWhen as ((values: unknown) => boolean) | undefined
}
>
<SubmitButton
testId={submitButtonTestId}
state={fetcher.state}
@@ -340,7 +355,7 @@ function SendouFormInner<T extends v.ObjectEntries>({
{submitButtonText ?? t("submit")}
</SubmitButton>
{secondarySubmit}
</div>
</SubmitRow>
)}
{fallbackError ? (
<div className="mt-4 mx-auto" data-testid="fallback-form-error">
@@ -372,6 +387,45 @@ function SendouFormInner<T extends v.ObjectEntries>({
);
}
function SubmitRow({
hideWhen,
children,
}: {
hideWhen: ((values: unknown) => boolean) | undefined;
children: React.ReactNode;
}) {
return hideWhen ? (
<ConditionalSubmitRow hideWhen={hideWhen}>{children}</ConditionalSubmitRow>
) : (
<div className={SUBMIT_ROW_CLASS_NAME}>{children}</div>
);
}
/**
* Split out of {@link SubmitRow} so that only forms that opt in subscribe to the
* form's values (and re-render on every edit).
*/
function ConditionalSubmitRow({
hideWhen,
children,
}: {
hideWhen: (values: unknown) => boolean;
children: React.ReactNode;
}) {
const context = React.useContext(FormContext);
const store = context?.store ?? EMPTY_FORM_STORE;
const getValues = () => store.values;
const values = React.useSyncExternalStore(
store.subscribe,
getValues,
getValues,
);
if (hideWhen(values)) return null;
return <div className={SUBMIT_ROW_CLASS_NAME}>{children}</div>;
}
function createFormStore(
initialValues: Record<string, unknown>,
initialClientErrors: Partial<Record<string, string>>,

View File

@@ -66,6 +66,8 @@ class ReportUserDialog {
this.locators = {
matchIdInput: page.getByLabel("Match ID"),
sentToast: page.getByText("Report sent to the staff"),
submitButton: page.getByTestId("submit-button"),
nicknameInstructions: page.getByTestId("nickname-report-instructions"),
};
}

View File

@@ -51,6 +51,28 @@ test.describe("User report", () => {
await expect(adminPage.text(description)).toBeVisible();
});
test("points elsewhere instead of reporting an inappropriate nickname", async ({
page,
factories,
}) => {
const reporter = await factories.UserFactory.create();
await factories.LFGPostFactory.create({ authorId: NZAP_TEST_ID });
await impersonate(page, reporter.id);
const lfg = new LFGPage(page);
await lfg.goto();
const card = await lfg.openUserCard("N-ZAP");
const reportDialog = await card.openReportDialog();
await expect(reportDialog.locators.submitButton).toBeVisible();
await reportDialog.form.select("category", "INAPPROPRIATE_NICKNAME");
await expect(reportDialog.locators.nicknameInstructions).toBeVisible();
await expect(reportDialog.locators.submitButton).not.toBeVisible();
});
test("prefills the match id when reporting from a match page", async ({
page,
factories,

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -251,6 +251,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -251,6 +251,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "Category",
"labels.reportMatchId": "Match ID",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "Inappropriate content",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "Inappropriate nickname (Splatoon/Discord)",
"options.userReportCategory.ALTING": "Alting",
"options.userReportCategory.HARASSMENT": "Harassment",
"options.userReportCategory.CHEATING": "Cheating",

View File

@@ -251,6 +251,9 @@
"card.editPrivateNote": "Edit private note",
"card.report.header": "Report {{name}}",
"card.report.falseReportsWarning": "Abuse of the reporting system including making false reports can lead to the suspension of your account.",
"card.report.nickname.explanation": "Please report them on the corresponding platform:",
"card.report.nickname.inGame": "Splatoon 3 in-game name: report the player in-game using Splatoon 3's own reporting feature (Battle Logs).",
"card.report.nickname.discord": "Discord username/avatar: report the account to Discord.",
"card.privateNote": "Private note",
"card.xp": "XP",
"card.freeAgent": "FA",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "Categoría",
"labels.reportMatchId": "ID de la partida",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "Contenido inapropiado",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "Multicuentas",
"options.userReportCategory.HARASSMENT": "Acoso",
"options.userReportCategory.CHEATING": "Trampas",

View File

@@ -254,6 +254,9 @@
"card.editPrivateNote": "Editar nota privada",
"card.report.header": "Reportar a {{name}}",
"card.report.falseReportsWarning": "Abusar del sistema de reportes, incluyendo hacer reportes falsos, puede llevar a la suspensión de tu cuenta.",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "Nota privada",
"card.xp": "XP",
"card.freeAgent": "FA",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "Categoría",
"labels.reportMatchId": "ID de la partida",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "Contenido inapropiado",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "Multicuentas",
"options.userReportCategory.HARASSMENT": "Acoso",
"options.userReportCategory.CHEATING": "Trampas",

View File

@@ -254,6 +254,9 @@
"card.editPrivateNote": "Editar nota privada",
"card.report.header": "Reportar a {{name}}",
"card.report.falseReportsWarning": "Abusar del sistema de reportes, incluyendo hacer reportes falsos, puede llevar a la suspensión de tu cuenta.",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "Nota privada",
"card.xp": "XP",
"card.freeAgent": "FA",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -254,6 +254,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -254,6 +254,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -254,6 +254,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -254,6 +254,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -245,6 +245,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -245,6 +245,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -251,6 +251,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -257,6 +257,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -254,6 +254,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -257,6 +257,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",

View File

@@ -218,6 +218,7 @@
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.INAPPROPRIATE_NICKNAME": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",
"options.userReportCategory.CHEATING": "",

View File

@@ -246,6 +246,9 @@
"card.editPrivateNote": "",
"card.report.header": "",
"card.report.falseReportsWarning": "",
"card.report.nickname.explanation": "",
"card.report.nickname.inGame": "",
"card.report.nickname.discord": "",
"card.privateNote": "",
"card.xp": "",
"card.freeAgent": "",