diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index f55751420e..34c773d3c9 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -57,7 +57,6 @@ import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingSto import { SdkContextClass } from "../../../contexts/SDKContext"; import { ModuleRunner } from "../../../modules/ModuleRunner"; import { parseUrl } from "../../../utils/UrlUtils"; -import ThemeWatcher, { ThemeWatcherEvents } from "../../../settings/watchers/ThemeWatcher"; interface IProps { app: IWidget | IApp; @@ -116,7 +115,6 @@ interface IState { menuDisplayed: boolean; requiresClient: boolean; hasContextMenuOptions: boolean; - widgetUrl?: string; } export default class AppTile extends React.Component { @@ -142,7 +140,7 @@ export default class AppTile extends React.Component { private sgWidget: StopGapWidget | null; private dispatcherRef?: string; private unmounted = false; - private themeWatcher = new ThemeWatcher(); + public constructor(props: IProps, context: ContextType) { super(props); this.context = context; // XXX: workaround for lack of `declare` support on `public context!:` definition @@ -269,7 +267,6 @@ export default class AppTile extends React.Component { !newProps.userWidget, newProps.onDeleteClick, ), - widgetUrl: this.sgWidget?.embedUrl, }; } @@ -355,8 +352,6 @@ export default class AppTile extends React.Component { } private setupSgListeners(): void { - this.themeWatcher.on(ThemeWatcherEvents.ThemeChange, this.onThemeChanged); - this.themeWatcher.start(); this.sgWidget?.on("ready", this.onWidgetReady); this.sgWidget?.on("error:preparing", this.updateRequiresClient); // emits when the capabilities have been set up or changed @@ -364,9 +359,7 @@ export default class AppTile extends React.Component { } private stopSgListeners(): void { - this.themeWatcher.stop(); if (!this.sgWidget) return; - this.themeWatcher.off(ThemeWatcherEvents.ThemeChange, this.onThemeChanged); this.sgWidget?.off("ready", this.onWidgetReady); this.sgWidget.off("error:preparing", this.updateRequiresClient); this.sgWidget.off("capabilitiesNotified", this.updateRequiresClient); @@ -389,7 +382,6 @@ export default class AppTile extends React.Component { private startWidget(): void { this.sgWidget?.prepare().then(() => { if (this.unmounted) return; - if (!this.state.initialising) return; this.setState({ initialising: false }); }); } @@ -464,17 +456,6 @@ export default class AppTile extends React.Component { }); }; - private onThemeChanged = (): void => { - // Regenerate widget url when the theme changes - // this updates the url from e.g. `theme=light` to `theme=dark` - // We only do this with EC widgets where the theme prop is in the hash. If the widget puts the - // theme template variable outside the url hash this would cause a (IFrame) page reload on every theme change. - if (WidgetType.CALL.matches(this.props.app.type)) this.setState({ widgetUrl: this.sgWidget?.embedUrl }); - - // TODO: This is a stop gap solution to responsively update the theme of the widget. - // A new action should be introduced and the widget driver should be called here, so it informs the widget. (or connect to this by itself) - }; - private onAction = (payload: ActionPayload): void => { switch (payload.action) { case "m.sticker": @@ -567,9 +548,9 @@ export default class AppTile extends React.Component { this.resetWidget(this.props); this.startMessaging(); - if (this.iframe && this.state.widgetUrl) { + if (this.iframe && this.sgWidget) { // Reload iframe - this.iframe.src = this.state.widgetUrl; + this.iframe.src = this.sgWidget.embedUrl; } }); } @@ -638,7 +619,7 @@ export default class AppTile extends React.Component { "mx_AppTileBody--mini": this.props.miniMode, "mx_AppTileBody--loading": this.state.loading, // We don't want mx_AppTileBody (rounded corners) for call widgets - "mx_AppTileBody--call": WidgetType.CALL.matches(this.props.app.type), + "mx_AppTileBody--call": this.props.app.type === WidgetType.CALL.preferred, }); const appTileBodyStyles: CSSProperties = {}; if (this.props.pointerEvents) { @@ -667,7 +648,7 @@ export default class AppTile extends React.Component { @@ -695,7 +676,7 @@ export default class AppTile extends React.Component { title={widgetTitle} allow={iframeFeatures} ref={this.iframeRefChange} - src={this.state.widgetUrl} + src={this.sgWidget.embedUrl} allowFullScreen={true} sandbox={sandboxFlags} /> @@ -718,12 +699,7 @@ export default class AppTile extends React.Component { const zIndexAboveOtherPersistentElements = 101; appTileBody = ( -
+
void; -}; - -export default class ThemeWatcher extends TypedEventEmitter { +export default class ThemeWatcher { private themeWatchRef: string | null; private systemThemeWatchRef: string | null; private dispatcherRef: string | null; @@ -46,7 +37,6 @@ export default class ThemeWatcher extends TypedEventEmitter Promise; // This promise will be called and needs to resolve before the widget will actually become sticky. - private themeWatcher = new ThemeWatcher(); public constructor(private appTileProps: IAppTileProps) { super(); @@ -212,19 +212,13 @@ export class StopGapWidget extends EventEmitter { private runUrlTemplate(opts = { asPopout: false }): string { const fromCustomisation = WidgetVariableCustomisations?.provideVariables?.() ?? {}; - let theme = this.themeWatcher.getEffectiveTheme(); - if (theme.startsWith("custom-")) { - // simplify custom theme to only light/dark - const customTheme = getCustomTheme(theme.slice(7)); - theme = customTheme.is_dark ? "dark" : "light"; - } const defaults: ITemplateParams = { widgetRoomId: this.roomId, currentUserId: this.client.getUserId()!, userDisplayName: OwnProfileStore.instance.displayName ?? undefined, userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl() ?? undefined, clientId: ELEMENT_CLIENT_ID, - clientTheme: theme, + clientTheme: SettingsStore.getValue("theme"), clientLanguage: getUserLanguage(), deviceId: this.client.getDeviceId() ?? undefined, baseUrl: this.client.baseUrl, diff --git a/src/theme.ts b/src/theme.ts index d462db466c..8e2e893334 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -121,7 +121,7 @@ function clearCustomTheme(): void { // remove all css variables, we assume these are there because of the custom theme const inlineStyleProps = Object.values(document.body.style); for (const prop of inlineStyleProps) { - if (typeof prop === "string" && prop.startsWith("--")) { + if (prop.startsWith("--")) { document.body.style.removeProperty(prop); } } diff --git a/test/components/views/elements/AppTile-test.tsx b/test/components/views/elements/AppTile-test.tsx index b5510c7f3f..f15f0ce560 100644 --- a/test/components/views/elements/AppTile-test.tsx +++ b/test/components/views/elements/AppTile-test.tsx @@ -19,7 +19,7 @@ import { jest } from "@jest/globals"; import { Room, MatrixClient } from "matrix-js-sdk/src/matrix"; import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api"; import { Optional } from "matrix-events-sdk"; -import { act, render, RenderResult, waitFor } from "@testing-library/react"; +import { act, render, RenderResult } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { SpiedFunction } from "jest-mock"; import { @@ -50,8 +50,6 @@ import { ElementWidget } from "../../../../src/stores/widgets/StopGapWidget"; import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore"; import { ModuleRunner } from "../../../../src/modules/ModuleRunner"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; -import { SettingLevel } from "../../../../src/settings/SettingLevel"; -import { WidgetType } from "../../../../src/widgets/WidgetType"; jest.mock("../../../../src/stores/OwnProfileStore", () => ({ OwnProfileStore: { @@ -70,7 +68,6 @@ describe("AppTile", () => { const resizeNotifier = new ResizeNotifier(); let app1: IApp; let app2: IApp; - let appElementCall: IApp; const waitForRps = (roomId: string) => new Promise((resolve) => { @@ -123,17 +120,6 @@ describe("AppTile", () => { creatorUserId: cli.getSafeUserId(), avatar_url: undefined, }; - appElementCall = { - id: "1", - eventId: "2", - roomId: "r2", - type: WidgetType.CALL.preferred, - url: "https://example.com#theme=$org.matrix.msc2873.client_theme", - name: "Element Call", - creatorUserId: cli.getSafeUserId(), - avatar_url: undefined, - }; - jest.spyOn(WidgetStore.instance, "getApps").mockImplementation((roomId: string): Array => { if (roomId === "r1") return [app1]; if (roomId === "r2") return [app2]; @@ -453,6 +439,7 @@ describe("AppTile", () => { expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top); }); }); + describe("with an existing widgetApi with requiresClient = false", () => { beforeEach(() => { const api = { @@ -479,68 +466,6 @@ describe("AppTile", () => { }); }); - describe("with an element call widget", () => { - beforeEach(() => { - document.body.style.setProperty("--custom-color", "red"); - document.body.style.setProperty("normal-color", "blue"); - }); - it("should update the widget url on theme change", async () => { - const renderResult = render( - - - A - - - B - - - , - ); - await waitFor(() => { - expect(renderResult.getByTestId("widget-app-tile").dataset.testWidgetUrl).toEqual( - "https://example.com/?widgetId=1&parentUrl=http%3A%2F%2Flocalhost%2F#theme=light", - ); - }); - await SettingsStore.setValue("theme", null, SettingLevel.DEVICE, "dark"); - await waitFor(() => { - expect(renderResult.getByTestId("widget-app-tile").dataset.testWidgetUrl).toEqual( - "https://example.com/?widgetId=1&parentUrl=http%3A%2F%2Flocalhost%2F#theme=dark", - ); - }); - await SettingsStore.setValue("theme", null, SettingLevel.DEVICE, "light"); - await waitFor(() => { - expect(renderResult.getByTestId("widget-app-tile").dataset.testWidgetUrl).toEqual( - "https://example.com/?widgetId=1&parentUrl=http%3A%2F%2Flocalhost%2F#theme=light", - ); - }); - }); - it("should not update the widget url for non Element Call widgets on theme change", async () => { - const appNonElementCall = { ...appElementCall, type: MatrixWidgetType.Custom }; - const renderResult = render( - - - A - - - B - - - , - ); - await waitFor(() => { - expect(renderResult.getByTestId("widget-app-tile").dataset.testWidgetUrl).toEqual( - "https://example.com/?widgetId=1&parentUrl=http%3A%2F%2Flocalhost%2F#theme=light", - ); - }); - await SettingsStore.setValue("theme", null, SettingLevel.DEVICE, "dark"); - await waitFor(() => { - expect(renderResult.getByTestId("widget-app-tile").dataset.testWidgetUrl).toEqual( - "https://example.com/?widgetId=1&parentUrl=http%3A%2F%2Flocalhost%2F#theme=light", - ); - }); - }); - }); - describe("for a persistent app", () => { let renderResult: RenderResult; diff --git a/test/components/views/elements/__snapshots__/AppTile-test.tsx.snap b/test/components/views/elements/__snapshots__/AppTile-test.tsx.snap index 932306ac5c..92b89013e1 100644 --- a/test/components/views/elements/__snapshots__/AppTile-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/AppTile-test.tsx.snap @@ -85,8 +85,6 @@ exports[`AppTile for a persistent app should render 1`] = ` >
@@ -174,8 +172,6 @@ exports[`AppTile for a pinned widget should render 1`] = `
diff --git a/test/stores/widgets/StopGapWidget-test.ts b/test/stores/widgets/StopGapWidget-test.ts index e190aa91c0..9d1ade0f39 100644 --- a/test/stores/widgets/StopGapWidget-test.ts +++ b/test/stores/widgets/StopGapWidget-test.ts @@ -28,7 +28,6 @@ import { VoiceBroadcastInfoEventType, VoiceBroadcastRecording } from "../../../s import { SdkContextClass } from "../../../src/contexts/SDKContext"; import ActiveWidgetStore from "../../../src/stores/ActiveWidgetStore"; import SettingsStore from "../../../src/settings/SettingsStore"; -import * as Theme from "../../../src/theme"; jest.mock("matrix-widget-api/lib/ClientWidgetApi"); @@ -64,46 +63,18 @@ describe("StopGapWidget", () => { widget.stopMessaging(); }); - describe("url template", () => { - it("should replace parameters in widget url template", () => { - const originalGetValue = SettingsStore.getValue; - const spy = jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { - if (setting === "theme") return "my-theme-for-testing"; - return originalGetValue(setting); - }); - expect(widget.embedUrl).toBe( - "https://example.org/?user-id=%40userId%3Amatrix.org&device-id=ABCDEFGHI&base-url=https%3A%2F%2Fmatrix-client.matrix.org&theme=my-theme-for-testing&widgetId=test&parentUrl=http%3A%2F%2Flocalhost%2F", - ); - spy.mockRestore(); - }); - it("should replace custom theme with light/dark", () => { - const originalGetValue = SettingsStore.getValue; - const spy = jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { - if (setting === "theme") return "custom-my-theme"; - return originalGetValue(setting); - }); - jest.spyOn(Theme, "getCustomTheme").mockReturnValue({ is_dark: false } as unknown as Theme.CustomTheme); - expect(widget.embedUrl).toBe( - "https://example.org/?user-id=%40userId%3Amatrix.org&device-id=ABCDEFGHI&base-url=https%3A%2F%2Fmatrix-client.matrix.org&theme=light&widgetId=test&parentUrl=http%3A%2F%2Flocalhost%2F", - ); - jest.spyOn(Theme, "getCustomTheme").mockReturnValue({ is_dark: true } as unknown as Theme.CustomTheme); - expect(widget.embedUrl).toBe( - "https://example.org/?user-id=%40userId%3Amatrix.org&device-id=ABCDEFGHI&base-url=https%3A%2F%2Fmatrix-client.matrix.org&theme=dark&widgetId=test&parentUrl=http%3A%2F%2Flocalhost%2F", - ); - spy.mockRestore(); - }); - it("should replace parameters in widget popoutUrl template", () => { - const originalGetValue = SettingsStore.getValue; - const spy = jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { - if (setting === "theme") return "my-theme-for-testing"; - return originalGetValue(setting); - }); - expect(widget.popoutUrl).toBe( - "https://example.org/?user-id=%40userId%3Amatrix.org&device-id=ABCDEFGHI&base-url=https%3A%2F%2Fmatrix-client.matrix.org&theme=my-theme-for-testing", - ); - spy.mockRestore(); + it("should replace parameters in widget url template", () => { + const originGetValue = SettingsStore.getValue; + const spy = jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { + if (setting === "theme") return "my-theme-for-testing"; + return originGetValue(setting); }); + expect(widget.embedUrl).toBe( + "https://example.org/?user-id=%40userId%3Amatrix.org&device-id=ABCDEFGHI&base-url=https%3A%2F%2Fmatrix-client.matrix.org&theme=my-theme-for-testing&widgetId=test&parentUrl=http%3A%2F%2Flocalhost%2F", + ); + spy.mockClear(); }); + it("feeds incoming to-device messages to the widget", async () => { const event = mkEvent({ event: true,