Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -131,6 +131,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";
Expand Down Expand Up @@ -463,6 +464,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.themeWatcher = new ThemeWatcher();
this.fontWatcher = new FontWatcher();
this.themeWatcher.start();
this.themeWatcher.on(ThemeWatcherEvent.Change, setTheme);
this.fontWatcher.start();

initSentry(SdkConfig.get("sentry"));
Expand Down Expand Up @@ -495,6 +497,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
public componentWillUnmount(): void {
Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef);
this.themeWatcher?.off(ThemeWatcherEvent.Change, setTheme);
this.themeWatcher?.stop();
this.fontWatcher?.stop();
UIStore.destroy();
Expand Down
15 changes: 13 additions & 2 deletions src/components/views/dialogs/ModalWidgetDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -54,6 +54,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
private readonly widget: Widget;
private readonly possibleButtons: ModalButtonID[];
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
private readonly themeWatcher = new ThemeWatcher();

public state: IState = {
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter((b) => b.disabled).map((b) => b.id),
Expand All @@ -77,13 +78,19 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
}

public componentWillUnmount(): void {
this.themeWatcher.off(ThemeWatcherEvent.Change, this.onThemeChange);
this.themeWatcher.stop();
if (!this.state.messaging) return;
this.state.messaging.off("ready", this.onReady);
this.state.messaging.off(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
this.state.messaging.stop();
}

private onReady = (): void => {
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);
};

Expand All @@ -94,6 +101,10 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
this.state.messaging.on(`action:${WidgetApiFromWidgetAction.SetModalButtonEnabled}`, this.onButtonEnableToggle);
};

private onThemeChange = (theme: string): void => {
this.state.messaging?.updateTheme({ name: theme });
};

private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>): void => {
this.props.onFinished(true, ev.detail.data);
};
Expand Down Expand Up @@ -127,7 +138,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
userDisplayName: OwnProfileStore.instance.displayName ?? undefined,
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl() ?? undefined,
clientId: ELEMENT_CLIENT_ID,
clientTheme: SettingsStore.getValue("theme"),
clientTheme: this.themeWatcher.getEffectiveTheme(),
clientLanguage: getUserLanguage(),
baseUrl: MatrixClientPeg.safeGet().baseUrl,
});
Expand Down
18 changes: 13 additions & 5 deletions src/settings/watchers/ThemeWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,25 @@ Please see LICENSE files in the repository root for full details.
*/

import { logger } from "matrix-js-sdk/src/logger";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";

import SettingsStore from "../SettingsStore";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import ThemeController from "../controllers/ThemeController";
import { findHighContrastTheme, setTheme } from "../../theme";
import { findHighContrastTheme } from "../../theme";
import { ActionPayload } from "../../dispatcher/payloads";
import { SettingLevel } from "../SettingLevel";

export default class ThemeWatcher {
export enum ThemeWatcherEvent {
Change = "change",
}

interface ThemeWatcherEventHandlerMap {
[ThemeWatcherEvent.Change]: (theme: string) => void;
}

export default class ThemeWatcher extends TypedEventEmitter<ThemeWatcherEvent, ThemeWatcherEventHandlerMap> {
private themeWatchRef?: string;
private systemThemeWatchRef?: string;
private dispatcherRef?: string;
Expand All @@ -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 = (<any>global).matchMedia("(prefers-color-scheme: dark)");
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 13 additions & 4 deletions src/stores/widgets/StopGapWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,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";
Expand All @@ -52,7 +51,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";
Expand Down Expand Up @@ -163,6 +162,7 @@ export class StopGapWidget extends EventEmitter {
private viewedRoomId: string | null = null;
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<void>;
Expand Down Expand Up @@ -213,7 +213,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,
Expand Down Expand Up @@ -245,6 +245,10 @@ export class StopGapWidget extends EventEmitter {
return !!this.messaging;
}

private onThemeChange = (theme: string): void => {
this.messaging?.updateTheme({ name: theme });
};

private onOpenModal = async (ev: CustomEvent<IModalWidgetOpenRequest>): Promise<void> => {
ev.preventDefault();
if (ModalWidgetStore.instance.canOpenModalWidget()) {
Expand Down Expand Up @@ -288,9 +292,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);
Expand Down
56 changes: 56 additions & 0 deletions test/components/views/dialogs/ModalWidgetDialog-test.tsx
Original file line number Diff line number Diff line change
@@ -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 : null));
try {
render(
<TooltipProvider>
<ModalWidgetDialog
widgetDefinition={{ type: MatrixWidgetType.Custom, url: "https://example.org" }}
sourceWidgetId=""
onFinished={() => {}}
/>
</TooltipProvider>,
);
// 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();
}
});
});
27 changes: 25 additions & 2 deletions test/unit-tests/stores/widgets/StopGapWidget-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/

import { mocked, MockedFunction, MockedObject } from "jest-mock";
import { last } from "lodash";
import { findLast, last } from "lodash";
import {
MatrixEvent,
MatrixClient,
Expand All @@ -27,10 +27,15 @@ 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";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore";

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<MatrixClient>;
Expand Down Expand Up @@ -104,6 +109,24 @@ describe("StopGapWidget", () => {
expect(messaging.feedStateUpdate).toHaveBeenCalledWith(event.getEffectiveEvent());
});

it("informs widget of theme changes", () => {
let theme = "light";
const settingsSpy = jest
.spyOn(SettingsStore, "getValue")
.mockImplementation((name) => (name === "theme" ? theme : null));
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 {
settingsSpy.mockRestore();
}
});

describe("feed event", () => {
let event1: MatrixEvent;
let event2: MatrixEvent;
Expand Down
Loading