diff --git a/src/useTheme.test.ts b/src/useTheme.test.ts index d7576d70e..650321a7a 100644 --- a/src/useTheme.test.ts +++ b/src/useTheme.test.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { renderHook } from "@testing-library/react"; +import { act, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, @@ -15,13 +15,19 @@ import { test, vi, } from "vitest"; +import EventEmitter from "events"; +import { WidgetApiToWidgetAction } from "matrix-widget-api"; import { useTheme } from "./useTheme"; -import { useUrlParams } from "./UrlParams"; +import { getUrlParams } from "./UrlParams"; +import { widget } from "./widget"; -// Mock the useUrlParams hook -vi.mock("./UrlParams", () => ({ - useUrlParams: vi.fn(), +vi.mock("./UrlParams", () => ({ getUrlParams: vi.fn() })); +vi.mock("./widget", () => ({ + widget: { + api: { transport: { reply: vi.fn() } }, + lazyActions: new EventEmitter(), + }, })); describe("useTheme", () => { @@ -33,6 +39,7 @@ describe("useTheme", () => { vi.spyOn(originalClassList, "add"); vi.spyOn(originalClassList, "remove"); vi.spyOn(originalClassList, "item").mockReturnValue(null); + (getUrlParams as Mock).mockReturnValue({ theme: "dark" }); }); afterEach(() => { @@ -46,7 +53,7 @@ describe("useTheme", () => { { setTheme: "light-high-contrast", add: ["cpd-theme-light-hc"] }, ])("apply procedure", ({ setTheme, add }) => { test(`should apply ${add[0]} theme when ${setTheme} theme is specified`, () => { - (useUrlParams as Mock).mockReturnValue({ theme: setTheme }); + (getUrlParams as Mock).mockReturnValue({ theme: setTheme }); renderHook(() => useTheme()); @@ -61,7 +68,6 @@ describe("useTheme", () => { }); test("should not reapply the same theme if it hasn't changed", () => { - (useUrlParams as Mock).mockReturnValue({ theme: "dark" }); // Simulate a previous theme originalClassList.item = vi.fn().mockReturnValue("cpd-theme-dark"); @@ -75,4 +81,25 @@ describe("useTheme", () => { expect(document.body.classList.remove).toHaveBeenCalledWith("no-theme"); expect(originalClassList.add).not.toHaveBeenCalled(); }); + + test("theme changes in response to widget actions", async () => { + renderHook(() => useTheme()); + + expect(originalClassList.add).toHaveBeenCalledWith("cpd-theme-dark"); + await act(() => + widget!.lazyActions.emit( + WidgetApiToWidgetAction.ThemeChange, + new CustomEvent(WidgetApiToWidgetAction.ThemeChange, { + detail: { data: { name: "light" } }, + }), + ), + ); + expect(originalClassList.remove).toHaveBeenCalledWith( + "cpd-theme-light", + "cpd-theme-dark", + "cpd-theme-light-hc", + "cpd-theme-dark-hc", + ); + expect(originalClassList.add).toHaveBeenLastCalledWith("cpd-theme-light"); + }); }); diff --git a/src/useTheme.ts b/src/useTheme.ts index 3ad1ed9d2..a599545b8 100644 --- a/src/useTheme.ts +++ b/src/useTheme.ts @@ -5,17 +5,46 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { useLayoutEffect, useRef } from "react"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { WidgetApiToWidgetAction } from "matrix-widget-api"; +import { type IThemeChangeActionRequest } from "matrix-widget-api/lib/interfaces/ThemeChangeAction"; -import { useUrlParams } from "./UrlParams"; +import { getUrlParams } from "./UrlParams"; +import { widget } from "./widget"; export const useTheme = (): void => { - const { theme: themeName } = useUrlParams(); + const [requestedTheme, setRequestedTheme] = useState( + () => getUrlParams().theme, + ); const previousTheme = useRef(document.body.classList.item(0)); + + useEffect(() => { + if (widget) { + const onThemeChange = ( + ev: CustomEvent, + ): void => { + ev.preventDefault(); + if ("name" in ev.detail.data && typeof ev.detail.data.name === "string") + setRequestedTheme(ev.detail.data.name); + widget!.api.transport.reply(ev.detail, {}); + }; + + widget.lazyActions.on(WidgetApiToWidgetAction.ThemeChange, onThemeChange); + return (): void => { + widget!.lazyActions.off( + WidgetApiToWidgetAction.ThemeChange, + onThemeChange, + ); + }; + } + }, []); + useLayoutEffect(() => { - // If the url does not contain a theme props we default to "dark". - const theme = themeName?.includes("light") ? "light" : "dark"; - const themeHighContrast = themeName?.includes("high-contrast") ? "-hc" : ""; + // If no theme has been explicitly requested we default to dark + const theme = requestedTheme?.includes("light") ? "light" : "dark"; + const themeHighContrast = requestedTheme?.includes("high-contrast") + ? "-hc" + : ""; const themeString = "cpd-theme-" + theme + themeHighContrast; if (themeString !== previousTheme.current) { document.body.classList.remove( @@ -28,5 +57,5 @@ export const useTheme = (): void => { previousTheme.current = themeString; } document.body.classList.remove("no-theme"); - }, [previousTheme, themeName]); + }, [previousTheme, requestedTheme]); }; diff --git a/src/widget.ts b/src/widget.ts index fb1b1cfdc..f2ce9b836 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -8,7 +8,11 @@ Please see LICENSE in the repository root for full details. import { logger } from "matrix-js-sdk/src/logger"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { createRoomWidgetClient } from "matrix-js-sdk/src/matrix"; -import { WidgetApi, MatrixCapabilities } from "matrix-widget-api"; +import { + WidgetApi, + MatrixCapabilities, + WidgetApiToWidgetAction, +} from "matrix-widget-api"; import type { MatrixClient } from "matrix-js-sdk/src/client"; import type { IWidgetApiRequest } from "matrix-widget-api"; @@ -70,6 +74,7 @@ export const widget = ((): WidgetHelpers | null => { // intend for the app to handle const lazyActions = new LazyEventEmitter(); [ + WidgetApiToWidgetAction.ThemeChange, ElementWidgetActions.JoinCall, ElementWidgetActions.HangupCall, ElementWidgetActions.TileLayout, diff --git a/yarn.lock b/yarn.lock index aa79781a4..ea5ce4e76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6457,9 +6457,9 @@ matrix-js-sdk@matrix-org/matrix-js-sdk#develop: uuid "11" 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"