Absorb the matrix-react-sdk repository (#28192)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: David Baker <dbkr@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: David Langley <davidl@element.io> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
committed by
GitHub
parent
2b99496025
commit
c05c429803
37
src/modules/AppModule.ts
Normal file
37
src/modules/AppModule.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
||||
|
||||
import { ModuleFactory } from "./ModuleFactory";
|
||||
import { ProxiedModuleApi } from "./ProxiedModuleApi";
|
||||
|
||||
/**
|
||||
* Wraps a module factory into a usable module. Acts as a simple container
|
||||
* for the constructs needed to operate a module.
|
||||
*/
|
||||
export class AppModule {
|
||||
/**
|
||||
* The module instance.
|
||||
*/
|
||||
public readonly module: RuntimeModule;
|
||||
|
||||
/**
|
||||
* The API instance used by the module.
|
||||
*/
|
||||
public readonly api = new ProxiedModuleApi();
|
||||
|
||||
/**
|
||||
* Converts a factory into an AppModule. The factory will be called
|
||||
* immediately.
|
||||
* @param factory The module factory.
|
||||
*/
|
||||
public constructor(factory: ModuleFactory) {
|
||||
this.module = factory(this.api);
|
||||
}
|
||||
}
|
||||
32
src/modules/ModuleComponents.tsx
Normal file
32
src/modules/ModuleComponents.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { TextInputField } from "@matrix-org/react-sdk-module-api/lib/components/TextInputField";
|
||||
import { Spinner as ModuleSpinner } from "@matrix-org/react-sdk-module-api/lib/components/Spinner";
|
||||
import React, { ChangeEvent } from "react";
|
||||
|
||||
import Field from "../components/views/elements/Field";
|
||||
import Spinner from "../components/views/elements/Spinner";
|
||||
|
||||
// Here we define all the render factories for the module API components. This file should be
|
||||
// imported by the ModuleRunner to load them into the call stack at runtime.
|
||||
//
|
||||
// If a new component is added to the module API, it should be added here too.
|
||||
//
|
||||
// Don't forget to add a test to ensure the renderFactory is overridden! See ModuleComponents-test.tsx
|
||||
|
||||
TextInputField.renderFactory = (props) => (
|
||||
<Field
|
||||
type="text"
|
||||
value={props.value}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => props.onChange(e.target.value)}
|
||||
label={props.label}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
ModuleSpinner.renderFactory = () => <Spinner />;
|
||||
12
src/modules/ModuleFactory.ts
Normal file
12
src/modules/ModuleFactory.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
||||
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
|
||||
|
||||
export type ModuleFactory = (api: ModuleApi) => RuntimeModule;
|
||||
185
src/modules/ModuleRunner.ts
Normal file
185
src/modules/ModuleRunner.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { safeSet } from "matrix-js-sdk/src/utils";
|
||||
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
||||
import { AnyLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/types";
|
||||
import {
|
||||
DefaultCryptoSetupExtensions,
|
||||
ProvideCryptoSetupExtensions,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions";
|
||||
import {
|
||||
DefaultExperimentalExtensions,
|
||||
ProvideExperimentalExtensions,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
|
||||
|
||||
import { AppModule } from "./AppModule";
|
||||
import { ModuleFactory } from "./ModuleFactory";
|
||||
|
||||
import "./ModuleComponents";
|
||||
|
||||
/**
|
||||
* Handles and manages extensions provided by modules.
|
||||
*/
|
||||
class ExtensionsManager {
|
||||
// Private backing fields for extensions
|
||||
private cryptoSetupExtension: ProvideCryptoSetupExtensions;
|
||||
private experimentalExtension: ProvideExperimentalExtensions;
|
||||
|
||||
/** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */
|
||||
private hasDefaultCryptoSetupExtension = true;
|
||||
|
||||
/** `true` if `experimentalExtension` is the default implementation; `false` if it is implemented by a module. */
|
||||
private hasDefaultExperimentalExtension = true;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*/
|
||||
public constructor() {
|
||||
// Set up defaults
|
||||
this.cryptoSetupExtension = new DefaultCryptoSetupExtensions();
|
||||
this.experimentalExtension = new DefaultExperimentalExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a crypto setup extension.
|
||||
*
|
||||
* @returns The registered extension. If no module provides this extension, a default implementation is returned.
|
||||
*/
|
||||
public get cryptoSetup(): ProvideCryptoSetupExtensions {
|
||||
return this.cryptoSetupExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an experimental extension.
|
||||
*
|
||||
* @remarks
|
||||
* This method extension is provided to simplify experimentation and development, and is not intended for production code.
|
||||
*
|
||||
* @returns The registered extension. If no module provides this extension, a default implementation is returned.
|
||||
*/
|
||||
public get experimental(): ProvideExperimentalExtensions {
|
||||
return this.experimentalExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add any extensions provided by the module.
|
||||
*
|
||||
* @param module - The appModule to check for extensions.
|
||||
*
|
||||
* @throws if an extension is provided by more than one module.
|
||||
*/
|
||||
public addExtensions(module: AppModule): void {
|
||||
const runtimeModule = module.module;
|
||||
|
||||
/* Add the cryptoSetup extension if any */
|
||||
if (runtimeModule.extensions?.cryptoSetup) {
|
||||
if (this.hasDefaultCryptoSetupExtension) {
|
||||
this.cryptoSetupExtension = runtimeModule.extensions?.cryptoSetup;
|
||||
this.hasDefaultCryptoSetupExtension = false;
|
||||
} else {
|
||||
throw new Error(
|
||||
`adding cryptoSetup extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add the experimental extension if any */
|
||||
if (runtimeModule.extensions?.experimental) {
|
||||
if (this.hasDefaultExperimentalExtension) {
|
||||
this.experimentalExtension = runtimeModule.extensions?.experimental;
|
||||
this.hasDefaultExperimentalExtension = false;
|
||||
} else {
|
||||
throw new Error(
|
||||
`adding experimental extension implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles and coordinates the operation of modules.
|
||||
*/
|
||||
export class ModuleRunner {
|
||||
public static readonly instance = new ModuleRunner();
|
||||
|
||||
private extensionsManager = new ExtensionsManager();
|
||||
|
||||
private modules: AppModule[] = [];
|
||||
|
||||
private constructor() {
|
||||
// we only want one instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes all extensions which may be overridden/provided by modules.
|
||||
*
|
||||
* @returns An `ExtensionsManager` which exposes the extensions.
|
||||
*/
|
||||
public get extensions(): ExtensionsManager {
|
||||
return this.extensionsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the runner, clearing all known modules, and all extensions
|
||||
*
|
||||
* Intended for test usage only.
|
||||
*/
|
||||
public reset(): void {
|
||||
this.modules = [];
|
||||
this.extensionsManager = new ExtensionsManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* All custom translations from all registered modules.
|
||||
*/
|
||||
public get allTranslations(): TranslationStringsObject {
|
||||
const merged: TranslationStringsObject = {};
|
||||
|
||||
for (const module of this.modules) {
|
||||
const i18n = module.api.translations;
|
||||
if (!i18n) continue;
|
||||
|
||||
for (const [lang, strings] of Object.entries(i18n)) {
|
||||
safeSet(merged, lang, merged[lang] || {});
|
||||
|
||||
for (const [str, val] of Object.entries(strings)) {
|
||||
safeSet(merged[lang], str, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a factory which creates a module for later loading. The factory
|
||||
* will be called immediately.
|
||||
* @param factory The module factory.
|
||||
*/
|
||||
public registerModule(factory: ModuleFactory): void {
|
||||
const appModule = new AppModule(factory);
|
||||
|
||||
this.modules.push(appModule);
|
||||
|
||||
// Check if the new module provides any extensions, and also ensure a given extension is only provided by a single runtime module.
|
||||
this.extensionsManager.addExtensions(appModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a lifecycle event, notifying registered modules.
|
||||
* @param lifecycleEvent The lifecycle event.
|
||||
* @param args The arguments for the lifecycle event.
|
||||
*/
|
||||
public invoke(lifecycleEvent: AnyLifecycle, ...args: any[]): void {
|
||||
for (const module of this.modules) {
|
||||
module.module.emit(lifecycleEvent, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
253
src/modules/ProxiedModuleApi.ts
Normal file
253
src/modules/ProxiedModuleApi.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
|
||||
import { TranslationStringsObject, PlainSubstitution } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { DialogContent, DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent";
|
||||
import React from "react";
|
||||
import { AccountAuthInfo } from "@matrix-org/react-sdk-module-api/lib/types/AccountAuthInfo";
|
||||
import * as Matrix from "matrix-js-sdk/src/matrix";
|
||||
import { IRegisterRequestParams } from "matrix-js-sdk/src/matrix";
|
||||
import { ModuleUiDialogOptions } from "@matrix-org/react-sdk-module-api/lib/types/ModuleUiDialogOptions";
|
||||
|
||||
import Modal from "../Modal";
|
||||
import { _t, TranslationKey } from "../languageHandler";
|
||||
import { ModuleUiDialog } from "../components/views/dialogs/ModuleUiDialog";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import PlatformPeg from "../PlatformPeg";
|
||||
import dispatcher from "../dispatcher/dispatcher";
|
||||
import { navigateToPermalink } from "../utils/permalinks/navigator";
|
||||
import { parsePermalink } from "../utils/permalinks/Permalinks";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { getCachedRoomIDForAlias } from "../RoomAliasCache";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { OverwriteLoginPayload } from "../dispatcher/payloads/OverwriteLoginPayload";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import WidgetStore, { IApp } from "../stores/WidgetStore";
|
||||
import { Container, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
/**
|
||||
* Glue between the `ModuleApi` interface and the react-sdk. Anticipates one instance
|
||||
* to be assigned to a single module.
|
||||
*/
|
||||
export class ProxiedModuleApi implements ModuleApi {
|
||||
private cachedTranslations: Optional<TranslationStringsObject>;
|
||||
|
||||
private overrideLoginResolve?: () => void;
|
||||
|
||||
public constructor() {
|
||||
dispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.action === Action.OnLoggedIn) {
|
||||
this.overrideLoginResolve?.();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* All custom translations used by the associated module.
|
||||
*/
|
||||
public get translations(): Optional<TranslationStringsObject> {
|
||||
return this.cachedTranslations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public registerTranslations(translations: TranslationStringsObject): void {
|
||||
this.cachedTranslations = translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public translateString(s: TranslationKey, variables?: Record<string, PlainSubstitution>): string {
|
||||
return _t(s, variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public openDialog<M extends object, P extends DialogProps, C extends DialogContent<P>>(
|
||||
initialTitleOrOptions: string | ModuleUiDialogOptions,
|
||||
body: (props: P, ref: React.RefObject<C>) => React.ReactNode,
|
||||
props?: Omit<P, keyof DialogProps>,
|
||||
): Promise<{ didOkOrSubmit: boolean; model: M }> {
|
||||
const initialOptions: ModuleUiDialogOptions =
|
||||
typeof initialTitleOrOptions === "string" ? { title: initialTitleOrOptions } : initialTitleOrOptions;
|
||||
|
||||
return new Promise<{ didOkOrSubmit: boolean; model: M }>((resolve) => {
|
||||
Modal.createDialog(
|
||||
ModuleUiDialog<P, C>,
|
||||
{
|
||||
initialOptions,
|
||||
contentFactory: body,
|
||||
moduleApi: this,
|
||||
additionalContentProps: props,
|
||||
},
|
||||
"mx_CompoundDialog",
|
||||
).finished.then(([didOkOrSubmit, model]) => {
|
||||
resolve({ didOkOrSubmit: !!didOkOrSubmit, model: model as M });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public async registerSimpleAccount(
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
): Promise<AccountAuthInfo> {
|
||||
const hsUrl = SdkConfig.get("validated_server_config")?.hsUrl;
|
||||
if (!hsUrl) throw new Error("Could not get homeserver url");
|
||||
const client = Matrix.createClient({ baseUrl: hsUrl });
|
||||
const deviceName =
|
||||
SdkConfig.get("default_device_display_name") || PlatformPeg.get()?.getDefaultDeviceDisplayName();
|
||||
const req: IRegisterRequestParams = {
|
||||
username,
|
||||
password,
|
||||
initial_device_display_name: deviceName,
|
||||
auth: undefined,
|
||||
inhibit_login: false,
|
||||
};
|
||||
const creds = await client.registerRequest(req).catch((resp) =>
|
||||
client.registerRequest({
|
||||
...req,
|
||||
auth: {
|
||||
session: resp.data.session,
|
||||
type: "m.login.dummy",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (displayName) {
|
||||
const profileClient = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
userId: creds.user_id,
|
||||
deviceId: creds.device_id,
|
||||
accessToken: creds.access_token,
|
||||
});
|
||||
await profileClient.setDisplayName(displayName);
|
||||
}
|
||||
|
||||
return {
|
||||
homeserverUrl: hsUrl,
|
||||
userId: creds.user_id!,
|
||||
deviceId: creds.device_id!,
|
||||
accessToken: creds.access_token!,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public async overwriteAccountAuth(accountInfo: AccountAuthInfo): Promise<void> {
|
||||
// We want to wait for the new login to complete before returning.
|
||||
// See `Action.OnLoggedIn` in dispatcher.
|
||||
const awaitNewLogin = new Promise<void>((resolve) => {
|
||||
this.overrideLoginResolve = resolve;
|
||||
});
|
||||
|
||||
dispatcher.dispatch<OverwriteLoginPayload>(
|
||||
{
|
||||
action: Action.OverwriteLogin,
|
||||
credentials: {
|
||||
...accountInfo,
|
||||
guest: false,
|
||||
},
|
||||
},
|
||||
true,
|
||||
); // require to be sync to match inherited interface behaviour
|
||||
|
||||
// wait for login to complete
|
||||
await awaitNewLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public async navigatePermalink(uri: string, andJoin?: boolean): Promise<void> {
|
||||
navigateToPermalink(uri);
|
||||
|
||||
const parts = parsePermalink(uri);
|
||||
if (parts?.roomIdOrAlias && andJoin) {
|
||||
let roomId: string | undefined = parts.roomIdOrAlias;
|
||||
let servers = parts.viaServers;
|
||||
if (roomId.startsWith("#")) {
|
||||
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
|
||||
if (!roomId) {
|
||||
// alias resolution failed
|
||||
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
|
||||
roomId = result.room_id;
|
||||
if (!servers) servers = result.servers; // use provided servers first, if available
|
||||
}
|
||||
}
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
via_servers: servers,
|
||||
});
|
||||
|
||||
if (andJoin) {
|
||||
dispatcher.dispatch({
|
||||
action: Action.JoinRoom,
|
||||
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public getConfigValue<T>(namespace: string, key: string): T | undefined {
|
||||
// Force cast to `any` because the namespace won't be known to the SdkConfig types
|
||||
const maybeObj = SdkConfig.get(namespace as any);
|
||||
if (!maybeObj || !(typeof maybeObj === "object")) return undefined;
|
||||
return maybeObj[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public getApps(roomId: string): IApp[] {
|
||||
return WidgetStore.instance.getApps(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public getAppAvatarUrl(app: IApp, width?: number, height?: number, resizeMethod?: string): string | null {
|
||||
if (!app.avatar_url) return null;
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
return MatrixClientPeg.safeGet().mxcUrlToHttp(app.avatar_url, width, height, resizeMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public isAppInContainer(app: IApp, container: Container, roomId: string): boolean {
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (!room) return false;
|
||||
return WidgetLayoutStore.instance.isInContainer(room, app, container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public moveAppToContainer(app: IApp, container: Container, roomId: string): void {
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (!room) return;
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, container);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user