Remove MSC3215 (Report to Moderators) labs feature

T&S does not believe this is used, and intends to replace it during Reporting v2 anyway.

Original MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/3215

New (WIP) MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4468

(This PR/commit doesn't add MSC4468 support because it's unclear if we should use MSC4468 directly or via another endpoint like MSC4457)

[MSC4457]: https://github.com/matrix-org/matrix-spec-proposals/pull/4457
This commit is contained in:
Travis Ralston 2026-05-13 12:15:51 -06:00
parent 8e22bc6e0e
commit 925326e2d9
4 changed files with 13 additions and 341 deletions

View File

@ -1,4 +1,5 @@
/*
Copyright 2026 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
@ -11,25 +12,16 @@ import React, { type JSX, type ChangeEvent } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { _t, UserFriendlyError } from "../../../languageHandler";
import { ensureDMExists } from "../../../createRoom";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import SdkConfig from "../../../SdkConfig";
import Markdown from "../../../Markdown";
import SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import Field from "../elements/Field";
import Spinner from "../elements/Spinner";
import LabelledCheckbox from "../elements/LabelledCheckbox";
declare module "matrix-js-sdk/src/types" {
interface TimelineEvents {
[ABUSE_EVENT_TYPE]: AbuseEventContent;
}
}
interface IProps {
mxEvent: MatrixEvent;
onFinished(report?: boolean): void;
@ -40,8 +32,6 @@ interface IState {
reason: string;
busy: boolean;
err?: string;
// If we know it, the nature of the abuse, as specified by MSC3215.
nature?: ExtendedNature;
ignoreUserToo: boolean; // if true, user will be ignored/blocked on submit
/*
* Whether the room is encrypted.
@ -49,148 +39,18 @@ interface IState {
isRoomEncrypted: boolean;
}
const MODERATED_BY_STATE_EVENT_TYPE = [
"org.matrix.msc3215.room.moderation.moderated_by",
/**
* Unprefixed state event. Not ready for prime time.
*
* "m.room.moderation.moderated_by"
*/
];
export const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
interface AbuseEventContent {
event_id: string;
room_id: string;
moderated_by_id: string;
nature?: ExtendedNature;
reporter: string;
comment: string;
}
// Standard abuse natures.
enum Nature {
Disagreement = "org.matrix.msc3215.abuse.nature.disagreement",
Toxic = "org.matrix.msc3215.abuse.nature.toxic",
Illegal = "org.matrix.msc3215.abuse.nature.illegal",
Spam = "org.matrix.msc3215.abuse.nature.spam",
Other = "org.matrix.msc3215.abuse.nature.other",
}
enum NonStandardValue {
// Non-standard abuse nature.
// It should never leave the client - we use it to fallback to
// server-wide abuse reporting.
Admin = "non-standard.abuse.nature.admin",
}
type ExtendedNature = Nature | NonStandardValue;
type Moderation = {
// The id of the moderation room.
moderationRoomId: string;
// The id of the bot in charge of forwarding abuse reports to the moderation room.
moderationBotUserId: string;
};
/*
* A dialog for reporting an event.
*
* The actual content of the dialog will depend on two things:
*
* 1. Is `feature_report_to_moderators` enabled?
* 2. Does the room support moderation as per MSC3215, i.e. is there
* a well-formed state event `m.room.moderation.moderated_by`
* /`org.matrix.msc3215.room.moderation.moderated_by`?
*/
export default class ReportEventDialog extends React.Component<IProps, IState> {
// If the room supports moderation, the moderation information.
private moderation?: Moderation;
public constructor(props: IProps) {
super(props);
let moderatedByRoomId: string | null = null;
let moderatedByUserId: string | null = null;
if (SettingsStore.getValue("feature_report_to_moderators")) {
// The client supports reporting to moderators.
// Does the room support it, too?
// Extract state events to determine whether we should display
const client = MatrixClientPeg.safeGet();
const room = client.getRoom(props.mxEvent.getRoomId());
for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
const stateEvent = room?.currentState.getStateEvents(stateEventType, stateEventType);
if (!stateEvent) {
continue;
}
if (Array.isArray(stateEvent)) {
// Internal error.
throw new TypeError(
`getStateEvents(${stateEventType}, ${stateEventType}) ` +
"should return at most one state event",
);
}
const event = stateEvent.event;
if (!("content" in event) || typeof event["content"] != "object") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug(
"Moderation error",
"state event",
stateEventType,
"should have an object field `content`, got",
event,
);
continue;
}
const content = event["content"];
if (!("room_id" in content) || typeof content["room_id"] != "string") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug(
"Moderation error",
"state event",
stateEventType,
"should have a string field `content.room_id`, got",
event,
);
continue;
}
if (!("user_id" in content) || typeof content["user_id"] != "string") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug(
"Moderation error",
"state event",
stateEventType,
"should have a string field `content.user_id`, got",
event,
);
continue;
}
moderatedByRoomId = content["room_id"];
moderatedByUserId = content["user_id"];
}
if (moderatedByRoomId && moderatedByUserId) {
// The room supports moderation.
this.moderation = {
moderationRoomId: moderatedByRoomId,
moderationBotUserId: moderatedByUserId,
};
}
}
this.state = {
// A free-form text describing the abuse.
reason: "",
busy: false,
err: undefined,
// If specified, the nature of the abuse, as specified by MSC3215.
nature: undefined,
ignoreUserToo: false, // default false, for now. Could easily be argued as default true
isRoomEncrypted: false, // async, will be set later
};
@ -215,11 +75,6 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
this.setState({ reason });
};
// The user has clicked on a nature.
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ nature: e.currentTarget.value as ExtendedNature });
};
// The user has clicked "cancel".
private onCancel = (): void => {
this.props.onFinished(false);
@ -229,28 +84,13 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
private onSubmit = async (): Promise<void> => {
let reason = this.state.reason || "";
reason = reason.trim();
if (this.moderation) {
// This room supports moderation.
// We need a nature.
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
if (
!this.state.nature ||
((this.state.nature == Nature.Other || this.state.nature == NonStandardValue.Admin) && !reason)
) {
this.setState({
err: _t("report_content|missing_reason"),
});
return;
}
} else {
// This room does not support moderation.
// We need a `reason`.
if (!reason) {
this.setState({
err: _t("report_content|missing_reason"),
});
return;
}
// Reasons are required on the API, even if unhelpful like "."
if (!reason) {
this.setState({
err: _t("report_content|missing_reason"),
});
return;
}
this.setState({
@ -261,28 +101,10 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
try {
const client = MatrixClientPeg.safeGet();
const ev = this.props.mxEvent;
if (this.moderation && this.state.nature !== NonStandardValue.Admin) {
const nature = this.state.nature;
// Report to moderators through to the dedicated bot,
// as configured in the room's state events.
const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
if (!dmRoomId) {
throw new UserFriendlyError("report_content|error_create_room_moderation_bot");
}
await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
event_id: ev.getId()!,
room_id: ev.getRoomId()!,
moderated_by_id: this.moderation.moderationRoomId,
nature,
reporter: client.getUserId()!,
comment: this.state.reason.trim(),
} satisfies AbuseEventContent);
} else {
// Report to homeserver admin through the dedicated Matrix API.
await client.reportEvent(ev.getRoomId()!, ev.getId()!, -100, this.state.reason.trim());
}
// Report to homeserver admin through the dedicated Matrix API. We hardcode the "score" because it's
// not actually used by anything.
await client.reportEvent(ev.getRoomId()!, ev.getId()!, -100, this.state.reason.trim());
// if the user should also be ignored, do that
if (this.state.ignoreUserToo) {
@ -331,123 +153,6 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
}
if (this.moderation) {
// Display report-to-moderator dialog.
// We let the user pick a nature.
const homeServerName = SdkConfig.get("validated_server_config")!.hsName;
let subtitle: string;
switch (this.state.nature) {
case Nature.Disagreement:
subtitle = _t("report_content|nature_disagreement");
break;
case Nature.Toxic:
subtitle = _t("report_content|nature_toxic");
break;
case Nature.Illegal:
subtitle = _t("report_content|nature_illegal");
break;
case Nature.Spam:
subtitle = _t("report_content|nature_spam");
break;
case NonStandardValue.Admin:
if (this.state.isRoomEncrypted) {
subtitle = _t("report_content|nature_nonstandard_admin_encrypted", {
homeserver: homeServerName,
});
} else {
subtitle = _t("report_content|nature_nonstandard_admin", { homeserver: homeServerName });
}
break;
case Nature.Other:
subtitle = _t("report_content|nature_other");
break;
default:
subtitle = _t("report_content|nature");
break;
}
return (
<BaseDialog
className="mx_ReportEventDialog"
onFinished={this.props.onFinished}
title={_t("action|report_content")}
contentId="mx_ReportEventDialog"
>
<div>
<StyledRadioButton
name="nature"
value={Nature.Disagreement}
checked={this.state.nature == Nature.Disagreement}
onChange={this.onNatureChosen}
>
{_t("report_content|disagree")}
</StyledRadioButton>
<StyledRadioButton
name="nature"
value={Nature.Toxic}
checked={this.state.nature == Nature.Toxic}
onChange={this.onNatureChosen}
>
{_t("report_content|toxic_behaviour")}
</StyledRadioButton>
<StyledRadioButton
name="nature"
value={Nature.Illegal}
checked={this.state.nature == Nature.Illegal}
onChange={this.onNatureChosen}
>
{_t("report_content|illegal_content")}
</StyledRadioButton>
<StyledRadioButton
name="nature"
value={Nature.Spam}
checked={this.state.nature == Nature.Spam}
onChange={this.onNatureChosen}
>
{_t("report_content|spam_or_propaganda")}
</StyledRadioButton>
<StyledRadioButton
name="nature"
value={NonStandardValue.Admin}
checked={this.state.nature == NonStandardValue.Admin}
onChange={this.onNatureChosen}
>
{_t("report_content|report_entire_room")}
</StyledRadioButton>
<StyledRadioButton
name="nature"
value={Nature.Other}
checked={this.state.nature == Nature.Other}
onChange={this.onNatureChosen}
>
{_t("report_content|other_label")}
</StyledRadioButton>
<p>{subtitle}</p>
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("room_settings|permissions|ban_reason")}
rows={5}
onChange={this.onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
{ignoreUserCheckbox}
</div>
<DialogButtons
primaryButton={_t("action|send_report")}
onPrimaryButtonClick={this.onSubmit}
focus={true}
onCancel={this.onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
// Report to homeserver admin.
// Currently, the API does not support natures.
return (
<BaseDialog
className="mx_ReportEventDialog"

View File

@ -97,7 +97,6 @@
"rename": "Rename",
"reply": "Reply",
"reply_in_thread": "Reply in thread",
"report_content": "Report Content",
"report_room": "Report room",
"resend": "Resend",
"reset": "Reset",
@ -1578,8 +1577,6 @@
"notifications": "Enable the notifications panel in the room header",
"render_reaction_images": "Render custom images in reactions",
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
"report_to_moderators": "Report to moderators",
"report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
"room_list_sections": "Room list sections",
"sliding_sync": "Sliding Sync mode",
"sliding_sync_description": "Under active development, cannot be disabled. Currently, not compatible with Element Call.",
@ -1864,25 +1861,10 @@
},
"report_content": {
"description": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
"disagree": "Disagree",
"error_create_room_moderation_bot": "Unable to create room with moderation bot",
"hide_messages_from_user": "Check if you want to hide all current and future messages from this user.",
"ignore_user": "Ignore user",
"illegal_content": "Illegal Content",
"missing_reason": "Please fill why you're reporting.",
"nature": "Please pick a nature and describe what makes this message abusive.",
"nature_disagreement": "What this user is writing is wrong.\nThis will be reported to the room moderators.",
"nature_illegal": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.",
"nature_nonstandard_admin": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s.",
"nature_nonstandard_admin_encrypted": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.",
"nature_other": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.",
"nature_spam": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.",
"nature_toxic": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.",
"other_label": "Other",
"report_content_to_homeserver": "Report Content to Your Homeserver Administrator",
"report_entire_room": "Report the entire room",
"spam_or_propaganda": "Spam or propaganda",
"toxic_behaviour": "Toxic Behaviour"
"report_content_to_homeserver": "Report Content to Your Homeserver Administrator"
},
"report_room": {
"description": "Report this room to your account provider. If the messages are encrypted, your admin will not be able to read them.",

View File

@ -208,7 +208,6 @@ export interface Settings {
"feature_video_rooms": IFeature;
[Features.NotificationSettings2]: IFeature;
"feature_msc3531_hide_messages_pending_moderation": IFeature;
"feature_report_to_moderators": IFeature;
"feature_latex_maths": IFeature;
"feature_wysiwyg_composer": IFeature;
"feature_mjolnir": IFeature;
@ -463,15 +462,6 @@ export const SETTINGS: Settings = {
supportedLevels: [SettingLevel.ACCOUNT],
default: false,
},
"feature_report_to_moderators": {
isFeature: true,
labsGroup: LabGroup.Moderation,
displayName: _td("labs|report_to_moderators"),
description: _td("labs|report_to_moderators_description"),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
supportedLevelsAreOrdered: true,
default: false,
},
"feature_latex_maths": {
isFeature: true,
labsGroup: LabGroup.Messaging,

View File

@ -10,11 +10,6 @@ If a labs features gets more stable, it _may_ be promoted to a beta feature
**Be warned! Labs features are not finalised, they may be fragile, they may change, they may be
dropped. Ask in the room if you are unclear about any details here.**
## Submit Abuse Report to Moderators [MSC3215](https://github.com/matrix-org/matrix-doc/pull/3215) support (`feature_report_to_moderators`)
A new version of the "Report" dialog that lets users send abuse reports directly to room moderators,
if the room supports it.
## Render LaTeX maths in messages (`feature_latex_maths`)
Enables rendering of LaTeX maths in messages using [KaTeX](https://katex.org/). LaTeX between single dollar-signs is interpreted as inline maths and double dollar-signs as display maths (i.e. centred on its own line).