Merge pull request #3637 from matrix-org/dbkr/system_dark_mode

Get theme automatically from system setting
This commit is contained in:
David Baker 2019-11-20 16:32:37 +00:00 committed by GitHub
commit 00241a8d0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 18 deletions

View file

@ -59,7 +59,7 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap'; import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs'; import { countRoomsWithNotif } from '../../RoomNotifs';
import { setTheme } from "../../theme"; import { ThemeWatcher } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer } from "../../utils/promise"; import { defer } from "../../utils/promise";
@ -274,7 +274,8 @@ export default createReactClass({
componentDidMount: function() { componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onThemeChanged); this._themeWatcher = new ThemeWatcher();
this._themeWatcher.start();
this.focusComposer = false; this.focusComposer = false;
@ -361,7 +362,7 @@ export default createReactClass({
componentWillUnmount: function() { componentWillUnmount: function() {
Lifecycle.stopMatrixClient(); Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
SettingsStore.unwatchSetting(this._themeWatchRef); this._themeWatcher.stop();
window.removeEventListener("focus", this.onFocus); window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); 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() { startPageChangeTimer() {
// Tor doesn't support performance // Tor doesn't support performance
if (!performance || !performance.mark) return null; if (!performance || !performance.mark) return null;
@ -672,9 +666,6 @@ export default createReactClass({
}); });
break; break;
} }
case 'set_theme':
setTheme(payload.value);
break;
case 'on_logging_in': case 'on_logging_in':
// We are now logging in, so set the state to reflect that // We are now logging in, so set the state to reflect that
// NB. This does not touch 'ready' since if our dispatches // NB. This does not touch 'ready' since if our dispatches

View file

@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {enumerateThemes} from "../../../../../theme"; import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
import PlatformPeg from "../../../../../PlatformPeg"; import PlatformPeg from "../../../../../PlatformPeg";
import MatrixClientPeg from "../../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../.."; import sdk from "../../../../..";
@ -50,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
this.state = { this.state = {
language: languageHandler.getCurrentLanguage(), language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverSupportsSeparateAddAndBind: null, serverSupportsSeparateAddAndBind: null,
idServerHasUnsignedTerms: false, idServerHasUnsignedTerms: false,
@ -177,16 +178,25 @@ export default class GeneralUserSettingsTab extends React.Component {
// so remember what the value was before we tried to set it so we can revert // so remember what the value was before we tried to set it so we can revert
const oldTheme = SettingsStore.getValue('theme'); const oldTheme = SettingsStore.getValue('theme');
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { 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: oldTheme});
}); });
this.setState({theme: newTheme}); this.setState({theme: newTheme});
// The settings watcher doesn't fire until the echo comes back from the // 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 // server, so to make the theme change immediately we need to manually
// do the dispatch now // do the dispatch now
dis.dispatch({action: 'set_theme', value: newTheme}); // 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: 'recheck_theme', forceTheme: newTheme});
}; };
_onUseSystemThemeChanged = (checked) => {
this.setState({useSystemTheme: checked});
dis.dispatch({action: 'recheck_theme'});
}
_onPasswordChangeError = (err) => { _onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog // TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || ""; let errMsg = err.error || "";
@ -297,11 +307,24 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderThemeSection() { _renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); 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 ( return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection"> <div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span> <span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
{systemThemeSection}
<Field id="theme" label={_t("Theme")} element="select" <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]) => { {Object.entries(enumerateThemes()).map(([theme, text]) => {
return <option key={theme} value={theme}>{text}</option>; return <option key={theme} value={theme}>{text}</option>;
})} })}

View file

@ -364,6 +364,7 @@
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
"Mirror local video feed": "Mirror local video feed", "Mirror local video feed": "Mirror local video feed",
"Enable Community Filter Panel": "Enable Community Filter Panel", "Enable Community Filter Panel": "Enable Community Filter Panel",
"Match system dark mode setting": "Match system dark mode setting",
"Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls",
"Send analytics data": "Send analytics data", "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", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",

View file

@ -281,6 +281,11 @@ export const SETTINGS = {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: [], default: [],
}, },
"use_system_theme": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: true,
displayName: _td("Match system dark mode setting"),
},
"webRtcAllowPeerToPeer": { "webRtcAllowPeerToPeer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Allow Peer-to-Peer for 1:1 calls'), displayName: _td('Allow Peer-to-Peer for 1:1 calls'),

View file

@ -19,8 +19,75 @@ import {_t} from "./languageHandler";
export const DEFAULT_THEME = "light"; export const DEFAULT_THEME = "light";
import Tinter from "./Tinter"; import Tinter from "./Tinter";
import dis from "./dispatcher";
import SettingsStore from "./settings/SettingsStore"; 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') {
// XXX forceTheme
this.recheck(payload.forceTheme);
}
}
// XXX: forceTheme param aded here as local echo appears to be unreliable
// https://github.com/vector-im/riot-web/issues/11443
recheck(forceTheme) {
const oldTheme = this._currentTheme;
this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme;
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.matches || this._preferLight.matches;
}
}
export function enumerateThemes() { export function enumerateThemes() {
const BUILTIN_THEMES = { const BUILTIN_THEMES = {
"light": _t("Light theme"), "light": _t("Light theme"),
@ -83,7 +150,8 @@ export function getBaseTheme(theme) {
*/ */
export function setTheme(theme) { export function setTheme(theme) {
if (!theme) { if (!theme) {
theme = SettingsStore.getValue("theme"); const themeWatcher = new ThemeWatcher();
theme = themeWatcher.getEffectiveTheme();
} }
let stylesheetName = theme; let stylesheetName = theme;
if (theme.startsWith("custom-")) { if (theme.startsWith("custom-")) {