pull/28342/merge
Robin 2025-01-08 12:30:35 -05:00 committed by GitHub
commit 2524b29912
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 128 additions and 17 deletions

View File

@ -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();

View File

@ -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,
});

View File

@ -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 {

View File

@ -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);

View File

@ -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();
}
});
});

View File

@ -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;

View File

@ -8678,9 +8678,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"