From 5d8804d7e823aa339bd1cf5b7a3b250e8870f199 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 17 Dec 2024 19:42:04 -0500 Subject: [PATCH 1/4] React to theme changes in widget mode --- src/useTheme.test.ts | 37 +++++++++++++++++++++++++++++++------ src/useTheme.ts | 43 ++++++++++++++++++++++++++++++++++++------- src/widget.ts | 7 ++++++- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/useTheme.test.ts b/src/useTheme.test.ts index d0927b35..886e7caa 100644 --- a/src/useTheme.test.ts +++ b/src/useTheme.test.ts @@ -15,13 +15,19 @@ import { test, vi, } from "vitest"; +import EventEmitter from "events"; import { useTheme } from "./useTheme"; -import { useUrlParams } from "./UrlParams"; +import { getUrlParams } from "./UrlParams"; +import { widget } from "./widget"; +import { WidgetApiToWidgetAction } from "matrix-widget-api"; -// 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", () => { @@ -46,7 +52,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 +67,7 @@ describe("useTheme", () => { }); test("should not reapply the same theme if it hasn't changed", () => { - (useUrlParams as Mock).mockReturnValue({ theme: "dark" }); + (getUrlParams as Mock).mockReturnValue({ theme: "dark" }); // Simulate a previous theme originalClassList.item = vi.fn().mockReturnValue("cpd-theme-dark"); @@ -75,4 +81,23 @@ describe("useTheme", () => { expect(document.body.classList.remove).toHaveBeenCalledWith("no-theme"); expect(originalClassList.add).not.toHaveBeenCalled(); }); + + test("theme changes in response to widget actions", () => { + renderHook(() => useTheme()); + + expect(originalClassList.add).toHaveBeenCalledWith("cpd-theme-dark"); + 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 3ad1ed9d..a599545b 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 fb1b1cfd..f2ce9b83 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, From e477c16a0b649396d34f5fbd0d605df692b7b405 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 2 Jan 2025 13:32:39 -0500 Subject: [PATCH 2/4] Fix lint errors --- src/useTheme.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useTheme.test.ts b/src/useTheme.test.ts index 886e7caa..4c542901 100644 --- a/src/useTheme.test.ts +++ b/src/useTheme.test.ts @@ -16,11 +16,11 @@ import { vi, } from "vitest"; import EventEmitter from "events"; +import { WidgetApiToWidgetAction } from "matrix-widget-api"; import { useTheme } from "./useTheme"; import { getUrlParams } from "./UrlParams"; import { widget } from "./widget"; -import { WidgetApiToWidgetAction } from "matrix-widget-api"; vi.mock("./UrlParams", () => ({ getUrlParams: vi.fn() })); vi.mock("./widget", () => ({ From cdde53cc4463ceceae31182f68fde2ea08b3e293 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 8 Jan 2025 11:27:33 -0500 Subject: [PATCH 3/4] Bump matrix-widget-api --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5fdd147f..a405c746 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6587,9 +6587,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" From acd4a1f17929daa7a3c566badf7fac3cb2085944 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 8 Jan 2025 11:41:44 -0500 Subject: [PATCH 4/4] Fix tests --- src/useTheme.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/useTheme.test.ts b/src/useTheme.test.ts index 8c2b57b8..650321a7 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, @@ -39,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(() => { @@ -67,7 +68,6 @@ describe("useTheme", () => { }); test("should not reapply the same theme if it hasn't changed", () => { - (getUrlParams as Mock).mockReturnValue({ theme: "dark" }); // Simulate a previous theme originalClassList.item = vi.fn().mockReturnValue("cpd-theme-dark"); @@ -82,15 +82,17 @@ describe("useTheme", () => { expect(originalClassList.add).not.toHaveBeenCalled(); }); - test("theme changes in response to widget actions", () => { + test("theme changes in response to widget actions", async () => { renderHook(() => useTheme()); expect(originalClassList.add).toHaveBeenCalledWith("cpd-theme-dark"); - widget!.lazyActions.emit( - WidgetApiToWidgetAction.ThemeChange, - new CustomEvent(WidgetApiToWidgetAction.ThemeChange, { - detail: { data: { name: "light" } }, - }), + await act(() => + widget!.lazyActions.emit( + WidgetApiToWidgetAction.ThemeChange, + new CustomEvent(WidgetApiToWidgetAction.ThemeChange, { + detail: { data: { name: "light" } }, + }), + ), ); expect(originalClassList.remove).toHaveBeenCalledWith( "cpd-theme-light",