You've already forked element-web
mirror of
https://github.com/element-hq/element-web.git
synced 2025-07-31 19:44:30 +03:00
Initial support for runtime modules (#29104)
* Initial runtime Modules work Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Comments Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
committed by
GitHub
parent
3c690e685a
commit
4a231c6450
@ -592,3 +592,4 @@ The following are undocumented or intended for developer use only.
|
|||||||
2. `sync_timeline_limit`
|
2. `sync_timeline_limit`
|
||||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
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.
|
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.
|
||||||
|
@ -38,6 +38,8 @@ const config: Config = {
|
|||||||
"^!!raw-loader!.*": "jest-raw-loader",
|
"^!!raw-loader!.*": "jest-raw-loader",
|
||||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
"^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)).+$"],
|
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
|
@ -80,6 +80,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@element-hq/element-web-module-api": "^0.1.1",
|
||||||
"@fontsource/inconsolata": "^5",
|
"@fontsource/inconsolata": "^5",
|
||||||
"@fontsource/inter": "^5",
|
"@fontsource/inter": "^5",
|
||||||
"@formatjs/intl-segmenter": "^11.5.7",
|
"@formatjs/intl-segmenter": "^11.5.7",
|
||||||
|
35
playwright/e2e/modules/loader.spec.ts
Normal file
35
playwright/e2e/modules/loader.spec.ts
Normal file
@ -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
playwright/sample-files/example-module.js
Normal file
16
playwright/sample-files/example-module.js
Normal file
@ -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
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
@ -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 "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
||||||
import "@types/modernizr";
|
import "@types/modernizr";
|
||||||
|
|
||||||
|
import type { ModuleLoader } from "@element-hq/element-web-module-api";
|
||||||
import type { logger } from "matrix-js-sdk/src/logger";
|
import type { logger } from "matrix-js-sdk/src/logger";
|
||||||
import type ContentMessages from "../ContentMessages";
|
import type ContentMessages from "../ContentMessages";
|
||||||
import { type IMatrixClientPeg } from "../MatrixClientPeg";
|
import { type IMatrixClientPeg } from "../MatrixClientPeg";
|
||||||
@ -45,6 +46,7 @@ import { type MatrixDispatcher } from "../dispatcher/dispatcher";
|
|||||||
import { type DeepReadonly } from "./common";
|
import { type DeepReadonly } from "./common";
|
||||||
import type MatrixChat from "../components/structures/MatrixChat";
|
import type MatrixChat from "../components/structures/MatrixChat";
|
||||||
import { type InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore";
|
import { type InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore";
|
||||||
|
import { type ModuleApiType } from "../modules/Api.ts";
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
@ -122,6 +124,8 @@ declare global {
|
|||||||
mxRoomScrollStateStore?: RoomScrollStateStore;
|
mxRoomScrollStateStore?: RoomScrollStateStore;
|
||||||
mxActiveWidgetStore?: ActiveWidgetStore;
|
mxActiveWidgetStore?: ActiveWidgetStore;
|
||||||
mxOnRecaptchaLoaded?: () => void;
|
mxOnRecaptchaLoaded?: () => void;
|
||||||
|
mxModuleLoader: ModuleLoader;
|
||||||
|
mxModuleApi: ModuleApiType;
|
||||||
|
|
||||||
// electron-only
|
// electron-only
|
||||||
electron?: Electron;
|
electron?: Electron;
|
||||||
|
@ -206,6 +206,8 @@ export interface IConfigOptions {
|
|||||||
policy_uri?: string;
|
policy_uri?: string;
|
||||||
contacts?: string[];
|
contacts?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
modules?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISsoRedirectOptions {
|
export interface ISsoRedirectOptions {
|
||||||
|
@ -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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
import type { AliasCustomisations } from "@element-hq/element-web-module-api";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up `IAliasCustomisations`.
|
// customisation points that make up `AliasCustomisations`.
|
||||||
export default {} as IAliasCustomisations;
|
export default {} as AliasCustomisations;
|
||||||
|
@ -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.
|
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";
|
import { type ExportFormat, type ExportType } from "../utils/exportUtils/exportUtils";
|
||||||
|
|
||||||
export type ForceChatExportParameters = {
|
export type ForceChatExportParameters = ReturnType<
|
||||||
format?: ExportFormat;
|
ChatExportCustomisations<ExportFormat, ExportType>["getForceChatExportParameters"]
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force parameters in room chat export
|
* Force parameters in room chat export
|
||||||
@ -30,15 +23,8 @@ const getForceChatExportParameters = (): ForceChatExportParameters => {
|
|||||||
return {};
|
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
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up `IChatExportCustomisations`.
|
// customisation points that make up `IChatExportCustomisations`.
|
||||||
export default {
|
export default {
|
||||||
getForceChatExportParameters,
|
getForceChatExportParameters,
|
||||||
} as IChatExportCustomisations;
|
} as ChatExportCustomisations<ExportFormat, ExportType>;
|
||||||
|
@ -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.
|
// Populate this class with the details of your customisations when copying it.
|
||||||
|
|
||||||
import { type UIComponent } from "../settings/UIFeature";
|
import { type ComponentVisibilityCustomisations as IComponentVisibilityCustomisations } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up the interface above.
|
// customisation points that make up the interface above.
|
||||||
|
@ -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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
import type { DirectoryCustomisations } from "@element-hq/element-web-module-api";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up `IDirectoryCustomisations`.
|
// customisation points that make up `IDirectoryCustomisations`.
|
||||||
export default {} as IDirectoryCustomisations;
|
export default {} as DirectoryCustomisations;
|
||||||
|
@ -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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
import type { LifecycleCustomisations } from "@element-hq/element-web-module-api";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up `ILifecycleCustomisations`.
|
// customisation points that make up `ILifecycleCustomisations`.
|
||||||
export default {} as ILifecycleCustomisations;
|
export default {} as LifecycleCustomisations;
|
||||||
|
@ -10,6 +10,7 @@ import { type MatrixClient, parseErrorResponse, type ResizeMethod } from "matrix
|
|||||||
import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
||||||
import { type Optional } from "matrix-events-sdk";
|
import { type Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
|
import type { MediaCustomisations, Media } from "@element-hq/element-web-module-api";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { type IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
|
import { type IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
|
||||||
import { UserFriendlyError } from "../languageHandler";
|
import { UserFriendlyError } from "../languageHandler";
|
||||||
@ -25,7 +26,7 @@ import { UserFriendlyError } from "../languageHandler";
|
|||||||
* A media object is a representation of a "source media" and an optional
|
* A media object is a representation of a "source media" and an optional
|
||||||
* "thumbnail media", derived from event contents or external sources.
|
* "thumbnail media", derived from event contents or external sources.
|
||||||
*/
|
*/
|
||||||
export class Media {
|
class MediaImplementation implements Media {
|
||||||
private client: MatrixClient;
|
private client: MatrixClient;
|
||||||
|
|
||||||
// Per above, this constructor signature can be whatever is helpful for you.
|
// Per above, this constructor signature can be whatever is helpful for you.
|
||||||
@ -149,22 +150,27 @@ export class Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { Media };
|
||||||
|
|
||||||
|
type BaseMedia = MediaCustomisations<Partial<MediaEventContent>, MatrixClient, IPreparedMedia>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a media object from event content.
|
* Creates a media object from event content.
|
||||||
* @param {MediaEventContent} content The event content.
|
* @param {MediaEventContent} content The event content.
|
||||||
* @param {MatrixClient} client? Optional client to use.
|
* @param {MatrixClient} client Optional client to use.
|
||||||
* @returns {Media} The media object.
|
* @returns {MediaImplementation} The media object.
|
||||||
*/
|
*/
|
||||||
export function mediaFromContent(content: Partial<MediaEventContent>, client?: MatrixClient): Media {
|
export const mediaFromContent: BaseMedia["mediaFromContent"] = (
|
||||||
return new Media(prepEventContentAsMedia(content), client);
|
content: Partial<MediaEventContent>,
|
||||||
}
|
client?: MatrixClient,
|
||||||
|
): Media => new MediaImplementation(prepEventContentAsMedia(content), client);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a media object from an MXC URI.
|
* Creates a media object from an MXC URI.
|
||||||
* @param {string} mxc The MXC URI.
|
* @param {string} mxc The MXC URI.
|
||||||
* @param {MatrixClient} client? Optional client to use.
|
* @param {MatrixClient} client Optional client to use.
|
||||||
* @returns {Media} The media object.
|
* @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);
|
return mediaFromContent({ url: mxc }, client);
|
||||||
}
|
};
|
||||||
|
@ -8,31 +8,10 @@
|
|||||||
|
|
||||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import type { RoomListCustomisations as IRoomListCustomisations } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
// Populate this file with the details of your customisations when copying it.
|
// Populate this file with the details of your customisations when copying it.
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up the interface above.
|
// customisation points that make up the interface above.
|
||||||
export const RoomListCustomisations: IRoomListCustomisations = {};
|
export const RoomListCustomisations: IRoomListCustomisations<Room> = {};
|
||||||
|
@ -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.
|
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
|
* Customise display of the user identifier
|
||||||
* hide userId for guests, display 3pid
|
* hide userId for guests, display 3pid
|
||||||
@ -19,15 +21,8 @@ function getDisplayUserIdentifier(
|
|||||||
return userId;
|
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
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up `IUserIdentifierCustomisations`.
|
// customisation points that make up `IUserIdentifierCustomisations`.
|
||||||
export default {
|
export default {
|
||||||
getDisplayUserIdentifier,
|
getDisplayUserIdentifier,
|
||||||
} as IUserIdentifierCustomisations;
|
} as UserIdentifierCustomisations;
|
||||||
|
@ -9,33 +9,8 @@
|
|||||||
// Populate this class with the details of your customisations when copying it.
|
// Populate this class with the details of your customisations when copying it.
|
||||||
import { type Capability, type Widget } from "matrix-widget-api";
|
import { type Capability, type Widget } from "matrix-widget-api";
|
||||||
|
|
||||||
/**
|
import type { WidgetPermissionsCustomisations } from "@element-hq/element-web-module-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up the interface above.
|
// customisation points that make up the interface above.
|
||||||
export const WidgetPermissionCustomisations: IWidgetPermissionCustomisations = {};
|
export const WidgetPermissionCustomisations: WidgetPermissionsCustomisations<Widget, Capability> = {};
|
||||||
|
@ -7,41 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Populate this class with the details of your customisations when copying it.
|
// Populate this class with the details of your customisations when copying it.
|
||||||
import { type ITemplateParams } from "matrix-widget-api";
|
import { type WidgetVariablesCustomisations } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a partial set of the variables needed to render any widget. If
|
|
||||||
* variables are missing or not provided then they will be filled with the
|
|
||||||
* application-determined defaults.
|
|
||||||
*
|
|
||||||
* This will not be called until after isReady() resolves.
|
|
||||||
* @returns {Partial<Omit<ITemplateParams, "widgetRoomId">>} The variables.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
function provideVariables(): Partial<Omit<ITemplateParams, "widgetRoomId">> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves to whether or not the customisation point is ready for variables
|
|
||||||
* to be provided. This will block widgets being rendered.
|
|
||||||
* @returns {Promise<boolean>} Resolves when ready.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
async function isReady(): Promise<void> {
|
|
||||||
return; // default no waiting
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 IWidgetVariablesCustomisations {
|
|
||||||
provideVariables?: typeof provideVariables;
|
|
||||||
|
|
||||||
// If not provided, the app will assume that the customisation is always ready.
|
|
||||||
isReady?: typeof isReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
// customisation points that make up the interface above.
|
// customisation points that make up the interface above.
|
||||||
export const WidgetVariableCustomisations: IWidgetVariablesCustomisations = {};
|
export const WidgetVariableCustomisations: WidgetVariablesCustomisations = {};
|
||||||
|
75
src/modules/Api.ts
Normal file
75
src/modules/Api.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
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 type { Api, RuntimeModuleConstructor, Config } from "@element-hq/element-web-module-api";
|
||||||
|
import { ModuleRunner } from "./ModuleRunner.ts";
|
||||||
|
import AliasCustomisations from "../customisations/Alias.ts";
|
||||||
|
import { RoomListCustomisations } from "../customisations/RoomList.ts";
|
||||||
|
import ChatExportCustomisations from "../customisations/ChatExport.ts";
|
||||||
|
import { ComponentVisibilityCustomisations } from "../customisations/ComponentVisibility.ts";
|
||||||
|
import DirectoryCustomisations from "../customisations/Directory.ts";
|
||||||
|
import LifecycleCustomisations from "../customisations/Lifecycle.ts";
|
||||||
|
import * as MediaCustomisations from "../customisations/Media.ts";
|
||||||
|
import UserIdentifierCustomisations from "../customisations/UserIdentifier.ts";
|
||||||
|
import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissions.ts";
|
||||||
|
import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts";
|
||||||
|
import SdkConfig from "../SdkConfig.ts";
|
||||||
|
|
||||||
|
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
|
||||||
|
let used = false;
|
||||||
|
return (customisations: T) => {
|
||||||
|
if (used) throw new Error("Legacy customisations can only be registered by one module");
|
||||||
|
Object.assign(baseCustomisations, customisations);
|
||||||
|
used = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ConfigApi {
|
||||||
|
public get(): Config;
|
||||||
|
public get<K extends keyof Config>(key: K): Config[K];
|
||||||
|
public get<K extends keyof Config = never>(key?: K): Config | Config[K] {
|
||||||
|
if (key === undefined) {
|
||||||
|
return SdkConfig.get() as Config;
|
||||||
|
}
|
||||||
|
return SdkConfig.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the @element-hq/element-web-module-api runtime module API.
|
||||||
|
*/
|
||||||
|
class ModuleApi implements Api {
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
public async _registerLegacyModule(LegacyModule: RuntimeModuleConstructor): Promise<void> {
|
||||||
|
ModuleRunner.instance.registerModule((api) => new LegacyModule(api));
|
||||||
|
}
|
||||||
|
public readonly _registerLegacyAliasCustomisations = legacyCustomisationsFactory(AliasCustomisations);
|
||||||
|
public readonly _registerLegacyChatExportCustomisations = legacyCustomisationsFactory(ChatExportCustomisations);
|
||||||
|
public readonly _registerLegacyComponentVisibilityCustomisations = legacyCustomisationsFactory(
|
||||||
|
ComponentVisibilityCustomisations,
|
||||||
|
);
|
||||||
|
public readonly _registerLegacyDirectoryCustomisations = legacyCustomisationsFactory(DirectoryCustomisations);
|
||||||
|
public readonly _registerLegacyLifecycleCustomisations = legacyCustomisationsFactory(LifecycleCustomisations);
|
||||||
|
public readonly _registerLegacyMediaCustomisations = legacyCustomisationsFactory(MediaCustomisations);
|
||||||
|
public readonly _registerLegacyRoomListCustomisations = legacyCustomisationsFactory(RoomListCustomisations);
|
||||||
|
public readonly _registerLegacyUserIdentifierCustomisations =
|
||||||
|
legacyCustomisationsFactory(UserIdentifierCustomisations);
|
||||||
|
public readonly _registerLegacyWidgetPermissionsCustomisations =
|
||||||
|
legacyCustomisationsFactory(WidgetPermissionCustomisations);
|
||||||
|
public readonly _registerLegacyWidgetVariablesCustomisations =
|
||||||
|
legacyCustomisationsFactory(WidgetVariableCustomisations);
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
public readonly config = new ConfigApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModuleApiType = ModuleApi;
|
||||||
|
|
||||||
|
if (!window.mxModuleApi) {
|
||||||
|
window.mxModuleApi = new ModuleApi();
|
||||||
|
}
|
||||||
|
export default window.mxModuleApi;
|
@ -31,10 +31,6 @@ import { parseQs } from "./url_utils";
|
|||||||
import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
|
import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
|
||||||
import { UserFriendlyError } from "../languageHandler";
|
import { UserFriendlyError } from "../languageHandler";
|
||||||
|
|
||||||
// add React and ReactPerf to the global namespace, to make them easier to access via the console
|
|
||||||
// this incidentally means we can forget our React imports in JSX files without penalty.
|
|
||||||
window.React = React;
|
|
||||||
|
|
||||||
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
|
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
|
||||||
|
|
||||||
window.matrixLogger = logger;
|
window.matrixLogger = logger;
|
||||||
|
@ -114,6 +114,7 @@ async function start(): Promise<void> {
|
|||||||
loadTheme,
|
loadTheme,
|
||||||
loadApp,
|
loadApp,
|
||||||
loadModules,
|
loadModules,
|
||||||
|
loadPlugins,
|
||||||
showError,
|
showError,
|
||||||
showIncompatibleBrowser,
|
showIncompatibleBrowser,
|
||||||
_t,
|
_t,
|
||||||
@ -159,10 +160,12 @@ async function start(): Promise<void> {
|
|||||||
// now that the config is ready, try to persist logs
|
// now that the config is ready, try to persist logs
|
||||||
const persistLogsPromise = setupLogStorage();
|
const persistLogsPromise = setupLogStorage();
|
||||||
|
|
||||||
// Load modules before language to ensure any custom translations are respected, and any app
|
// Load modules & plugins before language to ensure any custom translations are respected, and any app
|
||||||
// startup functionality is run
|
// startup functionality is run
|
||||||
const loadModulesPromise = loadModules();
|
const loadModulesPromise = loadModules();
|
||||||
await settled(loadModulesPromise);
|
await settled(loadModulesPromise);
|
||||||
|
const loadPluginsPromise = loadPlugins();
|
||||||
|
await settled(loadPluginsPromise);
|
||||||
|
|
||||||
// Load language after loading config.json so that settingsDefaults.language can be applied
|
// Load language after loading config.json so that settingsDefaults.language can be applied
|
||||||
const loadLanguagePromise = loadLanguage();
|
const loadLanguagePromise = loadLanguage();
|
||||||
@ -215,6 +218,7 @@ async function start(): Promise<void> {
|
|||||||
// app load critical path starts here
|
// app load critical path starts here
|
||||||
// assert things started successfully
|
// assert things started successfully
|
||||||
// ##################################
|
// ##################################
|
||||||
|
await loadPluginsPromise;
|
||||||
await loadModulesPromise;
|
await loadModulesPromise;
|
||||||
await loadThemePromise;
|
await loadThemePromise;
|
||||||
await loadLanguagePromise;
|
await loadLanguagePromise;
|
||||||
|
@ -11,6 +11,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import React, { StrictMode } from "react";
|
import React, { StrictMode } from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { ModuleLoader } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
import type { QueryDict } from "matrix-js-sdk/src/utils";
|
import type { QueryDict } from "matrix-js-sdk/src/utils";
|
||||||
import * as languageHandler from "../languageHandler";
|
import * as languageHandler from "../languageHandler";
|
||||||
@ -24,6 +25,7 @@ import ElectronPlatform from "./platform/ElectronPlatform";
|
|||||||
import PWAPlatform from "./platform/PWAPlatform";
|
import PWAPlatform from "./platform/PWAPlatform";
|
||||||
import WebPlatform from "./platform/WebPlatform";
|
import WebPlatform from "./platform/WebPlatform";
|
||||||
import { initRageshake, initRageshakeStore } from "./rageshakesetup";
|
import { initRageshake, initRageshakeStore } from "./rageshakesetup";
|
||||||
|
import ModuleApi from "../modules/Api.ts";
|
||||||
|
|
||||||
export const rageshakePromise = initRageshake();
|
export const rageshakePromise = initRageshake();
|
||||||
|
|
||||||
@ -125,6 +127,9 @@ export async function showIncompatibleBrowser(onAccept: () => void): Promise<voi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated in favour of the plugin system
|
||||||
|
*/
|
||||||
export async function loadModules(): Promise<void> {
|
export async function loadModules(): Promise<void> {
|
||||||
const { INSTALLED_MODULES } = await import("../modules.js");
|
const { INSTALLED_MODULES } = await import("../modules.js");
|
||||||
for (const InstalledModule of INSTALLED_MODULES) {
|
for (const InstalledModule of INSTALLED_MODULES) {
|
||||||
@ -132,6 +137,24 @@ export async function loadModules(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadPlugins(): Promise<void> {
|
||||||
|
// Add React to the global namespace, this is part of the new Module API contract to avoid needing
|
||||||
|
// every single module to ship its own copy of React. This also makes it easier to access via the console
|
||||||
|
// and incidentally means we can forget our React imports in JSX files without penalty.
|
||||||
|
window.React = React;
|
||||||
|
|
||||||
|
const modules = SdkConfig.get("modules");
|
||||||
|
if (!modules?.length) return;
|
||||||
|
const moduleLoader = new ModuleLoader(ModuleApi);
|
||||||
|
window.mxModuleLoader = moduleLoader;
|
||||||
|
for (const src of modules) {
|
||||||
|
// We need to instruct webpack to not mangle this import as it is not available at compile time
|
||||||
|
const module = await import(/* webpackIgnore: true */ src);
|
||||||
|
await moduleLoader.load(module);
|
||||||
|
}
|
||||||
|
await moduleLoader.start();
|
||||||
|
}
|
||||||
|
|
||||||
export { _t } from "../languageHandler";
|
export { _t } from "../languageHandler";
|
||||||
|
|
||||||
export { extractErrorMessageFromError } from "../components/views/dialogs/ErrorDialog";
|
export { extractErrorMessageFromError } from "../components/views/dialogs/ErrorDialog";
|
||||||
|
@ -1522,6 +1522,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
|
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
|
||||||
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
|
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
|
||||||
|
|
||||||
|
"@element-hq/element-web-module-api@^0.1.1":
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-0.1.1.tgz#e2b24aa38aa9f7b6af3c4993e6402a8b7e2f3cb5"
|
||||||
|
integrity sha512-qtEQD5nFaRJ+vfAis7uhKB66SyCjrz7O+qGz/hKJjgNhBLT/6C5DK90waKINXSw0J3stFR43IWzEk5GBOrTMow==
|
||||||
|
|
||||||
"@eslint-community/eslint-utils@^4.2.0":
|
"@eslint-community/eslint-utils@^4.2.0":
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||||
|
Reference in New Issue
Block a user