diff --git a/apps/web/playwright/e2e/modules/settings.spec.ts b/apps/web/playwright/e2e/modules/settings.spec.ts new file mode 100644 index 0000000000..76791c97ba --- /dev/null +++ b/apps/web/playwright/e2e/modules/settings.spec.ts @@ -0,0 +1,34 @@ +/* +Copyright 2026 Element Creations 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"; +import { getSampleFilePath } from "../../sample-files"; + +test.describe("Settings API", () => { + test.use({ + displayName: "Manny", + config: { + modules: ["/modules/settings-module.js"], + setting_defaults: { + language: "de", + }, + }, + page: async ({ page }, use) => { + await page.route("/modules/settings-module.js", async (route) => { + await route.fulfill({ path: getSampleFilePath("settings-module.js") }); + }); + await use(page); + }, + }); + + test("should read a config-resolved setting value via api.settings.getValue", async ({ page }) => { + const dialogPromise = page.waitForEvent("dialog"); + await page.goto("/"); + const dialog = await dialogPromise; + expect(dialog.message()).toBe("de"); + }); +}); diff --git a/apps/web/playwright/sample-files/settings-module.js b/apps/web/playwright/sample-files/settings-module.js new file mode 100644 index 0000000000..7bf1d787c3 --- /dev/null +++ b/apps/web/playwright/sample-files/settings-module.js @@ -0,0 +1,16 @@ +/* +Copyright 2026 Element Creations 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 SettingsModule { + static moduleApiVersion = "^*"; + constructor(api) { + this.api = api; + } + async load() { + alert(this.api.settings.getValue("language")); + } +} diff --git a/apps/web/src/modules/Api.ts b/apps/web/src/modules/Api.ts index 789e8144d2..24b9306b7e 100644 --- a/apps/web/src/modules/Api.ts +++ b/apps/web/src/modules/Api.ts @@ -34,6 +34,7 @@ import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts"; import { WidgetApi } from "./WidgetApi.ts"; import { CustomisationsApi } from "./customisationsApi.ts"; import { ComposerApi } from "./ComposerApi.ts"; +import { SettingsApi } from "./SettingsApi.ts"; import defaultDispatcher from "../dispatcher/dispatcher.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { @@ -98,6 +99,7 @@ export class ModuleApi implements Api { public readonly client = new ClientApi(); public readonly stores = new StoresApi(); public readonly composer = new ComposerApi(defaultDispatcher); + public readonly settings = new SettingsApi(); public createRoot(element: Element): Root { return createRoot(element); diff --git a/apps/web/src/modules/SettingsApi.test.ts b/apps/web/src/modules/SettingsApi.test.ts new file mode 100644 index 0000000000..45bee785bb --- /dev/null +++ b/apps/web/src/modules/SettingsApi.test.ts @@ -0,0 +1,27 @@ +/* +Copyright 2026 Element Creations 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 { describe, it, expect, vi } from "vitest"; + +import SettingsStore from "../settings/SettingsStore"; +import { SettingsApi } from "./SettingsApi"; + +describe("SettingsApi", () => { + it("should return the value from SettingsStore.getValue", () => { + const spy = vi.spyOn(SettingsStore, "getValue").mockReturnValue("en" as any); + const api = new SettingsApi(); + expect(api.getValue("language")).toBe("en"); + expect(spy).toHaveBeenCalledWith("language", undefined, undefined); + }); + + it("should pass roomId and excludeDefault through to SettingsStore.getValue", () => { + const spy = vi.spyOn(SettingsStore, "getValue").mockReturnValue(null); + const api = new SettingsApi(); + api.getValue("m.setting", "!room:example.org", true); + expect(spy).toHaveBeenCalledWith("m.setting", "!room:example.org", true); + }); +}); diff --git a/apps/web/src/modules/SettingsApi.ts b/apps/web/src/modules/SettingsApi.ts new file mode 100644 index 0000000000..8140395a8f --- /dev/null +++ b/apps/web/src/modules/SettingsApi.ts @@ -0,0 +1,16 @@ +/* +Copyright 2026 Element Creations 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 { SettingsApi as ISettingsApi } from "@element-hq/element-web-module-api"; +import SettingsStore from "../settings/SettingsStore"; + +export class SettingsApi implements ISettingsApi { + public getValue(settingName: string, roomId?: string | null, excludeDefault?: boolean): T | undefined { + //@ts-expect-error: SettingsStore.getValue will throw on an invalid setting name anyway. + return SettingsStore.getValue(settingName, roomId, excludeDefault); + } +} diff --git a/packages/module-api/element-web-module-api.api.md b/packages/module-api/element-web-module-api.api.md index 2318c1028a..6557e89ac2 100644 --- a/packages/module-api/element-web-module-api.api.md +++ b/packages/module-api/element-web-module-api.api.md @@ -61,6 +61,8 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx readonly i18n: I18nApi; readonly navigation: NavigationApi; readonly rootNode: HTMLElement; + // @alpha + readonly settings: SettingsApi; readonly stores: StoresApi; // @alpha readonly widget: WidgetApi; @@ -478,6 +480,11 @@ export interface RoomViewProps { // @alpha @deprecated (undocumented) export type RuntimeModuleConstructor = new (api: ModuleApi) => RuntimeModule; +// @alpha +export interface SettingsApi { + getValue(settingName: string, roomId?: string | null, excludeDefault?: boolean): T | undefined; +} + // @alpha export interface SpacePanelItemProps { className?: string; diff --git a/packages/module-api/src/api/index.ts b/packages/module-api/src/api/index.ts index b8716bbbce..9d1556d97f 100644 --- a/packages/module-api/src/api/index.ts +++ b/packages/module-api/src/api/index.ts @@ -24,6 +24,7 @@ import { type WidgetLifecycleApi } from "./widget-lifecycle.ts"; import { type WidgetApi } from "./widget.ts"; import { type CustomisationsApi } from "./customisations.ts"; import { type ComposerApi } from "./composer.ts"; +import { type SettingsApi } from "./settings.ts"; /** * Module interface for modules to implement. @@ -166,6 +167,12 @@ export interface Api */ readonly composer: ComposerApi; + /** + * Allows modules to read application settings. + * @alpha Subject to change. + */ + readonly settings: SettingsApi; + /** * Create a ReactDOM root for rendering React components. * Exposed to allow modules to avoid needing to bundle their own ReactDOM. diff --git a/packages/module-api/src/api/settings.ts b/packages/module-api/src/api/settings.ts new file mode 100644 index 0000000000..12f4089511 --- /dev/null +++ b/packages/module-api/src/api/settings.ts @@ -0,0 +1,22 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +/** + * API for reading application settings. + * @alpha Subject to change. + */ +export interface SettingsApi { + /** + * Gets the value of a setting, computed across all applicable levels + * (device, room, account, config, default, etc.). + * @param settingName - The name of the setting to read. + * @param roomId - Room ID to read a room-scoped value for, or null/undefined for a + * non-room-scoped value. + * @param excludeDefault - If true, do not fall back to the setting's default value. + */ + getValue(settingName: string, roomId?: string | null, excludeDefault?: boolean): T | undefined; +} diff --git a/packages/module-api/src/index.ts b/packages/module-api/src/index.ts index b573479549..71058f0a79 100644 --- a/packages/module-api/src/index.ts +++ b/packages/module-api/src/index.ts @@ -35,5 +35,6 @@ export type * from "./api/widget-lifecycle"; export type * from "./api/widget"; export type * from "./api/customisations"; export { UIComponent } from "./api/customisations"; +export type * from "./api/settings"; export * from "./api/watchable"; export type * from "./utils";