Make themed widgets reflect the effective theme
So that widgets such as Element Call will show up in the right theme even if the app is set to match the system theme.pull/28342/head
parent
f1899b9eb1
commit
4d91e97f0f
|
@ -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<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"));
|
||||
|
@ -497,6 +499,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();
|
||||
|
|
|
@ -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<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),
|
||||
|
@ -77,6 +78,8 @@ 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);
|
||||
|
@ -84,6 +87,10 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
@ -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)");
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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<void>;
|
||||
|
@ -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<IModalWidgetOpenRequest>): Promise<void> => {
|
||||
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);
|
||||
|
|
|
@ -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(
|
||||
<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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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<MatrixClient>;
|
||||
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue