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:
Michael Telatynski
2024-10-16 13:31:55 +01:00
committed by GitHub
parent 2b99496025
commit c05c429803
3280 changed files with 586617 additions and 905 deletions

37
src/modules/AppModule.ts Normal file
View 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);
}
}

View 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 />;

View 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
View 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);
}
}
}

View 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);
}
}