Skip to content

Commit

Permalink
Initial support for runtime modules (#29104)
Browse files Browse the repository at this point in the history
* Initial runtime Modules work

Signed-off-by: Michael Telatynski <[email protected]>

* Iterate

Signed-off-by: Michael Telatynski <[email protected]>

* Iterate

Signed-off-by: Michael Telatynski <[email protected]>

* Iterate

Signed-off-by: Michael Telatynski <[email protected]>

* Iterate

Signed-off-by: Michael Telatynski <[email protected]>

* Comments

Signed-off-by: Michael Telatynski <[email protected]>

---------

Signed-off-by: Michael Telatynski <[email protected]>
  • Loading branch information
t3chguy authored Feb 6, 2025
1 parent 3c690e6 commit 4a231c6
Show file tree
Hide file tree
Showing 22 changed files with 209 additions and 191 deletions.
1 change: 1 addition & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,3 +592,4 @@ The following are undocumented or intended for developer use only.
2. `sync_timeline_limit`
3. `dangerously_allow_unsafe_and_insecure_passwords`
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.
2 changes: 2 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const config: Config = {
"^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
// Requires ESM which is incompatible with our current Jest setup
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
},
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
collectCoverageFrom: [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "^0.1.1",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
Expand Down
35 changes: 35 additions & 0 deletions playwright/e2e/modules/loader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright 2025 New Vector 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 { test, expect } from "../../element-web-test";

test.describe("Module loading", () => {
test.use({
displayName: "Manny",
});

test.describe("Example Module", () => {
test.use({
config: {
modules: ["/modules/example-module.js"],
},
page: async ({ page }, use) => {
await page.route("/modules/example-module.js", async (route) => {
await route.fulfill({ path: "playwright/sample-files/example-module.js" });
});
await use(page);
},
});

test("should show alert", async ({ page }) => {
const dialogPromise = page.waitForEvent("dialog");
await page.goto("/");
const dialog = await dialogPromise;
expect(dialog.message()).toBe("Testing module loading successful!");
});
});
});
16 changes: 16 additions & 0 deletions playwright/sample-files/example-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Copyright 2025 New Vector 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.
*/

export default class ExampleModule {
static moduleApiVersion = "^0.1.0";
constructor(api) {
this.api = api;
}
async load() {
alert("Testing module loading successful!");
}
}
4 changes: 4 additions & 0 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import "@types/modernizr";

import type { ModuleLoader } from "@element-hq/element-web-module-api";
import type { logger } from "matrix-js-sdk/src/logger";
import type ContentMessages from "../ContentMessages";
import { type IMatrixClientPeg } from "../MatrixClientPeg";
Expand Down Expand Up @@ -45,6 +46,7 @@ import { type MatrixDispatcher } from "../dispatcher/dispatcher";
import { type DeepReadonly } from "./common";
import type MatrixChat from "../components/structures/MatrixChat";
import { type InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore";
import { type ModuleApiType } from "../modules/Api.ts";

/* eslint-disable @typescript-eslint/naming-convention */

Expand Down Expand Up @@ -122,6 +124,8 @@ declare global {
mxRoomScrollStateStore?: RoomScrollStateStore;
mxActiveWidgetStore?: ActiveWidgetStore;
mxOnRecaptchaLoaded?: () => void;
mxModuleLoader: ModuleLoader;
mxModuleApi: ModuleApiType;

// electron-only
electron?: Electron;
Expand Down
2 changes: 2 additions & 0 deletions src/IConfigOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ export interface IConfigOptions {
policy_uri?: string;
contacts?: string[];
};

modules?: string[];
}

export interface ISsoRedirectOptions {
Expand Down
17 changes: 3 additions & 14 deletions src/customisations/Alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,8 @@ 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.
*/

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getDisplayAliasForAliasSet(canonicalAlias: string | null, altAliases: string[]): string | null {
// E.g. prefer one of the aliases over another
return null;
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IAliasCustomisations {
getDisplayAliasForAliasSet?: typeof getDisplayAliasForAliasSet;
}
import type { AliasCustomisations } from "@element-hq/element-web-module-api";

// A real customisation module will define and export one or more of the
// customisation points that make up `IAliasCustomisations`.
export default {} as IAliasCustomisations;
// customisation points that make up `AliasCustomisations`.
export default {} as AliasCustomisations;
26 changes: 6 additions & 20 deletions src/customisations/ChatExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,13 @@ 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 ChatExportCustomisations } from "@element-hq/element-web-module-api";

import { type ExportFormat, type ExportType } from "../utils/exportUtils/exportUtils";

export type ForceChatExportParameters = {
format?: ExportFormat;
range?: ExportType;
// must be < 10**8
// only used when range is 'LastNMessages'
// default is 100
numberOfMessages?: number;
includeAttachments?: boolean;
// maximum size of exported archive
// must be > 0 and < 8000
sizeMb?: number;
};
export type ForceChatExportParameters = ReturnType<
ChatExportCustomisations<ExportFormat, ExportType>["getForceChatExportParameters"]
>;

/**
* Force parameters in room chat export
Expand All @@ -30,15 +23,8 @@ const getForceChatExportParameters = (): ForceChatExportParameters => {
return {};
};

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IChatExportCustomisations {
getForceChatExportParameters: typeof getForceChatExportParameters;
}

// A real customisation module will define and export one or more of the
// customisation points that make up `IChatExportCustomisations`.
export default {
getForceChatExportParameters,
} as IChatExportCustomisations;
} as ChatExportCustomisations<ExportFormat, ExportType>;
24 changes: 1 addition & 23 deletions src/customisations/ComponentVisibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,7 @@ Please see LICENSE files in the repository root for full details.

// Populate this class with the details of your customisations when copying it.

import { type UIComponent } from "../settings/UIFeature";

/**
* Determines whether or not the active MatrixClient user should be able to use
* the given UI component. If shown, the user might still not be able to use the
* component depending on their contextual permissions. For example, invite options
* might be shown to the user but they won't have permission to invite users to
* the current room: the button will appear disabled.
* @param {UIComponent} component The component to check visibility for.
* @returns {boolean} True (default) if the user is able to see the component, false
* otherwise.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function shouldShowComponent(component: UIComponent): boolean {
return true; // default to visible
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IComponentVisibilityCustomisations {
shouldShowComponent?: typeof shouldShowComponent;
}
import { type ComponentVisibilityCustomisations as IComponentVisibilityCustomisations } from "@element-hq/element-web-module-api";

// A real customisation module will define and export one or more of the
// customisation points that make up the interface above.
Expand Down
15 changes: 2 additions & 13 deletions src/customisations/Directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,8 @@ 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.
*/

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function requireCanonicalAliasAccessToPublish(): boolean {
// Some environments may not care about this requirement and could return false
return true;
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IDirectoryCustomisations {
requireCanonicalAliasAccessToPublish?: typeof requireCanonicalAliasAccessToPublish;
}
import type { DirectoryCustomisations } from "@element-hq/element-web-module-api";

// A real customisation module will define and export one or more of the
// customisation points that make up `IDirectoryCustomisations`.
export default {} as IDirectoryCustomisations;
export default {} as DirectoryCustomisations;
14 changes: 2 additions & 12 deletions src/customisations/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,8 @@ 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.
*/

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function onLoggedOutAndStorageCleared(): void {
// E.g. redirect user or call other APIs after logout
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface ILifecycleCustomisations {
onLoggedOutAndStorageCleared?: typeof onLoggedOutAndStorageCleared;
}
import type { LifecycleCustomisations } from "@element-hq/element-web-module-api";

// A real customisation module will define and export one or more of the
// customisation points that make up `ILifecycleCustomisations`.
export default {} as ILifecycleCustomisations;
export default {} as LifecycleCustomisations;
26 changes: 16 additions & 10 deletions src/customisations/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type MatrixClient, parseErrorResponse, type ResizeMethod } from "matrix
import { type MediaEventContent } from "matrix-js-sdk/src/types";
import { type Optional } from "matrix-events-sdk";

import type { MediaCustomisations, Media } from "@element-hq/element-web-module-api";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { type IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
import { UserFriendlyError } from "../languageHandler";
Expand All @@ -25,7 +26,7 @@ import { UserFriendlyError } from "../languageHandler";
* A media object is a representation of a "source media" and an optional
* "thumbnail media", derived from event contents or external sources.
*/
export class Media {
class MediaImplementation implements Media {
private client: MatrixClient;

// Per above, this constructor signature can be whatever is helpful for you.
Expand Down Expand Up @@ -149,22 +150,27 @@ export class Media {
}
}

export type { Media };

type BaseMedia = MediaCustomisations<Partial<MediaEventContent>, MatrixClient, IPreparedMedia>;

/**
* Creates a media object from event content.
* @param {MediaEventContent} content The event content.
* @param {MatrixClient} client? Optional client to use.
* @returns {Media} The media object.
* @param {MatrixClient} client Optional client to use.
* @returns {MediaImplementation} The media object.
*/
export function mediaFromContent(content: Partial<MediaEventContent>, client?: MatrixClient): Media {
return new Media(prepEventContentAsMedia(content), client);
}
export const mediaFromContent: BaseMedia["mediaFromContent"] = (
content: Partial<MediaEventContent>,
client?: MatrixClient,
): Media => new MediaImplementation(prepEventContentAsMedia(content), client);

/**
* Creates a media object from an MXC URI.
* @param {string} mxc The MXC URI.
* @param {MatrixClient} client? Optional client to use.
* @returns {Media} The media object.
* @param {MatrixClient} client Optional client to use.
* @returns {MediaImplementation} The media object.
*/
export function mediaFromMxc(mxc?: string, client?: MatrixClient): Media {
export const mediaFromMxc: BaseMedia["mediaFromMxc"] = (mxc?: string, client?: MatrixClient): Media => {
return mediaFromContent({ url: mxc }, client);
}
};
27 changes: 3 additions & 24 deletions src/customisations/RoomList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,10 @@

import { type Room } from "matrix-js-sdk/src/matrix";

// Populate this file with the details of your customisations when copying it.
import type { RoomListCustomisations as IRoomListCustomisations } from "@element-hq/element-web-module-api";

/**
* Determines if a room is visible in the room list or not. By default,
* all rooms are visible. Where special handling is performed by Element,
* those rooms will not be able to override their visibility in the room
* list - Element will make the decision without calling this function.
*
* This function should be as fast as possible to avoid slowing down the
* client.
* @param {Room} room The room to check the visibility of.
* @returns {boolean} True if the room should be visible, false otherwise.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function isRoomVisible(room: Room): boolean {
return true;
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IRoomListCustomisations {
isRoomVisible?: typeof isRoomVisible;
}
// Populate this file with the details of your customisations when copying it.

// A real customisation module will define and export one or more of the
// customisation points that make up the interface above.
export const RoomListCustomisations: IRoomListCustomisations = {};
export const RoomListCustomisations: IRoomListCustomisations<Room> = {};
11 changes: 3 additions & 8 deletions src/customisations/UserIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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 { UserIdentifierCustomisations } from "@element-hq/element-web-module-api";

/**
* Customise display of the user identifier
* hide userId for guests, display 3pid
Expand All @@ -19,15 +21,8 @@ function getDisplayUserIdentifier(
return userId;
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IUserIdentifierCustomisations {
getDisplayUserIdentifier: typeof getDisplayUserIdentifier;
}

// A real customisation module will define and export one or more of the
// customisation points that make up `IUserIdentifierCustomisations`.
export default {
getDisplayUserIdentifier,
} as IUserIdentifierCustomisations;
} as UserIdentifierCustomisations;
29 changes: 2 additions & 27 deletions src/customisations/WidgetPermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,8 @@
// Populate this class with the details of your customisations when copying it.
import { type Capability, type Widget } from "matrix-widget-api";

/**
* Approves the widget for capabilities that it requested, if any can be
* approved. Typically this will be used to give certain widgets capabilities
* without having to prompt the user to approve them. This cannot reject
* capabilities that Element will be automatically granting, such as the
* ability for Jitsi widgets to stay on screen - those will be approved
* regardless.
* @param {Widget} widget The widget to approve capabilities for.
* @param {Set<Capability>} requestedCapabilities The capabilities the widget requested.
* @returns {Set<Capability>} Resolves to the capabilities that are approved for use
* by the widget. If none are approved, this should return an empty Set.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function preapproveCapabilities(
widget: Widget,
requestedCapabilities: Set<Capability>,
): Promise<Set<Capability>> {
return new Set(); // no additional capabilities approved
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IWidgetPermissionCustomisations {
preapproveCapabilities?: typeof preapproveCapabilities;
}
import type { WidgetPermissionsCustomisations } from "@element-hq/element-web-module-api";

// A real customisation module will define and export one or more of the
// customisation points that make up the interface above.
export const WidgetPermissionCustomisations: IWidgetPermissionCustomisations = {};
export const WidgetPermissionCustomisations: WidgetPermissionsCustomisations<Widget, Capability> = {};
Loading

0 comments on commit 4a231c6

Please sign in to comment.