Merge pull request #2910 from robintown/widget-theme-change

React to theme changes in widget mode
This commit is contained in:
Robin
2025-01-08 15:56:02 -05:00
committed by GitHub
4 changed files with 79 additions and 18 deletions

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { renderHook } from "@testing-library/react"; import { act, renderHook } from "@testing-library/react";
import { import {
afterEach, afterEach,
beforeEach, beforeEach,
@@ -15,13 +15,19 @@ import {
test, test,
vi, vi,
} from "vitest"; } from "vitest";
import EventEmitter from "events";
import { WidgetApiToWidgetAction } from "matrix-widget-api";
import { useTheme } from "./useTheme"; import { useTheme } from "./useTheme";
import { useUrlParams } from "./UrlParams"; import { getUrlParams } from "./UrlParams";
import { widget } from "./widget";
// Mock the useUrlParams hook vi.mock("./UrlParams", () => ({ getUrlParams: vi.fn() }));
vi.mock("./UrlParams", () => ({ vi.mock("./widget", () => ({
useUrlParams: vi.fn(), widget: {
api: { transport: { reply: vi.fn() } },
lazyActions: new EventEmitter(),
},
})); }));
describe("useTheme", () => { describe("useTheme", () => {
@@ -33,6 +39,7 @@ describe("useTheme", () => {
vi.spyOn(originalClassList, "add"); vi.spyOn(originalClassList, "add");
vi.spyOn(originalClassList, "remove"); vi.spyOn(originalClassList, "remove");
vi.spyOn(originalClassList, "item").mockReturnValue(null); vi.spyOn(originalClassList, "item").mockReturnValue(null);
(getUrlParams as Mock).mockReturnValue({ theme: "dark" });
}); });
afterEach(() => { afterEach(() => {
@@ -46,7 +53,7 @@ describe("useTheme", () => {
{ setTheme: "light-high-contrast", add: ["cpd-theme-light-hc"] }, { setTheme: "light-high-contrast", add: ["cpd-theme-light-hc"] },
])("apply procedure", ({ setTheme, add }) => { ])("apply procedure", ({ setTheme, add }) => {
test(`should apply ${add[0]} theme when ${setTheme} theme is specified`, () => { 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()); renderHook(() => useTheme());
@@ -61,7 +68,6 @@ describe("useTheme", () => {
}); });
test("should not reapply the same theme if it hasn't changed", () => { test("should not reapply the same theme if it hasn't changed", () => {
(useUrlParams as Mock).mockReturnValue({ theme: "dark" });
// Simulate a previous theme // Simulate a previous theme
originalClassList.item = vi.fn().mockReturnValue("cpd-theme-dark"); originalClassList.item = vi.fn().mockReturnValue("cpd-theme-dark");
@@ -75,4 +81,25 @@ describe("useTheme", () => {
expect(document.body.classList.remove).toHaveBeenCalledWith("no-theme"); expect(document.body.classList.remove).toHaveBeenCalledWith("no-theme");
expect(originalClassList.add).not.toHaveBeenCalled(); 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");
});
}); });

View File

@@ -5,17 +5,46 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. 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 => { export const useTheme = (): void => {
const { theme: themeName } = useUrlParams(); const [requestedTheme, setRequestedTheme] = useState(
() => getUrlParams().theme,
);
const previousTheme = useRef<string | null>(document.body.classList.item(0)); const previousTheme = useRef<string | null>(document.body.classList.item(0));
useEffect(() => {
if (widget) {
const onThemeChange = (
ev: CustomEvent<IThemeChangeActionRequest>,
): 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(() => { useLayoutEffect(() => {
// If the url does not contain a theme props we default to "dark". // If no theme has been explicitly requested we default to dark
const theme = themeName?.includes("light") ? "light" : "dark"; const theme = requestedTheme?.includes("light") ? "light" : "dark";
const themeHighContrast = themeName?.includes("high-contrast") ? "-hc" : ""; const themeHighContrast = requestedTheme?.includes("high-contrast")
? "-hc"
: "";
const themeString = "cpd-theme-" + theme + themeHighContrast; const themeString = "cpd-theme-" + theme + themeHighContrast;
if (themeString !== previousTheme.current) { if (themeString !== previousTheme.current) {
document.body.classList.remove( document.body.classList.remove(
@@ -28,5 +57,5 @@ export const useTheme = (): void => {
previousTheme.current = themeString; previousTheme.current = themeString;
} }
document.body.classList.remove("no-theme"); document.body.classList.remove("no-theme");
}, [previousTheme, themeName]); }, [previousTheme, requestedTheme]);
}; };

View File

@@ -8,7 +8,11 @@ Please see LICENSE in the repository root for full details.
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { createRoomWidgetClient } from "matrix-js-sdk/src/matrix"; 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 { MatrixClient } from "matrix-js-sdk/src/client";
import type { IWidgetApiRequest } from "matrix-widget-api"; import type { IWidgetApiRequest } from "matrix-widget-api";
@@ -70,6 +74,7 @@ export const widget = ((): WidgetHelpers | null => {
// intend for the app to handle // intend for the app to handle
const lazyActions = new LazyEventEmitter(); const lazyActions = new LazyEventEmitter();
[ [
WidgetApiToWidgetAction.ThemeChange,
ElementWidgetActions.JoinCall, ElementWidgetActions.JoinCall,
ElementWidgetActions.HangupCall, ElementWidgetActions.HangupCall,
ElementWidgetActions.TileLayout, ElementWidgetActions.TileLayout,

View File

@@ -6457,9 +6457,9 @@ matrix-js-sdk@matrix-org/matrix-js-sdk#develop:
uuid "11" uuid "11"
matrix-widget-api@^1.10.0: matrix-widget-api@^1.10.0:
version "1.10.0" version "1.11.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.11.0.tgz#2f548b11a7c0df789d5d4fdb5cc9ef7af8aef3da"
integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw== integrity sha512-ED/9hrJqDWVLeED0g1uJnYRhINh3ZTquwurdM+Hc8wLVJIQ8G/r7A7z74NC+8bBIHQ1Jo7i1Uq5CoJp/TzFYrA==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"