This commit is contained in:
R Midhun Suresh 2026-04-13 10:57:31 +05:30
parent 4cc51a6756
commit a084f0234a
No known key found for this signature in database
9 changed files with 228 additions and 4 deletions

View File

@ -152,6 +152,12 @@ export interface IMatrixClientPeg {
* see {@link ICreateClientOpts.tokenRefreshFunction}
*/
replaceUsingCreds(creds: IMatrixClientCreds, tokenRefreshFunction?: TokenRefreshFunction): void;
/**
* Returns a promise that resolve with MatrixClient is available.
* Useful when you want to access the matrix client early on the app lifecycle.
*/
clientReadyPromise: Promise<void>;
}
/**
@ -172,6 +178,8 @@ class MatrixClientPegClass implements IMatrixClientPeg {
private matrixClient: MatrixClient | null = null;
private justRegisteredUserId: string | null = null;
private matrixClientReady = Promise.withResolvers<void>();
public get(): MatrixClient | null {
return this.matrixClient;
}
@ -183,6 +191,10 @@ class MatrixClientPegClass implements IMatrixClientPeg {
return this.matrixClient;
}
public get clientReadyPromise(): Promise<void> {
return this.matrixClientReady.promise;
}
public unset(): void {
this.matrixClient = null;
@ -373,6 +385,9 @@ class MatrixClientPegClass implements IMatrixClientPeg {
logger.log(`MatrixClientPeg: really starting MatrixClient`);
await this.matrixClient!.startClient(opts);
logger.log(`MatrixClientPeg: MatrixClient started`);
console.log("Resolving client ready promise");
this.matrixClientReady.resolve();
}
private namesToRoomName(names: string[], count: number): string | undefined {

View File

@ -1617,6 +1617,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// 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() });
@ -2735,6 +2758,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (b) extraButtons.push(b);
}
const roomBanners: JSX.Element[] = [];
for (const cb of ModuleApi.instance.extras.roomBannerCallbacks) {
const b = cb(this.state.room.roomId);
if (b) roomBanners.push(b);
}
return (
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
<div
@ -2766,6 +2795,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
extraButtons={<>{extraButtons}</>}
/>
)}
{roomBanners}
{mainSplitBody}
</div>
</MainSplit>

View File

@ -54,6 +54,7 @@ import { type MatrixClientProps, withMatrixClientHOC } from "../../../contexts/M
import { UIFeature } from "../../../settings/UIFeature";
import { formatTimeLeft } from "../../../DateUtils";
import RoomReplacedSvg from "../../../../res/img/room_replaced.svg";
import { ModuleApi } from "../../../modules/Api";
// The prefix used when persisting editor drafts to localstorage.
export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_";
@ -723,4 +724,16 @@ export class MessageComposer extends React.Component<IProps, IState> {
}
const MessageComposerWithMatrixClient = withMatrixClientHOC(MessageComposer);
export default MessageComposerWithMatrixClient;
const WrappedMessageComposer: React.FC<IProps> = (props) => {
const renderer = ModuleApi.instance.customComponents.messageComposerRenderer;
if (renderer) {
return renderer({ ...props, roomId: props.room.roomId }, (props) => (
<MessageComposerWithMatrixClient {...props} />
));
}
return <MessageComposerWithMatrixClient {...props} />;
};
export default WrappedMessageComposer;

View File

@ -43,6 +43,7 @@ import SdkConfig from "../../../../../SdkConfig";
import { shouldForceDisableEncryption } from "../../../../../utils/crypto/shouldForceDisableEncryption";
import { Caption } from "../../../typography/Caption";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../../utils/crypto";
import { ModuleApi } from "../../../../../modules/Api";
interface IProps {
room: Room;
@ -532,6 +533,12 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const historySection = this.renderHistory();
const additionalSettings: JSX.Element[] = [];
for (const cb of ModuleApi.instance.extras.roomSettingsSecurityCallbacks) {
const b = cb(room.roomId);
additionalSettings.push(b!);
}
return (
<SettingsTab>
<Form.Root
@ -575,6 +582,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
</>
)}
</SettingsFieldset>
{additionalSettings}
{this.renderJoinRule()}
{historySection}
</SettingsSection>

View File

@ -373,6 +373,7 @@ export function renderReplyTile(
// Will return null if no custom component can render it.
return ModuleApi.instance.customComponents.renderMessage({
mxEvent: props.mxEvent,
isReplyTile: true,
});
}
@ -395,6 +396,7 @@ export function renderReplyTile(
return ModuleApi.instance.customComponents.renderMessage(
{
mxEvent: props.mxEvent,
isReplyTile: true,
},
(origProps) =>
factory(ref, {

View File

@ -4,7 +4,13 @@ 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,
EventContentTransformCallback,
UnregisterTransformCallback,
} from "@element-hq/element-web-module-api";
import { Room as ModuleRoom } from "./models/Room";
import { AccountDataApi } from "./AccountDataApi";
import { MatrixClientPeg } from "../MatrixClientPeg";
@ -17,4 +23,68 @@ export class ClientApi implements IClientApi {
if (sdkRoom) return new ModuleRoom(sdkRoom);
return null;
}
public async uploadContent(content: Blob | File, contentType?: string): Promise<string> {
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<string, unknown>,
stateKey: string = "",
): Promise<void> {
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<string> {
const client = MatrixClientPeg.safeGet();
// useAuthentication=true produces the authenticated /_matrix/client/v1/media/download URL
const httpUrl = client.mxcUrlToHttp(mxcUrl, undefined, undefined, undefined, false, true);
if (!httpUrl) throw new Error(`Cannot resolve mxc URL: ${mxcUrl}`);
const accessToken = client.getAccessToken();
const response = await fetch(httpUrl, {
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} downloading ${mxcUrl}`);
}
return response.text();
}
public async waitForClient(): Promise<void> {
const clientPromise = MatrixClientPeg.clientReadyPromise;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("ClientApi.waitForClient timed out.")), 10000);
});
await Promise.race([clientPromise, timeoutPromise]);
}
public registerEncryptedEventContentTransform(
transform: EventContentTransformCallback,
): UnregisterTransformCallback {
const client = MatrixClientPeg.safeGet();
return client.registerEncryptedEventContentTransform(transform);
}
public registerEventContentTransform(transform: EventContentTransformCallback): UnregisterTransformCallback {
const client = MatrixClientPeg.safeGet();
return client.registerEventContentTransform(transform);
}
}

View File

@ -11,6 +11,9 @@ import {
type SpacePanelItemProps,
type ExtrasApi,
type RoomHeaderButtonsCallback,
type RoomBannerCallback,
type EventContentTransformCallback,
type RoomSettingsSecurityCallback,
} from "@element-hq/element-web-module-api";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
@ -32,6 +35,10 @@ export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents,
public spacePanelItems = new Map<string, SpacePanelItemProps>();
public visibleRoomBySpaceKey = new Map<string, () => string[]>();
public roomHeaderButtonsCallbacks: RoomHeaderButtonsCallback[] = [];
public roomBannerCallbacks: RoomBannerCallback[] = [];
public eventContentTransformCallbacks: EventContentTransformCallback[] = [];
public encryptedEnvelopeTransformCallbacks: EventContentTransformCallback[] = [];
public roomSettingsSecurityCallbacks: RoomSettingsSecurityCallback[] = [];
public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void {
this.spacePanelItems.set(spacekey, item);
@ -45,6 +52,22 @@ export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents,
public addRoomHeaderButtonCallback(cb: RoomHeaderButtonsCallback): void {
this.roomHeaderButtonsCallbacks.push(cb);
}
public addRoomBannerCallback(cb: RoomBannerCallback): void {
this.roomBannerCallbacks.push(cb);
}
public addEventContentTransformCallback(cb: EventContentTransformCallback): void {
this.eventContentTransformCallbacks.push(cb);
}
public addEncryptedEnvelopeTransformCallback(cb: EventContentTransformCallback): void {
this.encryptedEnvelopeTransformCallbacks.push(cb);
}
public addRoomSettingsSecurityCallback(cb: RoomSettingsSecurityCallback): void {
this.roomSettingsSecurityCallbacks.push(cb);
}
}
export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpacePanelItem[] {

View File

@ -17,6 +17,7 @@ import type {
MatrixEvent as ModuleMatrixEvent,
CustomRoomPreviewBarRenderFunction,
CustomLoginRenderFunction,
CustomMessageComposerRenderFunction,
} from "@element-hq/element-web-module-api";
import type React from "react";
@ -62,6 +63,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
stateKey: mxEvent.getStateKey(),
type: mxEvent.getType(),
unsigned: mxEvent.getUnsigned(),
wireContent: mxEvent.getWireContent(),
};
}
@ -171,4 +173,14 @@ export class CustomComponentsApi implements ICustomComponentsApi {
public registerLoginComponent(renderer: CustomLoginRenderFunction): void {
this._loginRenderer = renderer;
}
private _messageComposerRenderer?: CustomMessageComposerRenderFunction;
public get messageComposerRenderer(): CustomMessageComposerRenderFunction | undefined {
return this._messageComposerRenderer;
}
public registerMessageComposerComponent(renderer: CustomMessageComposerRenderFunction): void {
this._messageComposerRenderer = renderer;
}
}

View File

@ -5,8 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type Room as IRoom, Watchable } from "@element-hq/element-web-module-api";
import { RoomEvent, type Room as SdkRoom } from "matrix-js-sdk/src/matrix";
import {
type Room as IRoom,
type MatrixEvent as ModuleMatrixEvent,
Watchable,
} from "@element-hq/element-web-module-api";
import { type MatrixEvent, RoomEvent, RoomStateEvent, type Room as SdkRoom } from "matrix-js-sdk/src/matrix";
export class Room implements IRoom {
public name: Watchable<string>;
@ -22,6 +26,10 @@ export class Room implements IRoom {
public get id(): string {
return this.sdkRoom.roomId;
}
public getStateEvent(eventType: string, stateKey: string = ""): WatchableStateEvent {
return new WatchableStateEvent(eventType, stateKey, this.sdkRoom);
}
}
/**
@ -43,3 +51,46 @@ class WatchableName extends Watchable<string> {
this.sdkRoom.off(RoomEvent.Name, this.onNameUpdate);
}
}
class WatchableStateEvent extends Watchable<ModuleMatrixEvent | null> {
public constructor(
private eventType: string,
private stateKey: string,
private sdkRoom: SdkRoom,
) {
const event = sdkRoom.currentState.getStateEvents(eventType, stateKey);
super(WatchableStateEvent.sdkEventToModuleEvent(event));
}
protected onFirstWatch(): void {
this.sdkRoom.on(RoomStateEvent.Events, this.updateEvent);
}
protected onLastWatch(): void {
this.sdkRoom.off(RoomStateEvent.Events, this.updateEvent);
}
private updateEvent = (event: MatrixEvent): void => {
if (event.isState() && event.getType() === this.eventType && event.getStateKey() === this.stateKey) {
this.value = WatchableStateEvent.sdkEventToModuleEvent(event);
}
};
public static sdkEventToModuleEvent(sdkEvent: MatrixEvent | null): ModuleMatrixEvent | null {
if (!sdkEvent) return null;
const eventId = sdkEvent.getId();
const roomId = sdkEvent.getRoomId();
const sender = sdkEvent.getSender();
if (!eventId || !roomId || !sender) return null;
return {
content: sdkEvent.getContent(),
eventId,
originServerTs: sdkEvent.getTs(),
roomId,
sender,
stateKey: sdkEvent.getStateKey(),
type: sdkEvent.getType(),
unsigned: sdkEvent.getUnsigned(),
};
}
}