Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for runtime modules #29104

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open
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 ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
Expand Down Expand Up @@ -45,6 +46,7 @@ import { MatrixDispatcher } from "../dispatcher/dispatcher";
import { DeepReadonly } from "./common";
import MatrixChat from "../components/structures/MatrixChat";
import { InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore";
import { 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;
25 changes: 5 additions & 20 deletions src/customisations/ChatExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

import type { ChatExportCustomisations } from "@element-hq/element-web-module-api";
import { ExportFormat, 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 +22,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 { 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 { MatrixClient, parseErrorResponse, ResizeMethod } from "matrix-js-sdk/sr
import { MediaEventContent } from "matrix-js-sdk/src/types";
import { Optional } from "matrix-events-sdk";

import type { MediaCustomisations, Media } from "@element-hq/element-web-module-api";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { 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 { 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 { Capability, 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
Loading