diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 458dcaeac49..3966b04c7d0 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ import { Icon as PinUprightIcon } from "../../../../res/img/element-icons/room/p import { Icon as EllipsisIcon } from "../../../../res/img/element-icons/room/ellipsis.svg"; import { Icon as MembersIcon } from "../../../../res/img/element-icons/room/members.svg"; import { Icon as FavoriteIcon } from "../../../../res/img/element-icons/roomlist/favorite.svg"; -import SettingsStore from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; import DevtoolsDialog from "../dialogs/DevtoolsDialog"; import { SdkContextClass } from "../../../contexts/SDKContext"; @@ -46,6 +45,9 @@ const QuickSettingsButton: React.FC<{ const { [MetaSpace.Favourites]: favouritesEnabled, [MetaSpace.People]: peopleEnabled } = useSettingValue>("Spaces.enabledMetaSpaces"); + const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); + const developerModeEnabled = useSettingValue("developerMode"); + let contextMenu: JSX.Element | undefined; if (menuDisplayed && handle.current) { contextMenu = ( @@ -68,14 +70,14 @@ const QuickSettingsButton: React.FC<{ {_t("All settings")} - {SettingsStore.getValue("developerMode") && SdkContextClass.instance.roomViewStore.getRoomId() && ( + {currentRoomId && developerModeEnabled && ( { closeMenu(); Modal.createDialog( DevtoolsDialog, { - roomId: SdkContextClass.instance.roomViewStore.getRoomId()!, + roomId: currentRoomId, }, "mx_DevtoolsDialog_wrapper", ); diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index a79cbf0c0b4..236161e3f63 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 1d31e9d141f..671a15b2c3a 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -95,9 +95,9 @@ export const SpaceButton = forwardRef( } let notifBadge; - if (notificationState) { + if (space && notificationState) { let ariaLabel = _t("Jump to first unread room."); - if (space?.getMyMembership() === "invite") { + if (space.getMyMembership() === "invite") { ariaLabel = _t("Jump to first invite."); } @@ -133,8 +133,9 @@ export const SpaceButton = forwardRef( } const viewSpaceHome = (): void => - defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: space.roomId }); - const activateSpace = (): void => SpaceStore.instance.setActiveSpace(spaceKey ?? space.roomId); + // space is set here because of the assignment condition of onClick + defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: space!.roomId }); + const activateSpace = (): void => SpaceStore.instance.setActiveSpace(spaceKey ?? space?.roomId ?? ""); const onClick = props.onClick ?? (selected && space ? viewSpaceHome : activateSpace); return ( diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 52bcac185ad..4b7c7ca5b68 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -46,6 +46,7 @@ import { FontWatcher } from "./watchers/FontWatcher"; import RustCryptoSdkController from "./controllers/RustCryptoSdkController"; import ServerSupportUnstableFeatureController from "./controllers/ServerSupportUnstableFeatureController"; import { WatchManager } from "./WatchManager"; +import { CustomTheme } from "../theme"; export const defaultWatchManager = new WatchManager(); @@ -111,7 +112,15 @@ export const labGroupNames: Record = { [LabGroup.Developer]: _td("Developer"), }; -export type SettingValueType = boolean | number | string | number[] | string[] | Record | null; +export type SettingValueType = + | boolean + | number + | string + | number[] + | string[] + | Record + | Record[] + | null; export interface IBaseSetting { isFeature?: false | undefined; @@ -653,7 +662,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { }, "custom_themes": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: [], + default: [] as CustomTheme[], }, "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, diff --git a/src/theme.ts b/src/theme.ts index afd31057d8f..9ce8ef405a0 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -16,6 +16,7 @@ limitations under the License. */ import { compare } from "matrix-js-sdk/src/utils"; +import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "./languageHandler"; import SettingsStore from "./settings/SettingsStore"; @@ -34,7 +35,8 @@ interface IFontFaces extends Omit = {}; - for (const { name } of customThemes) { - customThemeNames[`custom-${name}`] = name; + + try { + for (const { name } of customThemes) { + customThemeNames[`custom-${name}`] = name; + } + } catch (err) { + logger.warn("Error loading custom themes", { + err, + customThemes, + }); } + return Object.assign({}, customThemeNames, BUILTIN_THEMES); } @@ -166,7 +177,7 @@ function generateCustomFontFaceCSS(faces: IFontFaces[]): string { .join("\n"); } -function setCustomThemeVars(customTheme: ICustomTheme): void { +function setCustomThemeVars(customTheme: CustomTheme): void { const { style } = document.body; function setCSSColorVariable(name: string, hexColor: string, doPct = true): void { @@ -209,7 +220,7 @@ function setCustomThemeVars(customTheme: ICustomTheme): void { } } -export function getCustomTheme(themeName: string): ICustomTheme { +export function getCustomTheme(themeName: string): CustomTheme { // set css variables const customThemes = SettingsStore.getValue("custom_themes"); if (!customThemes) { diff --git a/test/components/views/spaces/QuickSettingsButton-test.tsx b/test/components/views/spaces/QuickSettingsButton-test.tsx new file mode 100644 index 00000000000..e518a3a10cc --- /dev/null +++ b/test/components/views/spaces/QuickSettingsButton-test.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; + +import QuickSettingsButton from "../../../../src/components/views/spaces/QuickSettingsButton"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext"; + +describe("QuickSettingsButton", () => { + const roomId = "!room:example.com"; + + const renderQuickSettingsButton = () => { + render(); + }; + + const getQuickSettingsButton = () => { + return screen.getByRole("button", { name: "Quick settings" }); + }; + + const openQuickSettings = async () => { + await userEvent.click(getQuickSettingsButton()); + await screen.findByText("Quick settings"); + }; + + it("should render the quick settings button", () => { + renderQuickSettingsButton(); + expect(getQuickSettingsButton()).toBeInTheDocument(); + }); + + describe("when the quick settings are open", () => { + beforeEach(async () => { + renderQuickSettingsButton(); + await openQuickSettings(); + }); + + it("should not render the »Developer tools« button", () => { + renderQuickSettingsButton(); + expect(screen.queryByText("Developer tools")).not.toBeInTheDocument(); + }); + }); + + describe("when developer mode is enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode"); + renderQuickSettingsButton(); + }); + + afterEach(() => { + mocked(SettingsStore.getValue).mockRestore(); + }); + + describe("and no room is viewed", () => { + it("should not render the »Developer tools« button", () => { + renderQuickSettingsButton(); + expect(screen.queryByText("Developer tools")).not.toBeInTheDocument(); + }); + }); + + describe("and a room is viewed", () => { + beforeEach(() => { + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(roomId); + }); + + afterEach(() => { + mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockRestore(); + }); + + describe("and the quick settings are open", () => { + beforeEach(async () => { + await openQuickSettings(); + }); + + it("should render the »Developer tools« button", () => { + expect(screen.getByRole("button", { name: "Developer tools" })).toBeInTheDocument(); + }); + }); + }); + }); +}); diff --git a/test/theme-test.ts b/test/theme-test.ts index b7d5f5060d2..3789028f81b 100644 --- a/test/theme-test.ts +++ b/test/theme-test.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { setTheme } from "../src/theme"; +import SettingsStore from "../src/settings/SettingsStore"; +import { enumerateThemes, setTheme } from "../src/theme"; describe("theme", () => { describe("setTheme", () => { @@ -124,4 +125,25 @@ describe("theme", () => { }); }); }); + + describe("enumerateThemes", () => { + it("should return a list of themes", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue([{ name: "pink" }]); + expect(enumerateThemes()).toEqual({ + "light": "Light", + "light-high-contrast": "Light high contrast", + "dark": "Dark", + "custom-pink": "pink", + }); + }); + + it("should be robust to malformed custom_themes values", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue([23]); + expect(enumerateThemes()).toEqual({ + "light": "Light", + "light-high-contrast": "Light high contrast", + "dark": "Dark", + }); + }); + }); });