From 29be1e29d80c8664bca77bb54ab728734176af3e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 10 Apr 2026 17:52:06 +0100 Subject: [PATCH] implement experimental module APIs for additional room sec settings --- .../src/components/structures/RoomView.tsx | 23 ++++++++++++++ .../tabs/room/SecurityRoomSettingsTab.tsx | 5 +++ apps/web/src/modules/Api.ts | 1 + apps/web/src/modules/ClientApi.ts | 31 ++++++++++++++++++- apps/web/src/modules/ExtrasApi.ts | 6 ++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/structures/RoomView.tsx b/apps/web/src/components/structures/RoomView.tsx index 1ade60664a..0f021a07c4 100644 --- a/apps/web/src/components/structures/RoomView.tsx +++ b/apps/web/src/components/structures/RoomView.tsx @@ -1617,6 +1617,29 @@ export class RoomView extends React.Component { // ignore if we don't have a room yet if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return; + // Notify module state event listeners and force a re-render so + // module-provided UI (banners, composer components) updates. + const listeners = ModuleApi.instance.client.stateEventListeners; + if (listeners.length) { + const eventId = ev.getId(); + const roomId = ev.getRoomId(); + const sender = ev.getSender(); + if (eventId && roomId && sender) { + const moduleEvent = { + content: ev.getContent(), + eventId, + originServerTs: ev.getTs(), + roomId, + sender, + stateKey: ev.getStateKey(), + type: ev.getType(), + unsigned: ev.getUnsigned(), + }; + for (const cb of listeners) cb(moduleEvent); + this.forceUpdate(); + } + } + switch (ev.getType()) { case EventType.RoomTombstone: this.setState({ tombstone: this.getRoomTombstone() }); diff --git a/apps/web/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/apps/web/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 15ca6e9788..c5d81c2b04 100644 --- a/apps/web/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/apps/web/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -28,6 +28,7 @@ import { SettingLevel } from "../../../../../settings/SettingLevel"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; import AccessibleButton from "../../../elements/AccessibleButton"; +import { ModuleApi } from "../../../../../modules/Api"; import SettingsFlag from "../../../elements/SettingsFlag"; import createRoom from "../../../../../createRoom"; import CreateRoomDialog from "../../../dialogs/CreateRoomDialog"; @@ -575,6 +576,10 @@ export default class SecurityRoomSettingsTab extends React.Component )} + {ModuleApi.instance.extras.roomSettingsSecurityCallbacks.map((cb, i) => { + const el = cb(this.props.room.roomId); + return el ? {el} : null; + })} {this.renderJoinRule()} {historySection} diff --git a/apps/web/src/modules/Api.ts b/apps/web/src/modules/Api.ts index 858c4c5b81..44da332e88 100644 --- a/apps/web/src/modules/Api.ts +++ b/apps/web/src/modules/Api.ts @@ -136,6 +136,7 @@ export class ModuleApi implements Api { } return original.call(this, event, ...args); }; + } } diff --git a/apps/web/src/modules/ClientApi.ts b/apps/web/src/modules/ClientApi.ts index 9004b52114..0895180a04 100644 --- a/apps/web/src/modules/ClientApi.ts +++ b/apps/web/src/modules/ClientApi.ts @@ -4,7 +4,7 @@ Copyright 2025 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 type { ClientApi as IClientApi, Room } from "@element-hq/element-web-module-api"; +import type { ClientApi as IClientApi, Room, MatrixEvent as ModuleMatrixEvent } from "@element-hq/element-web-module-api"; import { Room as ModuleRoom } from "./models/Room"; import { AccountDataApi } from "./AccountDataApi"; import { MatrixClientPeg } from "../MatrixClientPeg"; @@ -18,6 +18,35 @@ export class ClientApi implements IClientApi { return null; } + public async uploadContent(content: Blob | File, contentType?: string): Promise { + const client = MatrixClientPeg.safeGet(); + const { content_uri: mxcUrl } = await client.uploadContent(content, { + includeFilename: false, + type: contentType, + }); + return mxcUrl; + } + + public async sendStateEvent( + roomId: string, + eventType: string, + content: Record, + stateKey: string = "", + ): Promise { + const client = MatrixClientPeg.safeGet(); + await client.sendStateEvent(roomId, eventType, content, stateKey); + } + + public stateEventListeners: Array<(event: ModuleMatrixEvent) => void> = []; + + public onStateEvent(callback: (event: ModuleMatrixEvent) => void): () => void { + this.stateEventListeners.push(callback); + return () => { + const idx = this.stateEventListeners.indexOf(callback); + if (idx >= 0) this.stateEventListeners.splice(idx, 1); + }; + } + public async downloadMxc(mxcUrl: string): Promise { const client = MatrixClientPeg.safeGet(); // useAuthentication=true produces the authenticated /_matrix/client/v1/media/download URL diff --git a/apps/web/src/modules/ExtrasApi.ts b/apps/web/src/modules/ExtrasApi.ts index 7638de77c0..658c22afda 100644 --- a/apps/web/src/modules/ExtrasApi.ts +++ b/apps/web/src/modules/ExtrasApi.ts @@ -14,6 +14,7 @@ import { type RoomBannerCallback, type ComposerLeftComponentCallback, type EventContentTransformCallback, + type RoomSettingsSecurityCallback, } from "@element-hq/element-web-module-api"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; @@ -39,6 +40,7 @@ export class ElementWebExtrasApi extends TypedEventEmitter