mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-05 04:06:44 +02:00
WIP
This commit is contained in:
parent
4cc51a6756
commit
a084f0234a
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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[] {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user