UNPKG

matrix-react-sdk

Version:
155 lines (133 loc) 6.3 kB
/* Copyright 2024 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../SettingsStore"; import dis from "../../dispatcher/dispatcher"; import { Action } from "../../dispatcher/actions"; import ThemeController from "../controllers/ThemeController"; import { findHighContrastTheme, setTheme } from "../../theme"; import { ActionPayload } from "../../dispatcher/payloads"; import { SettingLevel } from "../SettingLevel"; export default class ThemeWatcher { private themeWatchRef: string | null; private systemThemeWatchRef: string | null; private dispatcherRef: string | null; private preferDark: MediaQueryList; private preferLight: MediaQueryList; private preferHighContrast: MediaQueryList; private currentTheme: string; public constructor() { this.themeWatchRef = null; this.systemThemeWatchRef = null; this.dispatcherRef = null; // we have both here as each may either match or not match, so by having both // we can get the tristate of dark/light/unsupported this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)"); this.preferLight = (<any>global).matchMedia("(prefers-color-scheme: light)"); this.preferHighContrast = (<any>global).matchMedia("(prefers-contrast: more)"); this.currentTheme = this.getEffectiveTheme(); } public start(): void { this.themeWatchRef = SettingsStore.watchSetting("theme", null, this.onChange); this.systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this.onChange); this.preferDark.addEventListener("change", this.onChange); this.preferLight.addEventListener("change", this.onChange); this.preferHighContrast.addEventListener("change", this.onChange); this.dispatcherRef = dis.register(this.onAction); } public stop(): void { this.preferDark.removeEventListener("change", this.onChange); this.preferLight.removeEventListener("change", this.onChange); this.preferHighContrast.removeEventListener("change", this.onChange); if (this.systemThemeWatchRef) SettingsStore.unwatchSetting(this.systemThemeWatchRef); if (this.themeWatchRef) SettingsStore.unwatchSetting(this.themeWatchRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef); } private onChange = (): void => { this.recheck(); }; private onAction = (payload: ActionPayload): void => { if (payload.action === Action.RecheckTheme) { // XXX forceTheme this.recheck(payload.forceTheme); } }; // XXX: forceTheme param added here as local echo appears to be unreliable // https://github.com/vector-im/element-web/issues/11443 public recheck(forceTheme?: string): void { const oldTheme = this.currentTheme; this.currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; if (oldTheme !== this.currentTheme) { setTheme(this.currentTheme); } } public getEffectiveTheme(): string { // Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab // XXX: checking the isLight flag here makes checking it in the ThemeController // itself completely redundant since we just override the result here and we're // now effectively just using the ThemeController as a place to store the static // variable. The system theme setting probably ought to have an equivalent // controller that honours the same flag, although probably better would be to // have the theme logic in one place rather than split between however many // different places. if (ThemeController.isLogin) return "light"; // If the user has specifically enabled the system matching option (excluding default), // then use that over anything else. We pick the lowest possible level for the setting // to ensure the ordering otherwise works. const systemThemeExplicit = SettingsStore.getValueAt( SettingLevel.DEVICE, "use_system_theme", null, false, true, ); if (systemThemeExplicit) { logger.log("returning explicit system theme"); const theme = this.themeBasedOnSystem(); if (theme) { return theme; } } // If the user has specifically enabled the theme (without the system matching option being // enabled specifically and excluding the default), use that theme. We pick the lowest possible // level for the setting to ensure the ordering otherwise works. const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); if (themeExplicit) { logger.log("returning explicit theme: " + themeExplicit); return themeExplicit; } // If the user hasn't really made a preference in either direction, assume the defaults of the // settings and use those. if (SettingsStore.getValue("use_system_theme")) { const theme = this.themeBasedOnSystem(); if (theme) { return theme; } } logger.log("returning theme value"); return SettingsStore.getValue("theme"); } private themeBasedOnSystem(): string | undefined { let newTheme: string | undefined; if (this.preferDark.matches) { newTheme = "dark"; } else if (this.preferLight.matches) { newTheme = "light"; } if (newTheme && this.preferHighContrast.matches) { const hcTheme = findHighContrastTheme(newTheme); if (hcTheme) { newTheme = hcTheme; } } return newTheme; } public isSystemThemeSupported(): boolean { return this.preferDark.matches || this.preferLight.matches; } }