From aca8b19d3452d7ba13ebbd946fe44ec905ced59a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 3 Jun 2024 18:06:21 +0200 Subject: [PATCH 01/41] Add hook to get the theme --- .../views/spaces/QuickThemeSwitcher.tsx | 8 +-- src/hooks/useSettings.ts | 34 ++++++++++++ src/hooks/useTheme.ts | 53 +++++++++++++++++++ src/settings/SettingsStore.ts | 6 +-- 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useTheme.ts diff --git a/src/components/views/spaces/QuickThemeSwitcher.tsx b/src/components/views/spaces/QuickThemeSwitcher.tsx index 66f3d2c040e..1a6ee738bac 100644 --- a/src/components/views/spaces/QuickThemeSwitcher.tsx +++ b/src/components/views/spaces/QuickThemeSwitcher.tsx @@ -20,13 +20,13 @@ import { _t } from "../../../languageHandler"; import { Action } from "../../../dispatcher/actions"; import { findNonHighContrastTheme, getOrderedThemes } from "../../../theme"; import Dropdown from "../elements/Dropdown"; -import ThemeChoicePanel from "../settings/ThemeChoicePanel"; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; import dis from "../../../dispatcher/dispatcher"; import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; import PosthogTrackers from "../../../PosthogTrackers"; import { NonEmptyArray } from "../../../@types/common"; +import { useTheme } from "../../../hooks/useTheme"; type Props = { requestClose: () => void; @@ -37,10 +37,10 @@ const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID"; const QuickThemeSwitcher: React.FC = ({ requestClose }) => { const orderedThemes = useMemo(getOrderedThemes, []); - const themeState = ThemeChoicePanel.calculateThemeState(); + const themeState = useTheme(); const nonHighContrast = findNonHighContrastTheme(themeState.theme); const theme = nonHighContrast ? nonHighContrast : themeState.theme; - const { useSystemTheme } = themeState; + const { systemThemeActivated } = themeState; const themeOptions = [ { @@ -50,7 +50,7 @@ const QuickThemeSwitcher: React.FC = ({ requestClose }) => { ...orderedThemes, ]; - const selectedTheme = useSystemTheme ? MATCH_SYSTEM_THEME_ID : theme; + const selectedTheme = systemThemeActivated ? MATCH_SYSTEM_THEME_ID : theme; const onOptionChange = async (newTheme: string): Promise => { PosthogTrackers.trackInteraction("WebQuickSettingsThemeDropdown"); diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index cabd4cbee9a..23ce891b1e3 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -17,6 +17,7 @@ limitations under the License. import { useEffect, useState } from "react"; import SettingsStore from "../settings/SettingsStore"; +import { SettingLevel } from "../settings/SettingLevel"; // Hook to fetch the value of a setting and dynamically update when it changes export const useSettingValue = (settingName: string, roomId: string | null = null, excludeDefault = false): T => { @@ -35,6 +36,39 @@ export const useSettingValue = (settingName: string, roomId: string | null = return value; }; +/** + * Hook to fetch the value of a setting at a specific level and dynamically update when it changes + * @see SettingsStore.getValueAt + * @param level + * @param settingName + * @param roomId + * @param explicit + * @param excludeDefault + */ +export const useSettingValueAt = ( + level: SettingLevel, + settingName: string, + roomId: string | null = null, + explicit = false, + excludeDefault = false, +): T => { + const [value, setValue] = useState( + SettingsStore.getValueAt(level, settingName, roomId, explicit, excludeDefault), + ); + + useEffect(() => { + const ref = SettingsStore.watchSetting(settingName, roomId, () => { + setValue(SettingsStore.getValueAt(level, settingName, roomId, explicit, excludeDefault)); + }); + // clean-up + return () => { + SettingsStore.unwatchSetting(ref); + }; + }, [level, settingName, roomId, explicit, excludeDefault]); + + return value; +}; + // Hook to fetch whether a feature is enabled and dynamically update when that changes export const useFeatureEnabled = (featureName: string, roomId: string | null = null): boolean => { const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 00000000000..fcf77d4c67c --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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 { SettingLevel } from "../settings/SettingLevel"; +import { useSettingValue, useSettingValueAt } from "./useSettings"; + +/** + * Hook to fetch the current theme and whether system theme matching is enabled. + */ +export function useTheme(): { theme: string; systemThemeActivated: boolean } { + // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we + // show the right values for things. + + const themeChoice = useSettingValue("theme"); + const systemThemeExplicit = useSettingValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + const themeExplicit = useSettingValueAt(SettingLevel.DEVICE, "theme", null, false, true); + const systemThemeActivated = useSettingValue("use_system_theme"); + + // If the user has enabled system theme matching, use that. + if (systemThemeExplicit) { + return { + theme: themeChoice, + systemThemeActivated: true, + }; + } + + // If the user has set a theme explicitly, use that (no system theme matching) + if (themeExplicit) { + return { + theme: themeChoice, + systemThemeActivated: false, + }; + } + + // Otherwise assume the defaults for the settings + return { + theme: themeChoice, + systemThemeActivated, + }; +} diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 7b1b6493d0e..acf81d56e79 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -349,7 +349,7 @@ export default class SettingsStore { const setting = SETTINGS[settingName]; const levelOrder = getLevelOrder(setting); - return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault); + return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault); } /** @@ -363,13 +363,13 @@ export default class SettingsStore { * @param {boolean} excludeDefault True to disable using the default value. * @return {*} The value, or null if not found. */ - public static getValueAt( + public static getValueAt( level: SettingLevel, settingName: string, roomId: string | null = null, explicit = false, excludeDefault = false, - ): any { + ): T { // Verify that the setting is actually a setting const setting = SETTINGS[settingName]; if (!setting) { From 2d303c473f99e3a6dbae0f2f75db682dc86b814c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 4 Jun 2024 13:36:45 +0200 Subject: [PATCH 02/41] Adapt subsection settings to new ui --- .../settings/shared/_SettingsSubsection.pcss | 10 +++++++ .../settings/shared/SettingsSubsection.tsx | 17 ++++++++++-- .../shared/SettingsSubsectionHeading.tsx | 26 +++++++++++++------ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss index 44d0a344264..f0e31285cbc 100644 --- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -17,6 +17,12 @@ limitations under the License. .mx_SettingsSubsection { width: 100%; box-sizing: border-box; + + &.mx_SettingsSubsection_newUi { + display: flex; + flex-direction: column; + gap: var(--cpd-space-8x); + } } .mx_SettingsSubsection_description { @@ -54,4 +60,8 @@ limitations under the License. &.mx_SettingsSubsection_noHeading { margin-top: 0; } + &.mx_SettingsSubsection_content_newUi { + gap: var(--cpd-space-6x); + margin-top: 0; + } } diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index 035306f5f34..a45d51987e7 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -16,6 +16,7 @@ limitations under the License. import classNames from "classnames"; import React, { HTMLAttributes } from "react"; +import { Separator } from "@vector-im/compound-web"; import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading"; @@ -25,6 +26,10 @@ export interface SettingsSubsectionProps extends HTMLAttributes children?: React.ReactNode; // when true content will be justify-items: stretch, which will make items within the section stretch to full width. stretchContent?: boolean; + /* + * When true, the new UI style will be applied to the subsection. + */ + newUi?: boolean; } export const SettingsSubsectionText: React.FC> = ({ children, ...rest }) => ( @@ -38,10 +43,16 @@ export const SettingsSubsection: React.FC = ({ description, children, stretchContent, + newUi, ...rest }) => ( -
- {typeof heading === "string" ? : <>{heading}} +
+ {typeof heading === "string" ? : <>{heading}} {!!description && (
{description} @@ -52,11 +63,13 @@ export const SettingsSubsection: React.FC = ({ className={classNames("mx_SettingsSubsection_content", { mx_SettingsSubsection_contentStretch: !!stretchContent, mx_SettingsSubsection_noHeading: !heading && !description, + mx_SettingsSubsection_content_newUi: newUi, })} > {children}
)} + {newUi && }
); diff --git a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx index 262b9f4d371..a8e862de1a5 100644 --- a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx +++ b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx @@ -20,14 +20,24 @@ import Heading from "../../typography/Heading"; export interface SettingsSubsectionHeadingProps extends HTMLAttributes { heading: string; + newUi?: boolean; children?: React.ReactNode; } -export const SettingsSubsectionHeading: React.FC = ({ heading, children, ...rest }) => ( -
- - {heading} - - {children} -
-); +export const SettingsSubsectionHeading: React.FC = ({ + heading, + newUi, + children, + ...rest +}) => { + const size = newUi ? "3" : "4"; + + return ( +
+ + {heading} + + {children} +
+ ); +}; From 908c7bf89314c8754c956e9de15bdf14c8cb3a76 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 4 Jun 2024 16:56:00 +0200 Subject: [PATCH 03/41] WIP new theme subsection --- res/css/views/settings/_ThemeChoicePanel.pcss | 35 +++++ .../views/settings/ThemeChoicePanel2.tsx | 138 ++++++++++++++++++ .../tabs/user/AppearanceUserSettingsTab.tsx | 2 + 3 files changed, 175 insertions(+) create mode 100644 src/components/views/settings/ThemeChoicePanel2.tsx diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index 8616668224d..a103573511e 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -59,3 +59,38 @@ limitations under the License. } } } + +.mx_ThemeChoicePanel_ThemeSelectors { + display: flex; + /* Form style */ + flex-direction: row !important; + gap: var(--cpd-space-4x) !important; + + .mx_ThemeChoicePanel_themeSelector { + border: 1px solid; + border-radius: var(--cpd-space-1-5x); + padding: var(--cpd-space-3x) var(--cpd-space-5x); + gap: var(--cpd-space-2x); + + /* TODO use correct token color for border and background + * https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=119-21045&t=cFg3nY6FwxW4naAe-0 + * The compound token is not available in the design system yet + * We need to use directly --light or --dark directly because we they are independent of the current theme + */ + &.mx_ThemeChoicePanel_themeSelector_dark { + background-color: #101317; + border-color: var(--cpd-color-gray-600); + color: #ebeef2; + } + + &.mx_ThemeChoicePanel_themeSelector_light { + background-color: #ffffff; + border-color: var(--cpd-color-gray-800); + color: #1b1d22; + } + + .mx_ThemeChoicePanel_themeSelector_Label { + font: var(--cpd-font-body-md-semibold); + } + } +} diff --git a/src/components/views/settings/ThemeChoicePanel2.tsx b/src/components/views/settings/ThemeChoicePanel2.tsx new file mode 100644 index 00000000000..69bbe2eb1a9 --- /dev/null +++ b/src/components/views/settings/ThemeChoicePanel2.tsx @@ -0,0 +1,138 @@ +/* + * Copyright 2024 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, { Dispatch, JSX, useRef, useState } from "react"; +import { InlineField, ToggleControl, Label, Root, RadioControl } from "@vector-im/compound-web"; +import classNames from "classnames"; + +import { _t } from "../../../languageHandler"; +import SettingsSubsection from "./shared/SettingsSubsection"; +import ThemeWatcher from "../../../settings/watchers/ThemeWatcher"; +import SettingsStore from "../../../settings/SettingsStore"; +import { SettingLevel } from "../../../settings/SettingLevel"; +import dis from "../../../dispatcher/dispatcher"; +import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; +import { Action } from "../../../dispatcher/actions"; +import { useTheme } from "../../../hooks/useTheme"; +import { getOrderedThemes } from "../../../theme"; + +export function ThemeChoicePanel(): JSX.Element { + const [themeState, setThemeState] = useThemeState(); + const themeWatcher = useRef(new ThemeWatcher()); + + return ( + + {themeWatcher.current.isSystemThemeSupported() && ( + + setThemeState((_themeState) => ({ ..._themeState, systemThemeActivated })) + } + /> + )} + + + ); +} + +/** + * Interface for the theme state + */ +interface ThemeState { + /* The theme */ + theme: string; + /* Whether the system theme is activated */ + systemThemeActivated: boolean; +} + +/** + * Hook to fetch the value of the theme and dynamically update when it changes + */ +function useThemeState(): [ThemeState, Dispatch>] { + const theme = useTheme(); + const [themeState, setThemeState] = useState(theme); + + return [themeState, setThemeState]; +} + +/** + * Component to toggle the system theme + */ +interface SystemThemeProps { + /* Whether the system theme is activated */ + systemThemeActivated: boolean; + /* Callback when the system theme is toggled */ + onChange: (systemThemeActivated: boolean) => void; +} + +/** + * Component to toggle the system theme + */ +function SystemTheme({ systemThemeActivated, onChange }: SystemThemeProps): JSX.Element { + return ( + { + // Needed to be able to have access to the `checked` attribute + if (evt.target instanceof HTMLInputElement) { + const { checked } = evt.target; + onChange(checked); + await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); + dis.dispatch({ action: Action.RecheckTheme }); + } + }} + > + } + > + + + + ); +} + +/** + * Component to select the theme + */ +interface ThemeSelectorProps { + /* The current theme */ + theme: string; +} + +/** + * Component to select the theme + */ +function ThemeSelectors({ theme }: ThemeSelectorProps): JSX.Element { + const orderedThemes = useRef(getOrderedThemes()); + + return ( + + {orderedThemes.current.map((_theme) => ( + } + > + + + ))} + + ); +} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 99f5a51c3b2..510ba488d03 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -29,6 +29,7 @@ import { Layout } from "../../../../../settings/enums/Layout"; import LayoutSwitcher from "../../LayoutSwitcher"; import FontScalingPanel from "../../FontScalingPanel"; import ThemeChoicePanel from "../../ThemeChoicePanel"; +import { ThemeChoicePanel as ThemeChoicePanel2 } from "../../ThemeChoicePanel2"; import ImageSizePanel from "../../ImageSizePanel"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; @@ -156,6 +157,7 @@ export default class AppearanceUserSettingsTab extends React.Component + Date: Wed, 5 Jun 2024 11:35:56 +0200 Subject: [PATCH 04/41] Add theme selection --- res/css/views/settings/_ThemeChoicePanel.pcss | 23 +-- .../views/settings/ThemeChoicePanel2.tsx | 133 +++++++++++++----- src/i18n/strings/en_EN.json | 1 + 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index a103573511e..0435de0f1c7 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -62,34 +62,23 @@ limitations under the License. .mx_ThemeChoicePanel_ThemeSelectors { display: flex; - /* Form style */ + /* Override form default style */ flex-direction: row !important; gap: var(--cpd-space-4x) !important; .mx_ThemeChoicePanel_themeSelector { - border: 1px solid; + border: 1px solid var(--cpd-color-border-interactive-secondary); border-radius: var(--cpd-space-1-5x); padding: var(--cpd-space-3x) var(--cpd-space-5x); gap: var(--cpd-space-2x); + background-color: var(--cpd-color-bg-canvas-default); - /* TODO use correct token color for border and background - * https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=119-21045&t=cFg3nY6FwxW4naAe-0 - * The compound token is not available in the design system yet - * We need to use directly --light or --dark directly because we they are independent of the current theme - */ - &.mx_ThemeChoicePanel_themeSelector_dark { - background-color: #101317; - border-color: var(--cpd-color-gray-600); - color: #ebeef2; - } - - &.mx_ThemeChoicePanel_themeSelector_light { - background-color: #ffffff; - border-color: var(--cpd-color-gray-800); - color: #1b1d22; + &.mx_ThemeChoicePanel_themeSelector_enabled { + border-color: var(--cpd-color-border-interactive-primary); } .mx_ThemeChoicePanel_themeSelector_Label { + color: var(--cpd-color-text-primary); font: var(--cpd-font-body-md-semibold); } } diff --git a/src/components/views/settings/ThemeChoicePanel2.tsx b/src/components/views/settings/ThemeChoicePanel2.tsx index 69bbe2eb1a9..4d4b816761f 100644 --- a/src/components/views/settings/ThemeChoicePanel2.tsx +++ b/src/components/views/settings/ThemeChoicePanel2.tsx @@ -27,26 +27,7 @@ import dis from "../../../dispatcher/dispatcher"; import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; import { Action } from "../../../dispatcher/actions"; import { useTheme } from "../../../hooks/useTheme"; -import { getOrderedThemes } from "../../../theme"; - -export function ThemeChoicePanel(): JSX.Element { - const [themeState, setThemeState] = useThemeState(); - const themeWatcher = useRef(new ThemeWatcher()); - - return ( - - {themeWatcher.current.isSystemThemeSupported() && ( - - setThemeState((_themeState) => ({ ..._themeState, systemThemeActivated })) - } - /> - )} - - - ); -} +import { findHighContrastTheme, getOrderedThemes } from "../../../theme"; /** * Interface for the theme state @@ -54,6 +35,8 @@ export function ThemeChoicePanel(): JSX.Element { interface ThemeState { /* The theme */ theme: string; + /* The apparent selected theme */ + apparentSelectedTheme?: string; /* Whether the system theme is activated */ systemThemeActivated: boolean; } @@ -63,11 +46,33 @@ interface ThemeState { */ function useThemeState(): [ThemeState, Dispatch>] { const theme = useTheme(); - const [themeState, setThemeState] = useState(theme); + const [themeState, setThemeState] = useState(theme); return [themeState, setThemeState]; } +export function ThemeChoicePanel(): JSX.Element { + const [themeState, setThemeState] = useThemeState(); + const themeWatcher = useRef(new ThemeWatcher()); + + return ( + + {themeWatcher.current.isSystemThemeSupported() && ( + + setThemeState((_themeState) => ({ ..._themeState, systemThemeActivated })) + } + /> + )} + setThemeState((_themeState) => ({ ..._themeState, theme }))} + /> + + ); +} + /** * Component to toggle the system theme */ @@ -85,13 +90,10 @@ function SystemTheme({ systemThemeActivated, onChange }: SystemThemeProps): JSX. return ( { - // Needed to be able to have access to the `checked` attribute - if (evt.target instanceof HTMLInputElement) { - const { checked } = evt.target; - onChange(checked); - await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); - dis.dispatch({ action: Action.RecheckTheme }); - } + const checked = new FormData(evt.currentTarget).get("systemTheme") === "on"; + onChange(checked); + await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); + dis.dispatch({ action: Action.RecheckTheme }); }} > void; } /** * Component to select the theme */ -function ThemeSelectors({ theme }: ThemeSelectorProps): JSX.Element { - const orderedThemes = useRef(getOrderedThemes()); +function ThemeSelectors({ theme, onChange }: ThemeSelectorProps): JSX.Element { + const orderedThemes = useRef(getThemes()); return ( - + { + // We don't have any file in the form, we can cast it as string safely + const newTheme = new FormData(evt.currentTarget).get("themeSelector") as string | null; + + // Do nothing if the same theme is selected + if (!newTheme || theme === newTheme) return; + + // doing getValue in the .catch will still return the value we failed to set, + // so remember what the value was before we tried to set it so we can revert + const oldTheme = SettingsStore.getValue("theme"); + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => { + dis.dispatch({ action: Action.RecheckTheme }); + onChange(oldTheme); + }); + + onChange(newTheme); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now + // XXX: The local echoed value appears to be unreliable, in particular + // when settings custom themes(!) so adding forceTheme to override + // the value from settings. + dis.dispatch({ action: Action.RecheckTheme, forceTheme: newTheme }); + }} + > {orderedThemes.current.map((_theme) => ( } + control={ + + } > @@ -136,3 +177,23 @@ function ThemeSelectors({ theme }: ThemeSelectorProps): JSX.Element { ); } + +/** + * Get the themes + * @returns The themes + */ +function getThemes(): ReturnType { + const themes = getOrderedThemes(); + + // Currently only light theme has a high contrast theme + const lightHighContrastId = findHighContrastTheme("light"); + if (lightHighContrastId) { + const lightHighContrast = { + name: _t("settings|appearance|high_contrast"), + id: lightHighContrastId, + }; + themes.push(lightHighContrast); + } + + return themes; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7db9435c00e..1801c592fea 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2422,6 +2422,7 @@ "dialog_title": "Settings: Appearance", "font_size": "Font size", "font_size_default": "%(fontSize)s (default)", + "high_contrast": "High contrast", "image_size_default": "Default", "image_size_large": "Large", "layout_bubbles": "Message bubbles", From 5407a562a3c398fd11b4ba0c91fda0864f1287c6 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 5 Jun 2024 11:48:24 +0200 Subject: [PATCH 05/41] Fix test types --- .../settings/tabs/user/LabsUserSettingsTab-test.tsx | 6 +++--- .../tabs/user/PreferencesUserSettingsTab-test.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx index 18622d87a1b..3e178c9fe8b 100644 --- a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -76,13 +76,13 @@ describe("", () => { const copyOfGetValueAt = SettingsStore.getValueAt; beforeEach(() => { - SettingsStore.getValueAt = ( + SettingsStore.getValueAt = ( level: SettingLevel, name: string, roomId?: string, isExplicit?: boolean, - ) => { - if (level == SettingLevel.CONFIG && name === "feature_rust_crypto") return false; + ): T => { + if (level == SettingLevel.CONFIG && name === "feature_rust_crypto") return false as T; return copyOfGetValueAt(level, name, roomId, isExplicit); }; }); diff --git a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx index 292f4e27465..82abc8fed66 100644 --- a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx @@ -64,8 +64,13 @@ describe("PreferencesUserSettingsTab", () => { const mockGetValue = (val: boolean) => { const copyOfGetValueAt = SettingsStore.getValueAt; - SettingsStore.getValueAt = (level: SettingLevel, name: string, roomId?: string, isExplicit?: boolean) => { - if (name === "sendReadReceipts") return val; + SettingsStore.getValueAt = ( + level: SettingLevel, + name: string, + roomId?: string, + isExplicit?: boolean, + ): T => { + if (name === "sendReadReceipts") return val as T; return copyOfGetValueAt(level, name, roomId, isExplicit); }; }; From 9de33b0d3573f6e444469dad5f234272fb6d3b87 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 5 Jun 2024 15:01:48 +0200 Subject: [PATCH 06/41] Disabled theme selector when system theme is used --- res/css/views/settings/_ThemeChoicePanel.pcss | 4 ++++ .../views/settings/ThemeChoicePanel2.tsx | 16 +++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index 0435de0f1c7..6ff2d466eac 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -77,6 +77,10 @@ limitations under the License. border-color: var(--cpd-color-border-interactive-primary); } + &.mx_ThemeChoicePanel_themeSelector_disabled { + border-color: var(--cpd-color-border-disabled); + } + .mx_ThemeChoicePanel_themeSelector_Label { color: var(--cpd-color-text-primary); font: var(--cpd-font-body-md-semibold); diff --git a/src/components/views/settings/ThemeChoicePanel2.tsx b/src/components/views/settings/ThemeChoicePanel2.tsx index 4d4b816761f..04d68699023 100644 --- a/src/components/views/settings/ThemeChoicePanel2.tsx +++ b/src/components/views/settings/ThemeChoicePanel2.tsx @@ -35,8 +35,6 @@ import { findHighContrastTheme, getOrderedThemes } from "../../../theme"; interface ThemeState { /* The theme */ theme: string; - /* The apparent selected theme */ - apparentSelectedTheme?: string; /* Whether the system theme is activated */ systemThemeActivated: boolean; } @@ -67,6 +65,7 @@ export function ThemeChoicePanel(): JSX.Element { )} setThemeState((_themeState) => ({ ..._themeState, theme }))} /> @@ -112,6 +111,8 @@ function SystemTheme({ systemThemeActivated, onChange }: SystemThemeProps): JSX. interface ThemeSelectorProps { /* The current theme */ theme: string; + /* The theme can't be selected */ + disabled: boolean; /* Callback when the theme is changed */ onChange: (theme: string) => void; } @@ -119,7 +120,7 @@ interface ThemeSelectorProps { /** * Component to select the theme */ -function ThemeSelectors({ theme, onChange }: ThemeSelectorProps): JSX.Element { +function ThemeSelectors({ theme, disabled, onChange }: ThemeSelectorProps): JSX.Element { const orderedThemes = useRef(getThemes()); return ( @@ -153,7 +154,8 @@ function ThemeSelectors({ theme, onChange }: ThemeSelectorProps): JSX.Element { {orderedThemes.current.map((_theme) => ( } From 800baa4833cb997976857e6c8bfd2f0374348650 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 5 Jun 2024 15:41:54 +0200 Subject: [PATCH 07/41] Update compound to `4.4.1` --- package.json | 2 +- yarn.lock | 180 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 174 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 00c1cc2e4b3..1835633273b 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^1.2.0", - "@vector-im/compound-web": "^4.3.1", + "@vector-im/compound-web": "^4.4.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index bf2d55683eb..9de0f150647 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1474,6 +1474,11 @@ resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b" integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -2444,6 +2449,98 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@storybook/channels@8.1.5": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-8.1.5.tgz#d00d033d318cf202ece1de728e55e85f82242e74" + integrity sha512-R+puP4tWYzQUbpIp8sX6U5oI+ZUevVOaFxXGaAN3PRXjIRC38oKTVWzj/G6GdziVFzN6rDn+JsYPmiRMYo1sYg== + dependencies: + "@storybook/client-logger" "8.1.5" + "@storybook/core-events" "8.1.5" + "@storybook/global" "^5.0.0" + telejson "^7.2.0" + tiny-invariant "^1.3.1" + +"@storybook/client-logger@8.1.5": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-8.1.5.tgz#aa4a6ce4ca46fdfe12539e571f9059a479c8ae43" + integrity sha512-zd+aENXnOHsxBATppELmhw/UywLzCxQjz/8i/xkUjeTRB4Ggp0hJlOUdJUEdIJz631ydyytfvM70ktBj9gMl1w== + dependencies: + "@storybook/global" "^5.0.0" + +"@storybook/core-events@8.1.5": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-8.1.5.tgz#d921984e12b27aaaa623499a7ac0c3eea5e96264" + integrity sha512-fgwbrHoLtSX6kfmamTGJqD+KfuEgun8cc4mWKZK094ByaqbSjhnOyeYO1sfVk8qst7QTFlOfhLAUe4cz1z149A== + dependencies: + "@storybook/csf" "^0.1.7" + ts-dedent "^2.0.0" + +"@storybook/csf@^0.1.7": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.8.tgz#63a83dc493c462d84e0f333e3f3264d319bec716" + integrity sha512-Ntab9o7LjBCbFIao5l42itFiaSh/Qu+l16l/r/9qmV9LnYZkO+JQ7tzhdlwpgJfhs+B5xeejpdAtftDRyXNajw== + dependencies: + type-fest "^2.19.0" + +"@storybook/global@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" + integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== + +"@storybook/icons@^1.2.5": + version "1.2.9" + resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-1.2.9.tgz#bb4a51a79e186b62e2dd0e04928b8617ac573838" + integrity sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg== + +"@storybook/manager-api@^8.1.1": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-8.1.5.tgz#1f1a8875cbc19fad5435f670943207158dc76551" + integrity sha512-iVP7FOKDf9L7zWCb8C2XeZjWSILS3hHeNwILvd9YSX9dg9du41kJYahsAHxDCR/jp/gv0ZM/V0vuHzi+naVPkQ== + dependencies: + "@storybook/channels" "8.1.5" + "@storybook/client-logger" "8.1.5" + "@storybook/core-events" "8.1.5" + "@storybook/csf" "^0.1.7" + "@storybook/global" "^5.0.0" + "@storybook/icons" "^1.2.5" + "@storybook/router" "8.1.5" + "@storybook/theming" "8.1.5" + "@storybook/types" "8.1.5" + dequal "^2.0.2" + lodash "^4.17.21" + memoizerific "^1.11.3" + store2 "^2.14.2" + telejson "^7.2.0" + ts-dedent "^2.0.0" + +"@storybook/router@8.1.5": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-8.1.5.tgz#e1dd831136e874df833286fd76554958af6132fa" + integrity sha512-DCwvAswlbLhQu6REPV04XNRhtPvsrRqHjMHKzjlfs+qYJWY7Egkofy05qlegqjkMDve33czfnRGBm0C16IydkA== + dependencies: + "@storybook/client-logger" "8.1.5" + memoizerific "^1.11.3" + qs "^6.10.0" + +"@storybook/theming@8.1.5": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.1.5.tgz#8eb0718907ec443cfca1b73491f5e99df65930af" + integrity sha512-E4z1t49fMbVvd/t2MSL0Ecp5zbqsU/QfWBX/eorJ+m+Xc9skkwwG5qf/FnP9x4RZ9KaX8U8+862t0eafVvf4Tw== + dependencies: + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@storybook/client-logger" "8.1.5" + "@storybook/global" "^5.0.0" + memoizerific "^1.11.3" + +"@storybook/types@8.1.5": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@storybook/types/-/types-8.1.5.tgz#627cac55e8034deed4b763327ff938c84c541a05" + integrity sha512-/PfAZh1xtXN2MvAZZKpiL/nPkC3bZj8BQ7P7z5a/aQarP+y7qdXuoitYQ6oOH3rkaiYywmkWzA/y4iW70KXLKg== + dependencies: + "@storybook/channels" "8.1.5" + "@types/express" "^4.7.0" + file-system-cache "2.3.0" + "@testing-library/dom@^8.0.0": version "8.20.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6" @@ -2631,7 +2728,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@^4.17.21": +"@types/express@^4.17.21", "@types/express@^4.7.0": version "4.17.21" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== @@ -3114,10 +3211,10 @@ dependencies: svg2vectordrawable "^2.9.1" -"@vector-im/compound-web@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-4.3.1.tgz#4570adeccdfcac6919256c5e399d592510bdb15d" - integrity sha512-/Sw27GI0jCg6A7E+93SWFyF3pEwLyLzExB3lIVPTY0mMTx50+rZloRuhuqftUlIscWSlmAUex8Lo4WK8WKPFPA== +"@vector-im/compound-web@^4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-4.4.1.tgz#378c6874888becd4b6dd3541904f63300b9ba09a" + integrity sha512-KLYSU8GxR8EBuz+gKSoLLs4+s5xV4stUDbqJu5GG52OmO3YQlvmz/e5/uHYvzfbqBBU5dMmZhz5bdJJ38qxHPQ== dependencies: "@floating-ui/react" "^0.26.9" "@floating-ui/react-dom" "^2.0.8" @@ -3127,6 +3224,7 @@ "@radix-ui/react-separator" "^1.0.3" "@radix-ui/react-slot" "^1.0.2" "@radix-ui/react-tooltip" "^1.0.6" + "@storybook/manager-api" "^8.1.1" classnames "^2.3.2" graphemer "^1.4.0" vaul "^0.7.0" @@ -4324,7 +4422,7 @@ depd@2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -dequal@^2.0.3: +dequal@^2.0.2, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -5351,6 +5449,14 @@ file-saver@^2.0.5: resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== +file-system-cache@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-2.3.0.tgz#201feaf4c8cd97b9d0d608e96861bb6005f46fe6" + integrity sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ== + dependencies: + fs-extra "11.1.1" + ramda "0.29.0" + filesize@10.1.2: version "10.1.2" resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.2.tgz#33bb71c5c134102499f1bc36e6f2863137f6cb0c" @@ -5480,6 +5586,15 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs-extra@11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^11.0.0: version "11.2.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" @@ -7101,6 +7216,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +map-or-similar@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" + integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg== + maplibre-gl@^2.0.0: version "2.4.0" resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-2.4.0.tgz#2b53dbf526626bf4ee92ad4f33f13ef09e5af182" @@ -7223,6 +7343,13 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== +memoizerific@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" + integrity sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog== + dependencies: + map-or-similar "^1.5.0" + meow@^13.2.0: version "13.2.0" resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" @@ -7984,6 +8111,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.10.0: + version "6.12.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" + integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== + dependencies: + side-channel "^1.0.6" + querystring@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" @@ -8009,6 +8143,11 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== +ramda@0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.29.0.tgz#fbbb67a740a754c8a4cbb41e2a6e0eb8507f55fb" + integrity sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA== + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -8641,7 +8780,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.4: +side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== @@ -8776,6 +8915,11 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +store2@^2.14.2: + version "2.14.3" + resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.3.tgz#24077d7ba110711864e4f691d2af941ec533deb5" + integrity sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -9119,6 +9263,13 @@ tar-js@^0.3.0: resolved "https://registry.yarnpkg.com/tar-js/-/tar-js-0.3.0.tgz#6949aabfb0ba18bb1562ae51a439fd0f30183a17" integrity sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA== +telejson@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/telejson/-/telejson-7.2.0.tgz#3994f6c9a8f8d7f2dba9be2c7c5bbb447e876f32" + integrity sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ== + dependencies: + memoizerific "^1.11.3" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -9138,6 +9289,11 @@ tiny-invariant@^1.0.6: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== +tiny-invariant@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tinyqueue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" @@ -9206,6 +9362,11 @@ ts-api-utils@^1.3.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== +ts-dedent@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" + integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== + ts-node@^10.9.1: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" @@ -9272,6 +9433,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" From 130b21343f418b618fc912a9f1c41dfc3db9c36f Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 18 Jun 2024 15:56:56 +0200 Subject: [PATCH 08/41] Add custom theme support --- res/css/_common.pcss | 14 +- res/css/views/settings/_ThemeChoicePanel.pcss | 39 +++ .../views/settings/ThemeChoicePanel2.tsx | 223 +++++++++++++++--- src/i18n/strings/en_EN.json | 6 +- src/theme.ts | 2 +- 5 files changed, 239 insertions(+), 45 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index d120194491b..9823227627c 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -603,7 +603,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ), + ):not(.mx_ThemeChoicePanel_CustomTheme_container button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -623,14 +623,14 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme_container button):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme_container button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -642,7 +642,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ), + ):not(.mx_ThemeChoicePanel_CustomTheme_container button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -653,7 +653,9 @@ legend { .mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]), .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons - button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button), + button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( + .mx_ThemeChoicePanel_CustomTheme_container button + ), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -669,7 +671,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme_container button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index 6ff2d466eac..250c9986ea0 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -62,6 +62,7 @@ limitations under the License. .mx_ThemeChoicePanel_ThemeSelectors { display: flex; + flex-wrap: wrap; /* Override form default style */ flex-direction: row !important; gap: var(--cpd-space-4x) !important; @@ -87,3 +88,41 @@ limitations under the License. } } } + +.mx_ThemeChoicePanel_CustomTheme_header { + margin-block: 0; + margin-top: var(--cpd-space-2x); + border-bottom: 1px solid var(--cpd-color-alpha-gray-400); + padding-bottom: var(--cpd-space-2x); + width: 100%; +} + +.mx_ThemeChoicePanel_CustomTheme_container { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + + .mx_ThemeChoicePanel_CustomTheme_EditInPlace input:focus { + // When the input is focused, the border is growing + // We need to move it a bit to avoid the left border to be under the left panel + margin-left: 2px; + } + + .mx_ThemeChoicePanel_CustomThemeList { + display: flex; + justify-content: space-between; + align-items: center; + + .mx_ThemeChoicePanel_CustomThemeList_name { + font: var(--cpd-font-body-sm-semibold); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .mx_ThemeChoicePanel_CustomThemeList_delete { + color: var(--cpd-color-icon-critical-primary); + } + } +} diff --git a/src/components/views/settings/ThemeChoicePanel2.tsx b/src/components/views/settings/ThemeChoicePanel2.tsx index 04d68699023..5e584a133e1 100644 --- a/src/components/views/settings/ThemeChoicePanel2.tsx +++ b/src/components/views/settings/ThemeChoicePanel2.tsx @@ -14,8 +14,18 @@ * limitations under the License. */ -import React, { Dispatch, JSX, useRef, useState } from "react"; -import { InlineField, ToggleControl, Label, Root, RadioControl } from "@vector-im/compound-web"; +import React, { ChangeEvent, Dispatch, JSX, useMemo, useRef, useState } from "react"; +import { + InlineField, + ToggleControl, + Label, + Root, + RadioControl, + Text, + EditInPlace, + IconButton, +} from "@vector-im/compound-web"; +import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; @@ -27,7 +37,9 @@ import dis from "../../../dispatcher/dispatcher"; import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; import { Action } from "../../../dispatcher/actions"; import { useTheme } from "../../../hooks/useTheme"; -import { findHighContrastTheme, getOrderedThemes } from "../../../theme"; +import { findHighContrastTheme, getOrderedThemes, CustomTheme as CustomThemeType, ITheme } from "../../../theme"; +import { useSettingValue } from "../../../hooks/useSettings"; +import { logger } from "../../../../../matrix-js-sdk/src/logger"; /** * Interface for the theme state @@ -49,9 +61,13 @@ function useThemeState(): [ThemeState, Dispatch return [themeState, setThemeState]; } +/** + * Panel to choose the theme + */ export function ThemeChoicePanel(): JSX.Element { const [themeState, setThemeState] = useThemeState(); const themeWatcher = useRef(new ThemeWatcher()); + const customThemeEnabled = useSettingValue("feature_custom_themes"); return ( @@ -68,6 +84,7 @@ export function ThemeChoicePanel(): JSX.Element { disabled={themeState.systemThemeActivated} onChange={(theme) => setThemeState((_themeState) => ({ ..._themeState, theme }))} /> + {customThemeEnabled && } ); } @@ -121,7 +138,7 @@ interface ThemeSelectorProps { * Component to select the theme */ function ThemeSelectors({ theme, disabled, onChange }: ThemeSelectorProps): JSX.Element { - const orderedThemes = useRef(getThemes()); + const themes = useThemes(); return ( ({ action: Action.RecheckTheme, forceTheme: newTheme }); }} > - {orderedThemes.current.map((_theme) => ( - - } - > - - - ))} + {themes.map((_theme) => { + return ( + + } + > + + + ); + })} ); } /** - * Get the themes - * @returns The themes + * Return all the available themes */ -function getThemes(): ReturnType { - const themes = getOrderedThemes(); +function useThemes(): Array { + const customThemes = useSettingValue("custom_themes"); + return useMemo(() => { + const themes = getOrderedThemes(); + // Put the custom theme into a map + // To easily find the theme by name when going through the themes list + const customThemeMap = customThemes?.reduce( + (map, theme) => map.set(theme.name, theme), + new Map(), + ); + + // Separate the built-in themes from the custom themes + // To insert the high contrast theme between them + const builtInThemes = themes.filter((theme) => !customThemeMap?.has(theme.name)); + const otherThemes = themes.filter((theme) => customThemeMap?.has(theme.name)); + + const highContrastTheme = makeHighContrastTheme(); + if (highContrastTheme) builtInThemes.push(highContrastTheme); + + const allThemes = builtInThemes.concat(otherThemes); + + // Check if the themes are dark + return allThemes.map((theme) => { + const customTheme = customThemeMap?.get(theme.name); + const isDark = (customTheme ? customTheme.is_dark : theme.id.includes("dark")) || false; + return { ...theme, isDark }; + }); + }, [customThemes]); +} - // Currently only light theme has a high contrast theme +/** + * Create the light high contrast theme + */ +function makeHighContrastTheme(): ITheme | undefined { const lightHighContrastId = findHighContrastTheme("light"); if (lightHighContrastId) { - const lightHighContrast = { + return { name: _t("settings|appearance|high_contrast"), id: lightHighContrastId, }; - themes.push(lightHighContrast); } +} + +/** + * Add and manager custom themes + */ +function CustomTheme(): JSX.Element { + const [customTheme, setCustomTheme] = useState(""); + const [error, setError] = useState(); - return themes; + return ( + <> + + {_t("settings|appearance|custom_themes")} + +
+ ) => { + setError(undefined); + setCustomTheme(e.target.value); + }} + onSave={async () => { + // The field empty is empty + if (!customTheme) return; + + // Get the custom themes and do a cheap clone + // To avoid to mutate the original array in the settings + const currentThemes = + SettingsStore.getValue("custom_themes").map((t) => t) || []; + + try { + const r = await fetch(customTheme); + // XXX: need some schema for this + const themeInfo = await r.json(); + if ( + !themeInfo || + typeof themeInfo["name"] !== "string" || + typeof themeInfo["colors"] !== "object" + ) { + setError(_t("settings|appearance|custom_theme_invalid")); + return; + } + currentThemes.push(themeInfo); + } catch (e) { + logger.error(e); + setError(_t("settings|appearance|custom_theme_error_downloading")); + return; + } + + // Reset the error + setError(undefined); + setCustomTheme(""); + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); + }} + onCancel={() => { + setError(undefined); + setCustomTheme(""); + }} + /> + +
+ + ); +} + +/** + * List of the custom themes + * @constructor + */ +function CustomThemeList(): JSX.Element { + const customThemes = useSettingValue("custom_themes") || []; + + return ( + <> + {customThemes.map((theme) => { + return ( +
+ {theme.name} + { + // Get the custom themes and do a cheap clone + // To avoid to mutate the original array in the settings + const currentThemes = + SettingsStore.getValue("custom_themes").map((t) => t) || []; + + // Remove the theme from the list + const newThemes = currentThemes.filter((t) => t.name !== theme.name); + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, newThemes); + }} + > + + +
+ ); + })} + + ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 94fbc6bfcd8..a6692e06f52 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2425,11 +2425,15 @@ "custom_font_description": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "custom_font_name": "System font name", "custom_font_size": "Use custom size", + "custom_theme_add": "Add custom theme", "custom_theme_add_button": "Add theme", - "custom_theme_error_downloading": "Error downloading theme information.", + "custom_theme_downloading": "Downloading custom theme...", + "custom_theme_error_downloading": "Error downloading theme", + "custom_theme_help": "Enter the URL of a custom theme you want to apply.", "custom_theme_invalid": "Invalid theme schema.", "custom_theme_success": "Theme added!", "custom_theme_url": "Custom theme URL", + "custom_themes": "Custom themes", "dialog_title": "Settings: Appearance", "font_size": "Font size", "font_size_default": "%(fontSize)s (default)", diff --git a/src/theme.ts b/src/theme.ts index 8e2e8933340..3245d72b762 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -103,7 +103,7 @@ export function enumerateThemes(): { [key: string]: string } { return Object.assign({}, customThemeNames, BUILTIN_THEMES); } -interface ITheme { +export interface ITheme { id: string; name: string; } From 183c7b987abb7830e1bd53b695588f633b7f0ad4 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 18 Jun 2024 16:12:18 +0200 Subject: [PATCH 09/41] Remove old ThemChoicePanel --- res/css/views/settings/_ThemeChoicePanel.pcss | 46 -- .../views/settings/ThemeChoicePanel.tsx | 573 ++++++++++-------- .../views/settings/ThemeChoicePanel2.tsx | 350 ----------- .../tabs/user/AppearanceUserSettingsTab.tsx | 4 +- 4 files changed, 320 insertions(+), 653 deletions(-) delete mode 100644 src/components/views/settings/ThemeChoicePanel2.tsx diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index 250c9986ea0..867b9d4b376 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -14,52 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ThemeChoicePanel_themeSelectors { - color: $primary-content; - display: flex; - flex-direction: row; - flex-wrap: wrap; - - > .mx_StyledRadioButton { - align-items: center; - padding: $font-16px; - box-sizing: border-box; - border-radius: 10px; - width: 180px; - - background: $accent-200; - opacity: 0.4; - - flex-shrink: 1; - flex-grow: 0; - - margin-right: 15px; - margin-top: 10px; - - font-weight: var(--cpd-font-weight-semibold); - - > span { - justify-content: center; - } - } - - > .mx_StyledRadioButton_enabled { - opacity: 1; - - /* These colors need to be hardcoded because they don't change with the theme */ - &.mx_ThemeSelector_light { - background-color: #f3f8fd; - color: #2e2f32; - } - - &.mx_ThemeSelector_dark { - /* 5% lightened version of 181b21 */ - background-color: #25282e; - color: #f3f8fd; - } - } -} - .mx_ThemeChoicePanel_ThemeSelectors { display: flex; flex-wrap: wrap; diff --git a/src/components/views/settings/ThemeChoicePanel.tsx b/src/components/views/settings/ThemeChoicePanel.tsx index c7166fe8292..5e584a133e1 100644 --- a/src/components/views/settings/ThemeChoicePanel.tsx +++ b/src/components/views/settings/ThemeChoicePanel.tsx @@ -1,285 +1,350 @@ /* -Copyright 2021 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 { logger } from "matrix-js-sdk/src/logger"; + * Copyright 2024 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, { ChangeEvent, Dispatch, JSX, useMemo, useRef, useState } from "react"; +import { + InlineField, + ToggleControl, + Label, + Root, + RadioControl, + Text, + EditInPlace, + IconButton, +} from "@vector-im/compound-web"; +import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg"; +import classNames from "classnames"; import { _t } from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; -import { findHighContrastTheme, findNonHighContrastTheme, getOrderedThemes, isHighContrastTheme } from "../../../theme"; +import SettingsSubsection from "./shared/SettingsSubsection"; import ThemeWatcher from "../../../settings/watchers/ThemeWatcher"; -import AccessibleButton from "../elements/AccessibleButton"; +import SettingsStore from "../../../settings/SettingsStore"; +import { SettingLevel } from "../../../settings/SettingLevel"; import dis from "../../../dispatcher/dispatcher"; import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; import { Action } from "../../../dispatcher/actions"; -import StyledCheckbox from "../elements/StyledCheckbox"; -import Field from "../elements/Field"; -import StyledRadioGroup from "../elements/StyledRadioGroup"; -import { SettingLevel } from "../../../settings/SettingLevel"; -import PosthogTrackers from "../../../PosthogTrackers"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { useTheme } from "../../../hooks/useTheme"; +import { findHighContrastTheme, getOrderedThemes, CustomTheme as CustomThemeType, ITheme } from "../../../theme"; +import { useSettingValue } from "../../../hooks/useSettings"; +import { logger } from "../../../../../matrix-js-sdk/src/logger"; + +/** + * Interface for the theme state + */ +interface ThemeState { + /* The theme */ + theme: string; + /* Whether the system theme is activated */ + systemThemeActivated: boolean; +} -interface IProps {} +/** + * Hook to fetch the value of the theme and dynamically update when it changes + */ +function useThemeState(): [ThemeState, Dispatch>] { + const theme = useTheme(); + const [themeState, setThemeState] = useState(theme); -interface IThemeState { - theme: string; - useSystemTheme: boolean; + return [themeState, setThemeState]; } -export interface CustomThemeMessage { - isError: boolean; - text: string; +/** + * Panel to choose the theme + */ +export function ThemeChoicePanel(): JSX.Element { + const [themeState, setThemeState] = useThemeState(); + const themeWatcher = useRef(new ThemeWatcher()); + const customThemeEnabled = useSettingValue("feature_custom_themes"); + + return ( + + {themeWatcher.current.isSystemThemeSupported() && ( + + setThemeState((_themeState) => ({ ..._themeState, systemThemeActivated })) + } + /> + )} + setThemeState((_themeState) => ({ ..._themeState, theme }))} + /> + {customThemeEnabled && } + + ); } -interface IState extends IThemeState { - customThemeUrl: string; - customThemeMessage: CustomThemeMessage; +/** + * Component to toggle the system theme + */ +interface SystemThemeProps { + /* Whether the system theme is activated */ + systemThemeActivated: boolean; + /* Callback when the system theme is toggled */ + onChange: (systemThemeActivated: boolean) => void; } -export default class ThemeChoicePanel extends React.Component { - private themeTimer?: number; +/** + * Component to toggle the system theme + */ +function SystemTheme({ systemThemeActivated, onChange }: SystemThemeProps): JSX.Element { + return ( + { + const checked = new FormData(evt.currentTarget).get("systemTheme") === "on"; + onChange(checked); + await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); + dis.dispatch({ action: Action.RecheckTheme }); + }} + > + } + > + + + + ); +} - public constructor(props: IProps) { - super(props); +/** + * Component to select the theme + */ +interface ThemeSelectorProps { + /* The current theme */ + theme: string; + /* The theme can't be selected */ + disabled: boolean; + /* Callback when the theme is changed */ + onChange: (theme: string) => void; +} - this.state = { - ...ThemeChoicePanel.calculateThemeState(), - customThemeUrl: "", - customThemeMessage: { isError: false, text: "" }, - }; - } +/** + * Component to select the theme + */ +function ThemeSelectors({ theme, disabled, onChange }: ThemeSelectorProps): JSX.Element { + const themes = useThemes(); + + return ( + { + // We don't have any file in the form, we can cast it as string safely + const newTheme = new FormData(evt.currentTarget).get("themeSelector") as string | null; + + // Do nothing if the same theme is selected + if (!newTheme || theme === newTheme) return; + + // doing getValue in the .catch will still return the value we failed to set, + // so remember what the value was before we tried to set it so we can revert + const oldTheme = SettingsStore.getValue("theme"); + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => { + dis.dispatch({ action: Action.RecheckTheme }); + onChange(oldTheme); + }); - public static calculateThemeState(): IThemeState { - // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we - // show the right values for things. + onChange(newTheme); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now + // XXX: The local echoed value appears to be unreliable, in particular + // when settings custom themes(!) so adding forceTheme to override + // the value from settings. + dis.dispatch({ action: Action.RecheckTheme, forceTheme: newTheme }); + }} + > + {themes.map((_theme) => { + return ( + + } + > + + + ); + })} + + ); +} - const themeChoice: string = SettingsStore.getValue("theme"); - const systemThemeExplicit: boolean = SettingsStore.getValueAt( - SettingLevel.DEVICE, - "use_system_theme", - null, - false, - true, +/** + * Return all the available themes + */ +function useThemes(): Array { + const customThemes = useSettingValue("custom_themes"); + return useMemo(() => { + const themes = getOrderedThemes(); + // Put the custom theme into a map + // To easily find the theme by name when going through the themes list + const customThemeMap = customThemes?.reduce( + (map, theme) => map.set(theme.name, theme), + new Map(), ); - const themeExplicit: string = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); - - // If the user has enabled system theme matching, use that. - if (systemThemeExplicit) { - return { - theme: themeChoice, - useSystemTheme: true, - }; - } - // If the user has set a theme explicitly, use that (no system theme matching) - if (themeExplicit) { - return { - theme: themeChoice, - useSystemTheme: false, - }; - } + // Separate the built-in themes from the custom themes + // To insert the high contrast theme between them + const builtInThemes = themes.filter((theme) => !customThemeMap?.has(theme.name)); + const otherThemes = themes.filter((theme) => customThemeMap?.has(theme.name)); - // Otherwise assume the defaults for the settings - return { - theme: themeChoice, - useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), - }; - } + const highContrastTheme = makeHighContrastTheme(); + if (highContrastTheme) builtInThemes.push(highContrastTheme); - private onThemeChange = (newTheme: string): void => { - if (this.state.theme === newTheme) return; + const allThemes = builtInThemes.concat(otherThemes); - PosthogTrackers.trackInteraction("WebSettingsAppearanceTabThemeSelector"); - - // doing getValue in the .catch will still return the value we failed to set, - // so remember what the value was before we tried to set it so we can revert - const oldTheme: string = SettingsStore.getValue("theme"); - SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => { - dis.dispatch({ action: Action.RecheckTheme }); - this.setState({ theme: oldTheme }); - }); - this.setState({ theme: newTheme }); - // The settings watcher doesn't fire until the echo comes back from the - // server, so to make the theme change immediately we need to manually - // do the dispatch now - // XXX: The local echoed value appears to be unreliable, in particular - // when settings custom themes(!) so adding forceTheme to override - // the value from settings. - dis.dispatch({ action: Action.RecheckTheme, forceTheme: newTheme }); - }; - - private onUseSystemThemeChanged = (checked: boolean): void => { - this.setState({ useSystemTheme: checked }); - SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); - dis.dispatch({ action: Action.RecheckTheme }); - }; - - private onAddCustomTheme = async (): Promise => { - let currentThemes: string[] = SettingsStore.getValue("custom_themes"); - if (!currentThemes) currentThemes = []; - currentThemes = currentThemes.map((c) => c); // cheap clone - - if (this.themeTimer) { - clearTimeout(this.themeTimer); - } - - try { - const r = await fetch(this.state.customThemeUrl); - // XXX: need some schema for this - const themeInfo = await r.json(); - if (!themeInfo || typeof themeInfo["name"] !== "string" || typeof themeInfo["colors"] !== "object") { - this.setState({ - customThemeMessage: { text: _t("settings|appearance|custom_theme_invalid"), isError: true }, - }); - return; - } - currentThemes.push(themeInfo); - } catch (e) { - logger.error(e); - this.setState({ - customThemeMessage: { text: _t("settings|appearance|custom_theme_error_downloading"), isError: true }, - }); - return; // Don't continue on error - } - - await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); - this.setState({ - customThemeUrl: "", - customThemeMessage: { text: _t("settings|appearance|custom_theme_success"), isError: false }, + // Check if the themes are dark + return allThemes.map((theme) => { + const customTheme = customThemeMap?.get(theme.name); + const isDark = (customTheme ? customTheme.is_dark : theme.id.includes("dark")) || false; + return { ...theme, isDark }; }); + }, [customThemes]); +} - this.themeTimer = window.setTimeout(() => { - this.setState({ customThemeMessage: { text: "", isError: false } }); - }, 3000); - }; - - private onCustomThemeChange = (e: React.ChangeEvent): void => { - this.setState({ customThemeUrl: e.target.value }); - }; - - private renderHighContrastCheckbox(): React.ReactElement | undefined { - if ( - !this.state.useSystemTheme && - (findHighContrastTheme(this.state.theme) || isHighContrastTheme(this.state.theme)) - ) { - return ( -
- this.highContrastThemeChanged(e.target.checked)} - > - {_t("settings|appearance|use_high_contrast")} - -
- ); - } - } - - private highContrastThemeChanged(checked: boolean): void { - let newTheme: string | undefined; - if (checked) { - newTheme = findHighContrastTheme(this.state.theme); - } else { - newTheme = findNonHighContrastTheme(this.state.theme); - } - if (newTheme) { - this.onThemeChange(newTheme); - } +/** + * Create the light high contrast theme + */ +function makeHighContrastTheme(): ITheme | undefined { + const lightHighContrastId = findHighContrastTheme("light"); + if (lightHighContrastId) { + return { + name: _t("settings|appearance|high_contrast"), + id: lightHighContrastId, + }; } +} - public render(): React.ReactElement { - const themeWatcher = new ThemeWatcher(); - let systemThemeSection: JSX.Element | undefined; - if (themeWatcher.isSystemThemeSupported()) { - systemThemeSection = ( -
- this.onUseSystemThemeChanged(e.target.checked)} - > - {SettingsStore.getDisplayName("use_system_theme")} - -
- ); - } +/** + * Add and manager custom themes + */ +function CustomTheme(): JSX.Element { + const [customTheme, setCustomTheme] = useState(""); + const [error, setError] = useState(); + + return ( + <> + + {_t("settings|appearance|custom_themes")} + +
+ ) => { + setError(undefined); + setCustomTheme(e.target.value); + }} + onSave={async () => { + // The field empty is empty + if (!customTheme) return; + + // Get the custom themes and do a cheap clone + // To avoid to mutate the original array in the settings + const currentThemes = + SettingsStore.getValue("custom_themes").map((t) => t) || []; + + try { + const r = await fetch(customTheme); + // XXX: need some schema for this + const themeInfo = await r.json(); + if ( + !themeInfo || + typeof themeInfo["name"] !== "string" || + typeof themeInfo["colors"] !== "object" + ) { + setError(_t("settings|appearance|custom_theme_invalid")); + return; + } + currentThemes.push(themeInfo); + } catch (e) { + logger.error(e); + setError(_t("settings|appearance|custom_theme_error_downloading")); + return; + } + + // Reset the error + setError(undefined); + setCustomTheme(""); + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); + }} + onCancel={() => { + setError(undefined); + setCustomTheme(""); + }} + /> + +
+ + ); +} - let customThemeForm: JSX.Element | undefined; - if (SettingsStore.getValue("feature_custom_themes")) { - let messageElement: JSX.Element | undefined; - if (this.state.customThemeMessage.text) { - if (this.state.customThemeMessage.isError) { - messageElement =
{this.state.customThemeMessage.text}
; - } else { - messageElement =
{this.state.customThemeMessage.text}
; - } - } - customThemeForm = ( -
-
- - ("custom_themes") || []; + + return ( + <> + {customThemes.map((theme) => { + return ( +
+ {theme.name} + { + // Get the custom themes and do a cheap clone + // To avoid to mutate the original array in the settings + const currentThemes = + SettingsStore.getValue("custom_themes").map((t) => t) || []; + + // Remove the theme from the list + const newThemes = currentThemes.filter((t) => t.name !== theme.name); + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, newThemes); + }} > - {_t("settings|appearance|custom_theme_add_button")} - - {messageElement} - -
- ); - } - - const orderedThemes = getOrderedThemes(); - return ( - - {systemThemeSection} -
- ({ - value: t.id, - label: t.name, - disabled: this.state.useSystemTheme, - className: "mx_ThemeSelector_" + t.id, - }))} - onChange={this.onThemeChange} - value={this.apparentSelectedThemeId()} - outlined - /> -
- {this.renderHighContrastCheckbox()} - {customThemeForm} -
- ); - } - - public apparentSelectedThemeId(): string | undefined { - if (this.state.useSystemTheme) { - return undefined; - } - const nonHighContrast = findNonHighContrastTheme(this.state.theme); - return nonHighContrast ? nonHighContrast : this.state.theme; - } + + +
+ ); + })} + + ); } diff --git a/src/components/views/settings/ThemeChoicePanel2.tsx b/src/components/views/settings/ThemeChoicePanel2.tsx deleted file mode 100644 index 5e584a133e1..00000000000 --- a/src/components/views/settings/ThemeChoicePanel2.tsx +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright 2024 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, { ChangeEvent, Dispatch, JSX, useMemo, useRef, useState } from "react"; -import { - InlineField, - ToggleControl, - Label, - Root, - RadioControl, - Text, - EditInPlace, - IconButton, -} from "@vector-im/compound-web"; -import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg"; -import classNames from "classnames"; - -import { _t } from "../../../languageHandler"; -import SettingsSubsection from "./shared/SettingsSubsection"; -import ThemeWatcher from "../../../settings/watchers/ThemeWatcher"; -import SettingsStore from "../../../settings/SettingsStore"; -import { SettingLevel } from "../../../settings/SettingLevel"; -import dis from "../../../dispatcher/dispatcher"; -import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; -import { Action } from "../../../dispatcher/actions"; -import { useTheme } from "../../../hooks/useTheme"; -import { findHighContrastTheme, getOrderedThemes, CustomTheme as CustomThemeType, ITheme } from "../../../theme"; -import { useSettingValue } from "../../../hooks/useSettings"; -import { logger } from "../../../../../matrix-js-sdk/src/logger"; - -/** - * Interface for the theme state - */ -interface ThemeState { - /* The theme */ - theme: string; - /* Whether the system theme is activated */ - systemThemeActivated: boolean; -} - -/** - * Hook to fetch the value of the theme and dynamically update when it changes - */ -function useThemeState(): [ThemeState, Dispatch>] { - const theme = useTheme(); - const [themeState, setThemeState] = useState(theme); - - return [themeState, setThemeState]; -} - -/** - * Panel to choose the theme - */ -export function ThemeChoicePanel(): JSX.Element { - const [themeState, setThemeState] = useThemeState(); - const themeWatcher = useRef(new ThemeWatcher()); - const customThemeEnabled = useSettingValue("feature_custom_themes"); - - return ( - - {themeWatcher.current.isSystemThemeSupported() && ( - - setThemeState((_themeState) => ({ ..._themeState, systemThemeActivated })) - } - /> - )} - setThemeState((_themeState) => ({ ..._themeState, theme }))} - /> - {customThemeEnabled && } - - ); -} - -/** - * Component to toggle the system theme - */ -interface SystemThemeProps { - /* Whether the system theme is activated */ - systemThemeActivated: boolean; - /* Callback when the system theme is toggled */ - onChange: (systemThemeActivated: boolean) => void; -} - -/** - * Component to toggle the system theme - */ -function SystemTheme({ systemThemeActivated, onChange }: SystemThemeProps): JSX.Element { - return ( - { - const checked = new FormData(evt.currentTarget).get("systemTheme") === "on"; - onChange(checked); - await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); - dis.dispatch({ action: Action.RecheckTheme }); - }} - > - } - > - - - - ); -} - -/** - * Component to select the theme - */ -interface ThemeSelectorProps { - /* The current theme */ - theme: string; - /* The theme can't be selected */ - disabled: boolean; - /* Callback when the theme is changed */ - onChange: (theme: string) => void; -} - -/** - * Component to select the theme - */ -function ThemeSelectors({ theme, disabled, onChange }: ThemeSelectorProps): JSX.Element { - const themes = useThemes(); - - return ( - { - // We don't have any file in the form, we can cast it as string safely - const newTheme = new FormData(evt.currentTarget).get("themeSelector") as string | null; - - // Do nothing if the same theme is selected - if (!newTheme || theme === newTheme) return; - - // doing getValue in the .catch will still return the value we failed to set, - // so remember what the value was before we tried to set it so we can revert - const oldTheme = SettingsStore.getValue("theme"); - SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => { - dis.dispatch({ action: Action.RecheckTheme }); - onChange(oldTheme); - }); - - onChange(newTheme); - // The settings watcher doesn't fire until the echo comes back from the - // server, so to make the theme change immediately we need to manually - // do the dispatch now - // XXX: The local echoed value appears to be unreliable, in particular - // when settings custom themes(!) so adding forceTheme to override - // the value from settings. - dis.dispatch({ action: Action.RecheckTheme, forceTheme: newTheme }); - }} - > - {themes.map((_theme) => { - return ( - - } - > - - - ); - })} - - ); -} - -/** - * Return all the available themes - */ -function useThemes(): Array { - const customThemes = useSettingValue("custom_themes"); - return useMemo(() => { - const themes = getOrderedThemes(); - // Put the custom theme into a map - // To easily find the theme by name when going through the themes list - const customThemeMap = customThemes?.reduce( - (map, theme) => map.set(theme.name, theme), - new Map(), - ); - - // Separate the built-in themes from the custom themes - // To insert the high contrast theme between them - const builtInThemes = themes.filter((theme) => !customThemeMap?.has(theme.name)); - const otherThemes = themes.filter((theme) => customThemeMap?.has(theme.name)); - - const highContrastTheme = makeHighContrastTheme(); - if (highContrastTheme) builtInThemes.push(highContrastTheme); - - const allThemes = builtInThemes.concat(otherThemes); - - // Check if the themes are dark - return allThemes.map((theme) => { - const customTheme = customThemeMap?.get(theme.name); - const isDark = (customTheme ? customTheme.is_dark : theme.id.includes("dark")) || false; - return { ...theme, isDark }; - }); - }, [customThemes]); -} - -/** - * Create the light high contrast theme - */ -function makeHighContrastTheme(): ITheme | undefined { - const lightHighContrastId = findHighContrastTheme("light"); - if (lightHighContrastId) { - return { - name: _t("settings|appearance|high_contrast"), - id: lightHighContrastId, - }; - } -} - -/** - * Add and manager custom themes - */ -function CustomTheme(): JSX.Element { - const [customTheme, setCustomTheme] = useState(""); - const [error, setError] = useState(); - - return ( - <> - - {_t("settings|appearance|custom_themes")} - -
- ) => { - setError(undefined); - setCustomTheme(e.target.value); - }} - onSave={async () => { - // The field empty is empty - if (!customTheme) return; - - // Get the custom themes and do a cheap clone - // To avoid to mutate the original array in the settings - const currentThemes = - SettingsStore.getValue("custom_themes").map((t) => t) || []; - - try { - const r = await fetch(customTheme); - // XXX: need some schema for this - const themeInfo = await r.json(); - if ( - !themeInfo || - typeof themeInfo["name"] !== "string" || - typeof themeInfo["colors"] !== "object" - ) { - setError(_t("settings|appearance|custom_theme_invalid")); - return; - } - currentThemes.push(themeInfo); - } catch (e) { - logger.error(e); - setError(_t("settings|appearance|custom_theme_error_downloading")); - return; - } - - // Reset the error - setError(undefined); - setCustomTheme(""); - await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); - }} - onCancel={() => { - setError(undefined); - setCustomTheme(""); - }} - /> - -
- - ); -} - -/** - * List of the custom themes - * @constructor - */ -function CustomThemeList(): JSX.Element { - const customThemes = useSettingValue("custom_themes") || []; - - return ( - <> - {customThemes.map((theme) => { - return ( -
- {theme.name} - { - // Get the custom themes and do a cheap clone - // To avoid to mutate the original array in the settings - const currentThemes = - SettingsStore.getValue("custom_themes").map((t) => t) || []; - - // Remove the theme from the list - const newThemes = currentThemes.filter((t) => t.name !== theme.name); - await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, newThemes); - }} - > - - -
- ); - })} - - ); -} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 510ba488d03..cc76a7b2c1e 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -28,8 +28,7 @@ import { UIFeature } from "../../../../../settings/UIFeature"; import { Layout } from "../../../../../settings/enums/Layout"; import LayoutSwitcher from "../../LayoutSwitcher"; import FontScalingPanel from "../../FontScalingPanel"; -import ThemeChoicePanel from "../../ThemeChoicePanel"; -import { ThemeChoicePanel as ThemeChoicePanel2 } from "../../ThemeChoicePanel2"; +import { ThemeChoicePanel } from "../../ThemeChoicePanel"; import ImageSizePanel from "../../ImageSizePanel"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; @@ -157,7 +156,6 @@ export default class AppearanceUserSettingsTab extends React.Component - Date: Tue, 18 Jun 2024 16:32:07 +0200 Subject: [PATCH 10/41] Fix QuickThemeSwitcher-test.tsx --- .../views/spaces/QuickThemeSwitcher-test.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/components/views/spaces/QuickThemeSwitcher-test.tsx b/test/components/views/spaces/QuickThemeSwitcher-test.tsx index 0fc72fe92bc..7fe5c691bd0 100644 --- a/test/components/views/spaces/QuickThemeSwitcher-test.tsx +++ b/test/components/views/spaces/QuickThemeSwitcher-test.tsx @@ -21,17 +21,17 @@ import { mocked } from "jest-mock"; import QuickThemeSwitcher from "../../../../src/components/views/spaces/QuickThemeSwitcher"; import { getOrderedThemes } from "../../../../src/theme"; -import ThemeChoicePanel from "../../../../src/components/views/settings/ThemeChoicePanel"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; import dis from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; import { mockPlatformPeg } from "../../../test-utils/platform"; +import { useTheme } from "../../../../src/hooks/useTheme"; -jest.mock("../../../../src/theme"); -jest.mock("../../../../src/components/views/settings/ThemeChoicePanel", () => ({ - calculateThemeState: jest.fn(), +jest.mock("../../../../src/hooks/useTheme", () => ({ + useTheme: jest.fn(), })); +jest.mock("../../../../src/theme"); jest.mock("../../../../src/settings/SettingsStore", () => ({ setValue: jest.fn(), getValue: jest.fn(), @@ -59,9 +59,10 @@ describe("", () => { { id: "light", name: "Light" }, { id: "dark", name: "Dark" }, ]); - mocked(ThemeChoicePanel).calculateThemeState.mockClear().mockReturnValue({ + + mocked(useTheme).mockClear().mockReturnValue({ theme: "light", - useSystemTheme: false, + systemThemeActivated: false, }); mocked(SettingsStore).setValue.mockClear().mockResolvedValue(); mocked(dis).dispatch.mockClear(); @@ -85,9 +86,9 @@ describe("", () => { }); it("renders dropdown correctly when use system theme is truthy", () => { - mocked(ThemeChoicePanel).calculateThemeState.mockClear().mockReturnValue({ + mocked(useTheme).mockClear().mockReturnValue({ theme: "light", - useSystemTheme: true, + systemThemeActivated: true, }); renderComponent(); expect(screen.getByText("Match system")).toBeInTheDocument(); From 651deaabf61d5b7e7af0fd222c450438d64763d8 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 18 Jun 2024 16:41:03 +0200 Subject: [PATCH 11/41] Fix AppearanceUserSettingsTab-test.tsx --- .../views/settings/ThemeChoicePanel.tsx | 4 +- .../AppearanceUserSettingsTab-test.tsx.snap | 144 +++++++++++++----- 2 files changed, 105 insertions(+), 43 deletions(-) diff --git a/src/components/views/settings/ThemeChoicePanel.tsx b/src/components/views/settings/ThemeChoicePanel.tsx index 5e584a133e1..e8ca5ecfa1e 100644 --- a/src/components/views/settings/ThemeChoicePanel.tsx +++ b/src/components/views/settings/ThemeChoicePanel.tsx @@ -203,12 +203,12 @@ function ThemeSelectors({ theme, disabled, onChange }: ThemeSelectorProps): JSX. * Return all the available themes */ function useThemes(): Array { - const customThemes = useSettingValue("custom_themes"); + const customThemes = useSettingValue("custom_themes") || []; return useMemo(() => { const themes = getOrderedThemes(); // Put the custom theme into a map // To easily find the theme by name when going through the themes list - const customThemeMap = customThemes?.reduce( + const customThemeMap = customThemes.reduce( (map, theme) => map.set(theme.name, theme), new Map(), ); diff --git a/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap index b2ab3a6e833..607a32c98c3 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap @@ -16,71 +16,133 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` class="mx_SettingsSection_subSections" >

Theme

-
-