Support a module API surface for custom functionality (#8246)
* Early implementation of module API surface + functions for ILAG module * Wire up dialog functions and ILAG-needed surface * Ensure component renders for modules get overridden * Respond to changes from module API interface * Use a real module-api dependency * Update for new Dialogs interface * Add support for getConfigValue from module API * Update the remainder of the module API interface * Docs & cleanup * Add some unit tests around module stuff Needs end-to-end tests still. * Appease early linters * Break import cycles by not directly depending on Lifecycle * Appease the linter * Fix bad merge
This commit is contained in:
parent
2dd683a42f
commit
7f5bb61a79
22 changed files with 906 additions and 34 deletions
|
@ -57,6 +57,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.1.1",
|
"@matrix-org/analytics-events": "^0.1.1",
|
||||||
|
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||||
"@sentry/browser": "^6.11.0",
|
"@sentry/browser": "^6.11.0",
|
||||||
"@sentry/tracing": "^6.11.0",
|
"@sentry/tracing": "^6.11.0",
|
||||||
"@testing-library/react": "^12.1.5",
|
"@testing-library/react": "^12.1.5",
|
||||||
|
|
|
@ -63,6 +63,7 @@ import VideoChannelStore from "./stores/VideoChannelStore";
|
||||||
import { fixStuckDevices } from "./utils/VideoChannelUtils";
|
import { fixStuckDevices } from "./utils/VideoChannelUtils";
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
|
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
|
||||||
|
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
|
||||||
|
|
||||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||||
|
@ -71,6 +72,10 @@ dis.register((payload) => {
|
||||||
if (payload.action === Action.TriggerLogout) {
|
if (payload.action === Action.TriggerLogout) {
|
||||||
// noinspection JSIgnoredPromiseFromCall - we don't care if it fails
|
// noinspection JSIgnoredPromiseFromCall - we don't care if it fails
|
||||||
onLoggedOut();
|
onLoggedOut();
|
||||||
|
} else if (payload.action === Action.OverwriteLogin) {
|
||||||
|
const typed = <OverwriteLoginPayload>payload;
|
||||||
|
// noinspection JSIgnoredPromiseFromCall - we don't care if it fails
|
||||||
|
doSetLoggedIn(typed.credentials, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
66
src/components/views/dialogs/ModuleUiDialog.tsx
Normal file
66
src/components/views/dialogs/ModuleUiDialog.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { createRef } from "react";
|
||||||
|
import { DialogContent, DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import ScrollableBaseModal, { IScrollableBaseState } from "./ScrollableBaseModal";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
contentFactory: (props: DialogProps, ref: React.Ref<DialogContent>) => React.ReactNode;
|
||||||
|
contentProps: DialogProps;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState extends IScrollableBaseState {
|
||||||
|
// nothing special
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModuleUiDialog extends ScrollableBaseModal<IProps, IState> {
|
||||||
|
private contentRef = createRef<DialogContent>();
|
||||||
|
|
||||||
|
public constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
title: this.props.title,
|
||||||
|
canSubmit: true,
|
||||||
|
actionLabel: _t("OK"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async submit() {
|
||||||
|
try {
|
||||||
|
const model = await this.contentRef.current.trySubmit();
|
||||||
|
this.props.onFinished(true, model);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Error during submission of module dialog:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected cancel(): void {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderContent(): React.ReactNode {
|
||||||
|
return <div className="mx_ModuleUiDialog">
|
||||||
|
{ this.props.contentFactory(this.props.contentProps, this.contentRef) }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,10 @@ import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
RoomPreviewOpts,
|
||||||
|
RoomViewLifecycle,
|
||||||
|
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -34,6 +38,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
|
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||||
|
|
||||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||||
|
|
||||||
|
@ -313,6 +318,18 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.NotLoggedIn: {
|
case MessageCase.NotLoggedIn: {
|
||||||
|
const opts: RoomPreviewOpts = { canJoin: false };
|
||||||
|
if (this.props.room?.roomId) {
|
||||||
|
ModuleRunner.instance
|
||||||
|
.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, this.props.room.roomId);
|
||||||
|
}
|
||||||
|
if (opts.canJoin) {
|
||||||
|
title = _t("Join the room to participate");
|
||||||
|
primaryActionLabel = _t("Join");
|
||||||
|
primaryActionHandler = () => {
|
||||||
|
ModuleRunner.instance.invoke(RoomViewLifecycle.JoinFromRoomPreview, this.props.room.roomId);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
title = _t("Join the conversation with an account");
|
title = _t("Join the conversation with an account");
|
||||||
if (SettingsStore.getValue(UIFeature.Registration)) {
|
if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||||
primaryActionLabel = _t("Sign Up");
|
primaryActionLabel = _t("Sign Up");
|
||||||
|
@ -320,6 +337,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
secondaryActionLabel = _t("Sign In");
|
secondaryActionLabel = _t("Sign In");
|
||||||
secondaryActionHandler = this.onLoginClick;
|
secondaryActionHandler = this.onLoginClick;
|
||||||
|
}
|
||||||
if (this.props.previewLoading) {
|
if (this.props.previewLoading) {
|
||||||
footer = (
|
footer = (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -316,6 +316,11 @@ export enum Action {
|
||||||
*/
|
*/
|
||||||
OnLoggedIn = "on_logged_in",
|
OnLoggedIn = "on_logged_in",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites the existing login with fresh session credentials. Use with a OverwriteLoginPayload.
|
||||||
|
*/
|
||||||
|
OverwriteLogin = "overwrite_login",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when the PlatformPeg gets a new platform set upon it, should only happen once per app load lifecycle.
|
* Fired when the PlatformPeg gets a new platform set upon it, should only happen once per app load lifecycle.
|
||||||
* Fires with the PlatformSetPayload.
|
* Fires with the PlatformSetPayload.
|
||||||
|
|
25
src/dispatcher/payloads/OverwriteLoginPayload.ts
Normal file
25
src/dispatcher/payloads/OverwriteLoginPayload.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ActionPayload } from "../payloads";
|
||||||
|
import { Action } from "../actions";
|
||||||
|
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||||
|
|
||||||
|
export interface OverwriteLoginPayload extends ActionPayload {
|
||||||
|
action: Action.OverwriteLogin;
|
||||||
|
|
||||||
|
credentials: IMatrixClientCreds;
|
||||||
|
}
|
|
@ -1805,6 +1805,7 @@
|
||||||
"Joining …": "Joining …",
|
"Joining …": "Joining …",
|
||||||
"Loading …": "Loading …",
|
"Loading …": "Loading …",
|
||||||
"Rejecting invite …": "Rejecting invite …",
|
"Rejecting invite …": "Rejecting invite …",
|
||||||
|
"Join the room to participate": "Join the room to participate",
|
||||||
"Join the conversation with an account": "Join the conversation with an account",
|
"Join the conversation with an account": "Join the conversation with an account",
|
||||||
"Sign Up": "Sign Up",
|
"Sign Up": "Sign Up",
|
||||||
"Loading preview": "Loading preview",
|
"Loading preview": "Loading preview",
|
||||||
|
|
|
@ -28,6 +28,7 @@ import PlatformPeg from "./PlatformPeg";
|
||||||
import { SettingLevel } from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import { retry } from "./utils/promise";
|
import { retry } from "./utils/promise";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
|
import { ModuleRunner } from "./modules/ModuleRunner";
|
||||||
|
|
||||||
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
|
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
|
||||||
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
|
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
|
||||||
|
@ -609,15 +610,40 @@ export class CustomTranslationOptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function doRegisterTranslations(customTranslations: ICustomTranslations) {
|
||||||
|
// We convert the operator-friendly version into something counterpart can
|
||||||
|
// consume.
|
||||||
|
const langs: {
|
||||||
|
// same structure, just flipped key order
|
||||||
|
[lang: string]: {
|
||||||
|
[str: string]: string;
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
for (const [str, translations] of Object.entries(customTranslations)) {
|
||||||
|
for (const [lang, newStr] of Object.entries(translations)) {
|
||||||
|
if (!langs[lang]) langs[lang] = {};
|
||||||
|
langs[lang][str] = newStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, tell counterpart about our translations
|
||||||
|
for (const [lang, translations] of Object.entries(langs)) {
|
||||||
|
counterpart.registerTranslations(lang, translations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a custom translations file is configured, it will be parsed and registered.
|
* Any custom modules with translations to load are parsed first, followed by an
|
||||||
* If no customization is made, or the file can't be parsed, no action will be
|
* optionally defined translations file in the config. If no customization is made,
|
||||||
* taken.
|
* or the file can't be parsed, no action will be taken.
|
||||||
*
|
*
|
||||||
* This function should be called *after* registering other translations data to
|
* This function should be called *after* registering other translations data to
|
||||||
* ensure it overrides strings properly.
|
* ensure it overrides strings properly.
|
||||||
*/
|
*/
|
||||||
export async function registerCustomTranslations() {
|
export async function registerCustomTranslations() {
|
||||||
|
const moduleTranslations = ModuleRunner.instance.allTranslations;
|
||||||
|
doRegisterTranslations(moduleTranslations);
|
||||||
|
|
||||||
const lookupUrl = SdkConfig.get().custom_translations_url;
|
const lookupUrl = SdkConfig.get().custom_translations_url;
|
||||||
if (!lookupUrl) return; // easy - nothing to do
|
if (!lookupUrl) return; // easy - nothing to do
|
||||||
|
|
||||||
|
@ -639,25 +665,8 @@ export async function registerCustomTranslations() {
|
||||||
// If the (potentially cached) json is invalid, don't use it.
|
// If the (potentially cached) json is invalid, don't use it.
|
||||||
if (!json) return;
|
if (!json) return;
|
||||||
|
|
||||||
// We convert the operator-friendly version into something counterpart can
|
// Finally, register it.
|
||||||
// consume.
|
doRegisterTranslations(json);
|
||||||
const langs: {
|
|
||||||
// same structure, just flipped key order
|
|
||||||
[lang: string]: {
|
|
||||||
[str: string]: string;
|
|
||||||
};
|
|
||||||
} = {};
|
|
||||||
for (const [str, translations] of Object.entries(json)) {
|
|
||||||
for (const [lang, newStr] of Object.entries(translations)) {
|
|
||||||
if (!langs[lang]) langs[lang] = {};
|
|
||||||
langs[lang][str] = newStr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, tell counterpart about our translations
|
|
||||||
for (const [lang, translations] of Object.entries(langs)) {
|
|
||||||
counterpart.registerTranslations(lang, translations);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// We consume all exceptions because it's considered non-fatal for custom
|
// We consume all exceptions because it's considered non-fatal for custom
|
||||||
// translations to break. Most failures will be during initial development
|
// translations to break. Most failures will be during initial development
|
||||||
|
|
45
src/modules/AppModule.ts
Normal file
45
src/modules/AppModule.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
40
src/modules/ModuleComponents.tsx
Normal file
40
src/modules/ModuleComponents.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 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 => props.onChange(e.target.value)}
|
||||||
|
label={props.label}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
ModuleSpinner.renderFactory = () => <Spinner />;
|
20
src/modules/ModuleFactory.ts
Normal file
20
src/modules/ModuleFactory.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
85
src/modules/ModuleRunner.ts
Normal file
85
src/modules/ModuleRunner.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 { AppModule } from "./AppModule";
|
||||||
|
import { ModuleFactory } from "./ModuleFactory";
|
||||||
|
import "./ModuleComponents";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles and coordinates the operation of modules.
|
||||||
|
*/
|
||||||
|
export class ModuleRunner {
|
||||||
|
public static readonly instance = new ModuleRunner();
|
||||||
|
|
||||||
|
private modules: AppModule[] = [];
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// we only want one instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the runner, clearing all known modules.
|
||||||
|
*
|
||||||
|
* Intended for test usage only.
|
||||||
|
*/
|
||||||
|
public reset() {
|
||||||
|
this.modules = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)) {
|
||||||
|
if (!merged[lang]) merged[lang] = {};
|
||||||
|
for (const [str, val] of Object.entries(strings)) {
|
||||||
|
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) {
|
||||||
|
this.modules.push(new AppModule(factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
191
src/modules/ProxiedModuleApi.ts
Normal file
191
src/modules/ProxiedModuleApi.ts
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
|
||||||
|
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
import { 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 { PlainSubstitution } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
||||||
|
import * as Matrix from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import { _t } 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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: string, variables?: Record<string, PlainSubstitution>): string {
|
||||||
|
return _t(s, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public openDialog<
|
||||||
|
M extends object,
|
||||||
|
P extends DialogProps = DialogProps,
|
||||||
|
C extends React.Component = React.Component,
|
||||||
|
>(
|
||||||
|
title: string,
|
||||||
|
body: (props: P, ref: React.RefObject<C>) => React.ReactNode,
|
||||||
|
): Promise<{ didOkOrSubmit: boolean, model: M }> {
|
||||||
|
return new Promise<{ didOkOrSubmit: boolean, model: M }>((resolve) => {
|
||||||
|
Modal.createDialog(ModuleUiDialog, {
|
||||||
|
title: title,
|
||||||
|
contentFactory: body,
|
||||||
|
contentProps: <DialogProps>{
|
||||||
|
moduleApi: this,
|
||||||
|
},
|
||||||
|
}, "mx_CompoundDialog").finished.then(([didOkOrSubmit, model]) => {
|
||||||
|
resolve({ didOkOrSubmit, model });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public async registerSimpleAccount(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName?: string,
|
||||||
|
): Promise<AccountAuthInfo> {
|
||||||
|
const hsUrl = SdkConfig.get("validated_server_config").hsUrl;
|
||||||
|
const client = Matrix.createClient({ baseUrl: hsUrl });
|
||||||
|
const deviceName = SdkConfig.get("default_device_display_name")
|
||||||
|
|| PlatformPeg.get().getDefaultDeviceDisplayName();
|
||||||
|
const req = {
|
||||||
|
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> {
|
||||||
|
dispatcher.dispatch<OverwriteLoginPayload>({
|
||||||
|
action: Action.OverwriteLogin,
|
||||||
|
credentials: {
|
||||||
|
...accountInfo,
|
||||||
|
guest: false,
|
||||||
|
},
|
||||||
|
}, true); // require to be sync to match inherited interface behaviour
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public async navigatePermalink(uri: string, andJoin?: boolean): Promise<void> {
|
||||||
|
navigateToPermalink(uri);
|
||||||
|
|
||||||
|
const parts = parsePermalink(uri);
|
||||||
|
if (parts.roomIdOrAlias && andJoin) {
|
||||||
|
let roomId = parts.roomIdOrAlias;
|
||||||
|
let servers = parts.viaServers;
|
||||||
|
if (roomId.startsWith("#")) {
|
||||||
|
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
|
||||||
|
if (!roomId) {
|
||||||
|
// alias resolution failed
|
||||||
|
const result = await MatrixClientPeg.get().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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public getConfigValue<T>(namespace: string, key: string): T {
|
||||||
|
// 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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,10 +46,10 @@ import { WidgetType } from "../../widgets/WidgetType";
|
||||||
import { CHAT_EFFECTS } from "../../effects";
|
import { CHAT_EFFECTS } from "../../effects";
|
||||||
import { containsEmoji } from "../../effects/utils";
|
import { containsEmoji } from "../../effects/utils";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks";
|
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { RoomViewStore } from "../RoomViewStore";
|
import { RoomViewStore } from "../RoomViewStore";
|
||||||
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
||||||
|
import { navigateToPermalink } from "../../utils/permalinks/navigator";
|
||||||
|
|
||||||
// TODO: Purge this from the universe
|
// TODO: Purge this from the universe
|
||||||
|
|
||||||
|
@ -280,10 +280,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async navigate(uri: string): Promise<void> {
|
public async navigate(uri: string): Promise<void> {
|
||||||
const localUri = tryTransformPermalinkToLocalHref(uri);
|
navigateToPermalink(uri);
|
||||||
if (!localUri || localUri === uri) { // parse failure can lead to an unmodified URL
|
|
||||||
throw new Error("Failed to transform URI");
|
|
||||||
}
|
|
||||||
window.location.hash = localUri; // it'll just be a fragment
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
src/utils/permalinks/navigator.ts
Normal file
30
src/utils/permalinks/navigator.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { tryTransformPermalinkToLocalHref } from "./Permalinks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a permalink to a local HREF and navigates accordingly. Throws if the permalink
|
||||||
|
* cannot be transformed.
|
||||||
|
* @param uri The permalink to navigate to.
|
||||||
|
*/
|
||||||
|
export function navigateToPermalink(uri: string): void {
|
||||||
|
const localUri = tryTransformPermalinkToLocalHref(uri);
|
||||||
|
if (!localUri || localUri === uri) { // parse failure can lead to an unmodified URL
|
||||||
|
throw new Error("Failed to transform URI");
|
||||||
|
}
|
||||||
|
window.location.hash = localUri; // it'll just be a fragment
|
||||||
|
}
|
36
test/modules/AppModule-test.ts
Normal file
36
test/modules/AppModule-test.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MockModule } from "./MockModule";
|
||||||
|
import { AppModule } from "../../src/modules/AppModule";
|
||||||
|
|
||||||
|
describe("AppModule", () => {
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("should call the factory immediately", () => {
|
||||||
|
let module: MockModule;
|
||||||
|
const appModule = new AppModule((api) => {
|
||||||
|
if (module) {
|
||||||
|
throw new Error("State machine error: Factory called twice");
|
||||||
|
}
|
||||||
|
module = new MockModule(api);
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
expect(appModule.module).toBeDefined();
|
||||||
|
expect(appModule.module).toBe(module);
|
||||||
|
expect(appModule.api).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
45
test/modules/MockModule.ts
Normal file
45
test/modules/MockModule.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
||||||
|
import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
|
||||||
|
|
||||||
|
import { ModuleRunner } from "../../src/modules/ModuleRunner";
|
||||||
|
|
||||||
|
export class MockModule extends RuntimeModule {
|
||||||
|
public get apiInstance(): ModuleApi {
|
||||||
|
return this.moduleApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(moduleApi: ModuleApi) {
|
||||||
|
super(moduleApi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerMockModule(): MockModule {
|
||||||
|
let module: MockModule;
|
||||||
|
ModuleRunner.instance.registerModule(api => {
|
||||||
|
if (module) {
|
||||||
|
throw new Error("State machine error: ModuleRunner created the module twice");
|
||||||
|
}
|
||||||
|
module = new MockModule(api);
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
if (!module) {
|
||||||
|
throw new Error("State machine error: ModuleRunner did not create module");
|
||||||
|
}
|
||||||
|
return module;
|
||||||
|
}
|
41
test/modules/ModuleComponents-test.tsx
Normal file
41
test/modules/ModuleComponents-test.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { mount } from "enzyme";
|
||||||
|
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 "../../src/modules/ModuleRunner";
|
||||||
|
|
||||||
|
describe("Module Components", () => {
|
||||||
|
// Note: we're not testing to see if there's components that are missing a renderFactory()
|
||||||
|
// but rather that the renderFactory() for components we do know about is actually defined
|
||||||
|
// and working.
|
||||||
|
//
|
||||||
|
// We do this by deliberately not importing the ModuleComponents file itself, relying on the
|
||||||
|
// ModuleRunner import to do its job (as per documentation in ModuleComponents).
|
||||||
|
|
||||||
|
it("should override the factory for a TextInputField", () => {
|
||||||
|
const component = mount(<TextInputField label="My Label" value="My Value" onChange={() => {}} />);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should override the factory for a ModuleSpinner", () => {
|
||||||
|
const component = mount(<ModuleSpinner />);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
54
test/modules/ModuleRunner-test.ts
Normal file
54
test/modules/ModuleRunner-test.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RoomPreviewOpts, RoomViewLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
|
|
||||||
|
import { MockModule, registerMockModule } from "./MockModule";
|
||||||
|
import { ModuleRunner } from "../../src/modules/ModuleRunner";
|
||||||
|
|
||||||
|
describe("ModuleRunner", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
ModuleRunner.instance.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Translations implicitly tested by ProxiedModuleApi integration tests.
|
||||||
|
|
||||||
|
describe("invoke", () => {
|
||||||
|
it("should invoke to every registered module", async () => {
|
||||||
|
const module1 = registerMockModule();
|
||||||
|
const module2 = registerMockModule();
|
||||||
|
|
||||||
|
const wrapEmit = (module: MockModule) => new Promise((resolve) => {
|
||||||
|
module.on(RoomViewLifecycle.PreviewRoomNotLoggedIn, (val1, val2) => {
|
||||||
|
resolve([val1, val2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const promises = Promise.all([
|
||||||
|
wrapEmit(module1),
|
||||||
|
wrapEmit(module2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const opts: RoomPreviewOpts = { canJoin: false };
|
||||||
|
ModuleRunner.instance.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, roomId);
|
||||||
|
const results = await promises;
|
||||||
|
expect(results).toEqual([
|
||||||
|
[opts, roomId], // module 1
|
||||||
|
[opts, roomId], // module 2
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
79
test/modules/ProxiedModuleApi-test.ts
Normal file
79
test/modules/ProxiedModuleApi-test.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
|
||||||
|
|
||||||
|
import { ProxiedModuleApi } from "../../src/modules/ProxiedModuleApi";
|
||||||
|
import { stubClient } from "../test-utils";
|
||||||
|
import { setLanguage } from "../../src/languageHandler";
|
||||||
|
import { ModuleRunner } from "../../src/modules/ModuleRunner";
|
||||||
|
import { registerMockModule } from "./MockModule";
|
||||||
|
|
||||||
|
describe("ProxiedApiModule", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
ModuleRunner.instance.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Remainder is implicitly tested from end-to-end tests of modules.
|
||||||
|
|
||||||
|
describe("translations", () => {
|
||||||
|
it("should cache translations", () => {
|
||||||
|
const api = new ProxiedModuleApi();
|
||||||
|
expect(api.translations).toBeFalsy();
|
||||||
|
|
||||||
|
const translations: TranslationStringsObject = {
|
||||||
|
["custom string"]: {
|
||||||
|
"en": "custom string",
|
||||||
|
"fr": "custom french string",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
api.registerTranslations(translations);
|
||||||
|
expect(api.translations).toBe(translations);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("integration", () => {
|
||||||
|
it("should translate strings using translation system", async () => {
|
||||||
|
// Test setup
|
||||||
|
stubClient();
|
||||||
|
|
||||||
|
// Set up a module to pull translations through
|
||||||
|
const module = registerMockModule();
|
||||||
|
const en = "custom string";
|
||||||
|
const de = "custom german string";
|
||||||
|
const enVars = "custom variable %(var)s";
|
||||||
|
const varVal = "string";
|
||||||
|
const deVars = "custom german variable %(var)s";
|
||||||
|
const deFull = `custom german variable ${varVal}`;
|
||||||
|
expect(module.apiInstance).toBeInstanceOf(ProxiedModuleApi);
|
||||||
|
module.apiInstance.registerTranslations({
|
||||||
|
[en]: {
|
||||||
|
"en": en,
|
||||||
|
"de": de,
|
||||||
|
},
|
||||||
|
[enVars]: {
|
||||||
|
"en": enVars,
|
||||||
|
"de": deVars,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await setLanguage("de"); // calls `registerCustomTranslations()` for us
|
||||||
|
|
||||||
|
// See if we can pull the German string out
|
||||||
|
expect(module.apiInstance.translateString(en)).toEqual(de);
|
||||||
|
expect(module.apiInstance.translateString(enVars, { var: varVal })).toEqual(deFull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
66
test/modules/__snapshots__/ModuleComponents-test.tsx.snap
Normal file
66
test/modules/__snapshots__/ModuleComponents-test.tsx.snap
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Module Components should override the factory for a ModuleSpinner 1`] = `
|
||||||
|
<Spinner>
|
||||||
|
<Spinner
|
||||||
|
h={32}
|
||||||
|
w={32}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
className="mx_Spinner_icon"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 32,
|
||||||
|
"width": 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Spinner>
|
||||||
|
</Spinner>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Module Components should override the factory for a TextInputField 1`] = `
|
||||||
|
<TextInputField
|
||||||
|
label="My Label"
|
||||||
|
onChange={[Function]}
|
||||||
|
value="My Value"
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
autoComplete="off"
|
||||||
|
element="input"
|
||||||
|
label="My Label"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="text"
|
||||||
|
validateOnBlur={true}
|
||||||
|
validateOnChange={true}
|
||||||
|
validateOnFocus={true}
|
||||||
|
value="My Value"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_Field mx_Field_input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
id="mx_Field_1"
|
||||||
|
label="My Label"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
placeholder="My Label"
|
||||||
|
type="text"
|
||||||
|
value="My Value"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="mx_Field_1"
|
||||||
|
>
|
||||||
|
My Label
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
</TextInputField>
|
||||||
|
`;
|
14
yarn.lock
14
yarn.lock
|
@ -1111,6 +1111,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.17.9":
|
||||||
|
version "7.18.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
|
||||||
|
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.16.7", "@babel/template@^7.3.3":
|
"@babel/template@^7.16.7", "@babel/template@^7.3.3":
|
||||||
version "7.16.7"
|
version "7.16.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
|
||||||
|
@ -1572,6 +1579,13 @@
|
||||||
version "3.2.8"
|
version "3.2.8"
|
||||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856"
|
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856"
|
||||||
|
|
||||||
|
"@matrix-org/react-sdk-module-api@^0.0.3":
|
||||||
|
version "0.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-0.0.3.tgz#a7ac1b18a72d18d08290b81fa33b0d8d00a77d2b"
|
||||||
|
integrity sha512-jQmLhVIanuX0g7Jx1OIqlzs0kp72PfSpv3umi55qVPYcAPQmO252AUs0vncatK8O4e013vohdnNhly19a/kmLQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.17.9"
|
||||||
|
|
||||||
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
|
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
|
||||||
version "2.1.8-no-fsevents.3"
|
version "2.1.8-no-fsevents.3"
|
||||||
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b"
|
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b"
|
||||||
|
|
Loading…
Reference in a new issue