diff --git a/docs/labs.md b/docs/labs.md index a4ab78b08b..17d8c328e1 100644 --- a/docs/labs.md +++ b/docs/labs.md @@ -15,6 +15,13 @@ dropped. Ask in the room if you are unclear about any details here.** A new version of the "Report" dialog that lets users send abuse reports directly to room moderators, if the room supports it. +## Enable options to set up Policy Servers in rooms (`feature_msc4284_setup`) + +Allows configuring a room's policy server ([MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284)). + +Users can see the current configuration from the 'Roles & Permissions' tab of room settings. If the user has permission +to change the policy server, they can also do that there. + ## 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). diff --git a/src/components/views/settings/PolicyServerConfig.tsx b/src/components/views/settings/PolicyServerConfig.tsx new file mode 100644 index 0000000000..717b8baf9f --- /dev/null +++ b/src/components/views/settings/PolicyServerConfig.tsx @@ -0,0 +1,119 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { type FormEvent, type JSX, useContext, useState } from "react"; +import { EventType, type Room } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { _t } from "../../../languageHandler"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { useRoomState } from "../../../hooks/useRoomState.ts"; +import SettingsFieldset from "./SettingsFieldset.tsx"; +import Field from "../elements/Field.tsx"; +import AccessibleButton from "../elements/AccessibleButton.tsx"; +import { useAsyncMemo } from "../../../hooks/useAsyncMemo.ts"; +import ExternalLink from "../elements/ExternalLink.tsx"; + +interface PolicyServerConfigProps { + room: Room; +} + +export const PolicyServerConfig: React.FC = ({ room }) => { + const client = useContext(MatrixClientContext); + const { policyServerEvent, canChange } = useRoomState(room, (roomState) => ({ + policyServerEvent: roomState.events.get(EventType.RoomPolicy)?.get(""), + canChange: roomState.maySendStateEvent(EventType.RoomPolicy, client.getSafeUserId()), + })); + const [isLoading, setIsLoading] = useState(false); + const currentPolicyServerName = policyServerEvent?.getContent()?.["via"] ?? ""; + const [serverName, setServerName] = useState(currentPolicyServerName); + const [error, setError] = useState(false); + const supportUrl = useAsyncMemo(async (): Promise => { + if (!currentPolicyServerName) { + return undefined; + } + + const res = await (await fetch(`https://${currentPolicyServerName}/.well-known/matrix/support`)).json(); + if (!!res["support_page"] && typeof res["support_page"] === "string") { + return res["support_page"]; + } + + return undefined; + }, [serverName, currentPolicyServerName], undefined); + + const onSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + await applyChange(); + }; + + const applyChange = async (): Promise => { + setIsLoading(true); + setError(false); + + try { + if (serverName.trim().length > 0) { + const res = await (await fetch(`https://${serverName.trim()}/.well-known/matrix/org.matrix.msc4284.policy_server`)).json(); + if (!!res["public_key"] && typeof res["public_key"] === "string") { + await client.sendStateEvent(room.roomId, EventType.RoomPolicy, { + via: serverName, + public_key: res["public_key"], + }, ""); + } else { + logger.error("Policy server returned non-string public key (or returned an error)") + setError(true); + } + } else { + // Empty object == remove + await client.sendStateEvent(room.roomId, EventType.RoomPolicy, {}, ""); + } + } catch (e) { + logger.error(e); + setError(true); + } + + setIsLoading(false); + } + + let supportSection: JSX.Element | undefined; + if (!!currentPolicyServerName) { + if (!!supportUrl) { + supportSection = { _t("room_settings|permissions|policy_server_support_page", {},{ + a: (sub) => {sub}, + }) }; + } else { + supportSection = { _t("room_settings|permissions|policy_server_generic_support") }; + } + } + + return ( +
+ + ) => setServerName(e.target.value)} + autoComplete="off" + disabled={isLoading || !canChange} + /> + + {_t("action|apply")} + + { error ? { _t("room_settings|permissions|policy_server_error") } : undefined } + { supportSection } + +
+ ); +}; diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 4c720a2372..aa5e1bce3f 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -26,6 +26,7 @@ import { SettingsSection } from "../../shared/SettingsSection"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import { PowerLevelSelector } from "../../PowerLevelSelector"; import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../call-types"; +import { PolicyServerConfig } from "../../PolicyServerConfig.tsx"; interface IEventShowOpts { isState?: boolean; @@ -59,6 +60,9 @@ const plEventsToShow: Record = { [ElementCallEventType.name]: { isState: true, hideForSpace: true }, [ElementCallMemberEventType.name]: { isState: true, hideForSpace: true }, + // MSC4284: Policy servers + [EventType.RoomPolicy]: { isState: true, hideForSpace: true }, + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": { isState: true, hideForSpace: true }, }; @@ -283,6 +287,13 @@ export default class RolesRoomSettingsTab extends React.Component; + } + const powerLevelDescriptors: Record = { "users_default": { desc: _t("room_settings|permissions|users_default"), @@ -454,6 +465,7 @@ export default class RolesRoomSettingsTab extends React.Component + {policyServerSection} {privilegedUsersSection} {canChangeLevels && } {mutedUsersSection} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 400c2d1679..9407cfac52 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1564,6 +1564,7 @@ "location_share_live_description": "Temporary implementation. Locations persist in room history.", "mjolnir": "New ways to ignore people", "msc3531_hide_messages_pending_moderation": "Let moderators hide messages pending moderation.", + "msc4284_setup": "Enable options to set up Policy Servers in rooms", "new_room_list": "Enable new room list", "notification_settings": "New Notification Settings", "notification_settings_beta_caption": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.", @@ -2392,6 +2393,7 @@ "m.room.name": "Change room name", "m.room.name_space": "Change space name", "m.room.pinned_events": "Manage pinned events", + "m.room.policy": "Configure the room's policy server", "m.room.power_levels": "Change permissions", "m.room.redaction": "Remove messages sent by me", "m.room.server_acl": "Change server ACLs", @@ -2406,6 +2408,12 @@ "permissions_section": "Permissions", "permissions_section_description_room": "Select the roles required to change various parts of the room", "permissions_section_description_space": "Select the roles required to change various parts of the space", + "policy_server_description": "Policy servers are a proactive moderation tool which compliment reactive tooling like moderation bots. If you have access to one, set its server name here. To remove the policy server, set this to an empty value.", + "policy_server_error": "There was an error changing the policy server name. Ensure the server name is a valid policy server and try again.", + "policy_server_field_label": "Policy server name", + "policy_server_generic_support": "This policy server may require additional setup before it will work. Consult your policy server's documentation for details.", + "policy_server_support_page": "This policy server may require additional setup before it will work. Click here to visit your policy server's support page.", + "policy_server_title": "Policy server", "privileged_users_section": "Privileged Users", "redact": "Remove messages sent by others", "send_event_type": "Send %(eventType)s events", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 2fdc1a79a2..1128606038 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -208,6 +208,7 @@ export interface Settings { [Features.NotificationSettings2]: IFeature; [Features.ReleaseAnnouncement]: IFeature; "feature_msc3531_hide_messages_pending_moderation": IFeature; + "feature_msc4284_setup": IFeature; "feature_report_to_moderators": IFeature; "feature_latex_maths": IFeature; "feature_wysiwyg_composer": IFeature; @@ -450,6 +451,14 @@ export const SETTINGS: Settings = { supportedLevelsAreOrdered: true, default: false, }, + "feature_msc4284_setup": { + isFeature: true, + labsGroup: LabGroup.Moderation, + displayName: _td("labs|msc4284_setup"), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, + supportedLevelsAreOrdered: true, + default: false, + }, "mediaPreviewConfig": { controller: new MediaPreviewConfigController(), supportedLevels: LEVELS_ROOM_SETTINGS,