diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index f417bd6045..14ac68fbd5 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -50,7 +50,7 @@ import ThemeController from "../../settings/controllers/ThemeController"; import { startAnyRegistrationFlow } from "../../Registration"; import ResizeNotifier from "../../utils/ResizeNotifier"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; -import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; +import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher"; import { FontWatcher } from "../../settings/watchers/FontWatcher"; import { storeRoomAliasInCache } from "../../RoomAliasCache"; import ToastStore from "../../stores/ToastStore"; @@ -133,6 +133,7 @@ import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView" import { LoginSplashView } from "./auth/LoginSplashView"; import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore"; +import { setTheme } from "../../theme"; // legacy export export { default as Views } from "../../Views"; @@ -465,6 +466,7 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher = new ThemeWatcher(); this.fontWatcher = new FontWatcher(); this.themeWatcher.start(); + this.themeWatcher.on(ThemeWatcherEvent.Change, setTheme); this.fontWatcher.start(); initSentry(SdkConfig.get("sentry")); @@ -497,6 +499,7 @@ export default class MatrixChat extends React.PureComponent { public componentWillUnmount(): void { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); + this.themeWatcher?.off(ThemeWatcherEvent.Change, setTheme); this.themeWatcher?.stop(); this.fontWatcher?.stop(); UIStore.destroy(); diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index 58c6c92a5e..1bc123c485 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -33,7 +33,7 @@ import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import { arrayFastClone } from "../../../utils/arrays"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ELEMENT_CLIENT_ID } from "../../../identifiers"; -import SettingsStore from "../../../settings/SettingsStore"; +import ThemeWatcher, { ThemeWatcherEvent } from "../../../settings/watchers/ThemeWatcher"; interface IProps { widgetDefinition: IModalWidgetOpenRequestData; @@ -54,6 +54,7 @@ export default class ModalWidgetDialog extends React.PureComponent = React.createRef(); + private readonly themeWatcher = new ThemeWatcher(); public state: IState = { disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter((b) => b.disabled).map((b) => b.id), @@ -77,6 +78,8 @@ export default class ModalWidgetDialog extends React.PureComponent { + this.themeWatcher.start(); + this.themeWatcher.on(ThemeWatcherEvent.Change, this.onThemeChange); + // Theme may have changed while messaging was starting + this.onThemeChange(this.themeWatcher.getEffectiveTheme()); this.state.messaging?.sendWidgetConfig(this.props.widgetDefinition); }; @@ -94,6 +101,10 @@ export default class ModalWidgetDialog extends React.PureComponent { + this.state.messaging?.updateTheme({ name: theme }); + }; + private onWidgetClose = (ev: CustomEvent): void => { this.props.onFinished(true, ev.detail.data); }; @@ -127,7 +138,7 @@ export default class ModalWidgetDialog extends React.PureComponent void; +} + +export default class ThemeWatcher extends TypedEventEmitter { private themeWatchRef?: string; private systemThemeWatchRef?: string; private dispatcherRef?: string; @@ -29,6 +38,7 @@ export default class ThemeWatcher { private currentTheme: string; public constructor() { + super(); // we have both here as each may either match or not match, so by having both // we can get the tristate of dark/light/unsupported this.preferDark = (global).matchMedia("(prefers-color-scheme: dark)"); @@ -72,9 +82,7 @@ export default class ThemeWatcher { public recheck(forceTheme?: string): void { const oldTheme = this.currentTheme; this.currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; - if (oldTheme !== this.currentTheme) { - setTheme(this.currentTheme); - } + if (oldTheme !== this.currentTheme) this.emit(ThemeWatcherEvent.Change, this.currentTheme); } public getEffectiveTheme(): string { diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 40e36473e3..59255213c6 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -37,7 +37,6 @@ import { MatrixClientPeg } from "../../MatrixClientPeg"; import { OwnProfileStore } from "../OwnProfileStore"; import WidgetUtils from "../../utils/WidgetUtils"; import { IntegrationManagers } from "../../integrations/IntegrationManagers"; -import SettingsStore from "../../settings/SettingsStore"; import { WidgetType } from "../../widgets/WidgetType"; import ActiveWidgetStore from "../ActiveWidgetStore"; import { objectShallowClone } from "../../utils/objects"; @@ -46,7 +45,7 @@ import { Action } from "../../dispatcher/actions"; import { ElementWidgetActions, IHangupCallApiRequest, IViewRoomApiRequest } from "./ElementWidgetActions"; import { ModalWidgetStore } from "../ModalWidgetStore"; import { IApp, isAppWidget } from "../WidgetStore"; -import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; +import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher"; import { getCustomTheme } from "../../theme"; import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities"; import { ELEMENT_CLIENT_ID } from "../../identifiers"; @@ -153,6 +152,7 @@ export class StopGapWidget extends EventEmitter { private roomId?: string; private kind: WidgetKind; private readonly virtual: boolean; + private readonly themeWatcher = new ThemeWatcher(); private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID // This promise will be called and needs to resolve before the widget will actually become sticky. private stickyPromise?: () => Promise; @@ -214,7 +214,7 @@ export class StopGapWidget extends EventEmitter { userDisplayName: OwnProfileStore.instance.displayName ?? undefined, userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl() ?? undefined, clientId: ELEMENT_CLIENT_ID, - clientTheme: SettingsStore.getValue("theme"), + clientTheme: this.themeWatcher.getEffectiveTheme(), clientLanguage: getUserLanguage(), deviceId: this.client.getDeviceId() ?? undefined, baseUrl: this.client.baseUrl, @@ -246,6 +246,10 @@ export class StopGapWidget extends EventEmitter { return !!this.messaging; } + private onThemeChange = (theme: string): void => { + this.messaging?.updateTheme({ name: theme }); + }; + private onOpenModal = async (ev: CustomEvent): Promise => { ev.preventDefault(); if (ModalWidgetStore.instance.canOpenModalWidget()) { @@ -278,9 +282,14 @@ export class StopGapWidget extends EventEmitter { this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging.on("preparing", () => this.emit("preparing")); this.messaging.on("error:preparing", (err: unknown) => this.emit("error:preparing", err)); - this.messaging.on("ready", () => { + this.messaging.once("ready", () => { WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.roomId, this.messaging!); this.emit("ready"); + + this.themeWatcher.start(); + this.themeWatcher.on(ThemeWatcherEvent.Change, this.onThemeChange); + // Theme may have changed while messaging was starting + this.onThemeChange(this.themeWatcher.getEffectiveTheme()); }); this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); diff --git a/test/components/views/dialogs/ModalWidgetDialog-test.tsx b/test/components/views/dialogs/ModalWidgetDialog-test.tsx new file mode 100644 index 0000000000..6ee5831f84 --- /dev/null +++ b/test/components/views/dialogs/ModalWidgetDialog-test.tsx @@ -0,0 +1,56 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { fireEvent, render } from "jest-matrix-react"; +import { ClientWidgetApi, MatrixWidgetType } from "matrix-widget-api"; +import React from "react"; +import { TooltipProvider } from "@vector-im/compound-web"; +import { mocked } from "jest-mock"; +import { findLast, last } from "lodash"; + +import ModalWidgetDialog from "../../../../src/components/views/dialogs/ModalWidgetDialog"; +import { stubClient } from "../../../test-utils"; +import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../src/dispatcher/actions"; +import SettingsStore from "../../../../src/settings/SettingsStore"; + +jest.mock("matrix-widget-api", () => ({ + ...jest.requireActual("matrix-widget-api"), + ClientWidgetApi: (jest.createMockFromModule("matrix-widget-api") as any).ClientWidgetApi, +})); + +describe("ModalWidgetDialog", () => { + it("informs the widget of theme changes", () => { + stubClient(); + let theme = "light"; + const settingsSpy = jest + .spyOn(SettingsStore, "getValue") + .mockImplementation((name) => (name === "theme" ? theme : undefined)); + try { + render( + + {}} + /> + , + ); + // Indicate that the widget is loaded and ready + fireEvent.load(document.getElementsByTagName("iframe").item(0)!); + const messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!); + findLast(messaging.once.mock.calls, ([eventName]) => eventName === "ready")![1](); + + // Now change the theme + theme = "dark"; + defaultDispatcher.dispatch({ action: Action.RecheckTheme }, true); + expect(messaging.updateTheme).toHaveBeenLastCalledWith({ name: "dark" }); + } finally { + settingsSpy.mockRestore(); + } + }); +}); diff --git a/test/unit-tests/stores/widgets/StopGapWidget-test.ts b/test/unit-tests/stores/widgets/StopGapWidget-test.ts index f767c96a02..a5006db7df 100644 --- a/test/unit-tests/stores/widgets/StopGapWidget-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidget-test.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { mocked, MockedObject } from "jest-mock"; -import { last } from "lodash"; +import { findLast, last } from "lodash"; import { MatrixEvent, MatrixClient, @@ -24,8 +24,13 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget"; import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore"; import SettingsStore from "../../../../src/settings/SettingsStore"; +import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../src/dispatcher/actions"; -jest.mock("matrix-widget-api/lib/ClientWidgetApi"); +jest.mock("matrix-widget-api", () => ({ + ...jest.requireActual("matrix-widget-api"), + ClientWidgetApi: (jest.createMockFromModule("matrix-widget-api") as any).ClientWidgetApi, +})); describe("StopGapWidget", () => { let client: MockedObject; @@ -84,6 +89,25 @@ describe("StopGapWidget", () => { expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false); }); + it("informs widget of theme changes", () => { + let theme = "light"; + const settingsSpy = jest + .spyOn(SettingsStore, "getValue") + .mockImplementation((name) => (name === "theme" ? theme : undefined)); + try { + // Indicate that the widget is ready + findLast(messaging.once.mock.calls, ([eventName]) => eventName === "ready")![1](); + + // Now change the theme + theme = "dark"; + defaultDispatcher.dispatch({ action: Action.RecheckTheme }, true); + expect(messaging.updateTheme).toHaveBeenLastCalledWith({ name: "dark" }); + } finally { + console.log("TEST OVER"); + settingsSpy.mockRestore(); + } + }); + describe("feed event", () => { let event1: MatrixEvent; let event2: MatrixEvent; diff --git a/yarn.lock b/yarn.lock index 3846aa17e8..17bc72ff46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8658,9 +8658,9 @@ matrix-web-i18n@^3.2.1: walk "^2.3.15" matrix-widget-api@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55" - integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw== + version "1.11.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.11.0.tgz#2f548b11a7c0df789d5d4fdb5cc9ef7af8aef3da" + integrity sha512-ED/9hrJqDWVLeED0g1uJnYRhINh3ZTquwurdM+Hc8wLVJIQ8G/r7A7z74NC+8bBIHQ1Jo7i1Uq5CoJp/TzFYrA== dependencies: "@types/events" "^3.0.0" events "^3.2.0"