Get theme automatically from system setting
Uses CSS `prefers-color-scheme` to get the user's preferred colour scheme. Also bundles up some theme logic into its own class.
This commit is contained in:
parent
55e5e63199
commit
5a700b518a
5 changed files with 100 additions and 18 deletions
|
@ -59,7 +59,7 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
|||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||
import DMRoomMap from '../../utils/DMRoomMap';
|
||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||
import { setTheme } from "../../theme";
|
||||
import { ThemeWatcher } from "../../theme";
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer } from "../../utils/promise";
|
||||
|
||||
|
@ -274,7 +274,8 @@ export default createReactClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onThemeChanged);
|
||||
this._themeWatcher = new ThemeWatcher();
|
||||
this._themeWatcher.start();
|
||||
|
||||
this.focusComposer = false;
|
||||
|
||||
|
@ -361,7 +362,7 @@ export default createReactClass({
|
|||
componentWillUnmount: function() {
|
||||
Lifecycle.stopMatrixClient();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
SettingsStore.unwatchSetting(this._themeWatchRef);
|
||||
this._themeWatcher.stop();
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize);
|
||||
|
@ -384,13 +385,6 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onThemeChanged: function(settingName, roomId, atLevel, newValue) {
|
||||
dis.dispatch({
|
||||
action: 'set_theme',
|
||||
value: newValue,
|
||||
});
|
||||
},
|
||||
|
||||
startPageChangeTimer() {
|
||||
// Tor doesn't support performance
|
||||
if (!performance || !performance.mark) return null;
|
||||
|
@ -672,9 +666,6 @@ export default createReactClass({
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'set_theme':
|
||||
setTheme(payload.value);
|
||||
break;
|
||||
case 'on_logging_in':
|
||||
// We are now logging in, so set the state to reflect that
|
||||
// NB. This does not touch 'ready' since if our dispatches
|
||||
|
|
|
@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
|
|||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||
import PropTypes from "prop-types";
|
||||
import {enumerateThemes} from "../../../../../theme";
|
||||
import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
|
||||
import PlatformPeg from "../../../../../PlatformPeg";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../..";
|
||||
|
@ -50,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
this.state = {
|
||||
language: languageHandler.getCurrentLanguage(),
|
||||
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
|
||||
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
|
||||
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
|
||||
serverSupportsSeparateAddAndBind: null,
|
||||
idServerHasUnsignedTerms: false,
|
||||
|
@ -177,16 +178,22 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
// 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.ACCOUNT, newTheme).catch(() => {
|
||||
dis.dispatch({action: 'set_theme', value: oldTheme});
|
||||
dis.dispatch({action: 'recheck_theme'});
|
||||
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
|
||||
dis.dispatch({action: 'set_theme', value: newTheme});
|
||||
dis.dispatch({action: 'recheck_theme'});
|
||||
};
|
||||
|
||||
_onUseSystemThemeChanged = (checked) => {
|
||||
this.setState({useSystemTheme: checked});
|
||||
dis.dispatch({action: 'recheck_theme'});
|
||||
}
|
||||
|
||||
|
||||
_onPasswordChangeError = (err) => {
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
let errMsg = err.error || "";
|
||||
|
@ -297,11 +304,24 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
_renderThemeSection() {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
let systemThemeSection;
|
||||
if (themeWatcher.isSystemThemeSupported()) {
|
||||
systemThemeSection = <div>
|
||||
<SettingsFlag name="use_system_theme" level={SettingLevel.DEVICE}
|
||||
onChange={this._onUseSystemThemeChanged}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
||||
{systemThemeSection}
|
||||
<Field id="theme" label={_t("Theme")} element="select"
|
||||
value={this.state.theme} onChange={this._onThemeChange}>
|
||||
value={this.state.theme} onChange={this._onThemeChange}
|
||||
disabled={this.state.useSystemTheme}
|
||||
>
|
||||
{Object.entries(enumerateThemes()).map(([theme, text]) => {
|
||||
return <option key={theme} value={theme}>{text}</option>;
|
||||
})}
|
||||
|
|
|
@ -363,6 +363,7 @@
|
|||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||
"Mirror local video feed": "Mirror local video feed",
|
||||
"Enable Community Filter Panel": "Enable Community Filter Panel",
|
||||
"Match system theme": "Match system theme",
|
||||
"Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls",
|
||||
"Send analytics data": "Send analytics data",
|
||||
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
|
||||
|
|
|
@ -275,6 +275,11 @@ export const SETTINGS = {
|
|||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: [],
|
||||
},
|
||||
"use_system_theme": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: true,
|
||||
displayName: _td("Match system theme"),
|
||||
},
|
||||
"webRtcAllowPeerToPeer": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
displayName: _td('Allow Peer-to-Peer for 1:1 calls'),
|
||||
|
|
67
src/theme.js
67
src/theme.js
|
@ -19,8 +19,72 @@ import {_t} from "./languageHandler";
|
|||
|
||||
export const DEFAULT_THEME = "light";
|
||||
import Tinter from "./Tinter";
|
||||
import dis from "./dispatcher";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
export class ThemeWatcher {
|
||||
static _instance = null;
|
||||
|
||||
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 = global.matchMedia("(prefers-color-scheme: dark)");
|
||||
this._preferLight = global.matchMedia("(prefers-color-scheme: light)");
|
||||
|
||||
this._currentTheme = this.getEffectiveTheme();
|
||||
}
|
||||
|
||||
start() {
|
||||
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._dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._preferDark.removeEventListener('change', this._onChange);
|
||||
this._preferLight.removeEventListener('change', this._onChange);
|
||||
SettingsStore.unwatchSetting(this._systemThemeWatchRef);
|
||||
SettingsStore.unwatchSetting(this._themeWatchRef);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
_onChange = () => {
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
if (payload.action === 'recheck_theme') {
|
||||
this.recheck();
|
||||
}
|
||||
}
|
||||
|
||||
recheck() {
|
||||
const oldTheme = this._currentTheme;
|
||||
this._currentTheme = this.getEffectiveTheme();
|
||||
if (oldTheme !== this._currentTheme) {
|
||||
setTheme(this._currentTheme);
|
||||
}
|
||||
}
|
||||
|
||||
getEffectiveTheme() {
|
||||
if (SettingsStore.getValue('use_system_theme')) {
|
||||
if (this._preferDark.matches) return 'dark';
|
||||
if (this._preferLight.matches) return 'light';
|
||||
}
|
||||
return SettingsStore.getValue('theme');
|
||||
}
|
||||
|
||||
isSystemThemeSupported() {
|
||||
return this._preferDark || this._preferLight;
|
||||
}
|
||||
}
|
||||
|
||||
export function enumerateThemes() {
|
||||
const BUILTIN_THEMES = {
|
||||
"light": _t("Light theme"),
|
||||
|
@ -83,7 +147,8 @@ export function getBaseTheme(theme) {
|
|||
*/
|
||||
export function setTheme(theme) {
|
||||
if (!theme) {
|
||||
theme = SettingsStore.getValue("theme");
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
theme = themeWatcher.getEffectiveTheme();
|
||||
}
|
||||
let stylesheetName = theme;
|
||||
if (theme.startsWith("custom-")) {
|
||||
|
|
Loading…
Reference in a new issue