Merge pull request #4658 from JorikSchellekens/joriks/appearance-tab-ts

Move Appearance tab to ts
pull/21833/head
Jorik Schellekens 2020-05-28 14:54:05 +01:00 committed by GitHub
commit 4bee6532e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 299 additions and 181 deletions

View File

@ -58,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap'; import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs'; import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../FontWatcher'; import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred } from "../../utils/promise"; import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";

View File

@ -34,7 +34,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInput
id?: string, id?: string,
// The element to create. Defaults to "input". // The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field> // To define options for a select, use <Field><option ... /></Field>
element?: "input" | " select" | "textarea", element?: "input" | "select" | "textarea",
// The field's type (when used as an <input>). Defaults to "text". // The field's type (when used as an <input>). Defaults to "text".
type?: string, type?: string,
// id of a <datalist> element for suggestions // id of a <datalist> element for suggestions

View File

@ -20,34 +20,64 @@ import React from 'react';
import {_t} from "../../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import * as sdk from "../../../../../index"; import * as sdk from "../../../../../index";
import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import { enumerateThemes } from "../../../../../theme";
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
import Field from "../../../elements/Field"; import Field from "../../../elements/Field";
import Slider from "../../../elements/Slider"; import Slider from "../../../elements/Slider";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import { FontWatcher } from "../../../../../FontWatcher"; import { FontWatcher } from "../../../../../settings/watchers/FontWatcher";
import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload';
import { Action } from '../../../../../dispatcher/actions';
import { IValidationResult, IFieldState } from '../../../elements/Validation';
export default class AppearanceUserSettingsTab extends React.Component { interface IProps {
constructor() { }
super();
interface IThemeState {
theme: string,
useSystemTheme: boolean,
}
export interface CustomThemeMessage {
isError: boolean,
text: string
};
interface IState extends IThemeState {
// String displaying the current selected fontSize.
// Needs to be string for things like '17.' without
// trailing 0s.
fontSize: string,
customThemeUrl: string,
customThemeMessage: CustomThemeMessage,
useCustomFontSize: boolean,
}
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
private themeTimer: NodeJS.Timeout;
constructor(props: IProps) {
super(props);
this.state = { this.state = {
fontSize: SettingsStore.getValue("fontSize", null), fontSize: SettingsStore.getValue("fontSize", null).toString(),
...this._calculateThemeState(), ...this.calculateThemeState(),
customThemeUrl: "", customThemeUrl: "",
customThemeMessage: {isError: false, text: ""}, customThemeMessage: {isError: false, text: ""},
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"), useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
}; };
} }
_calculateThemeState() { private calculateThemeState(): IThemeState {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things. // show the right values for things.
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"); const themeChoice: string = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
const systemThemeExplicit = SettingsStore.getValueAt( const systemThemeExplicit: boolean = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true); SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit = SettingsStore.getValueAt( const themeExplicit: string = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true); SettingLevel.DEVICE, "theme", null, false, true);
// If the user has enabled system theme matching, use that. // If the user has enabled system theme matching, use that.
@ -73,15 +103,15 @@ export default class AppearanceUserSettingsTab extends React.Component {
}; };
} }
_onThemeChange = (e) => { private onThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void {
const newTheme = e.target.value; const newTheme = e.target.value;
if (this.state.theme === newTheme) return; if (this.state.theme === newTheme) return;
// doing getValue in the .catch will still return the value we failed to set, // 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 // so remember what the value was before we tried to set it so we can revert
const oldTheme = SettingsStore.getValue('theme'); const oldTheme: string = SettingsStore.getValue('theme');
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
dis.dispatch({action: 'recheck_theme'}); dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
this.setState({theme: oldTheme}); this.setState({theme: oldTheme});
}); });
this.setState({theme: newTheme}); this.setState({theme: newTheme});
@ -91,23 +121,21 @@ export default class AppearanceUserSettingsTab extends React.Component {
// XXX: The local echoed value appears to be unreliable, in particular // XXX: The local echoed value appears to be unreliable, in particular
// when settings custom themes(!) so adding forceTheme to override // when settings custom themes(!) so adding forceTheme to override
// the value from settings. // the value from settings.
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme, forceTheme: newTheme});
}; };
_onUseSystemThemeChanged = (checked) => { private onUseSystemThemeChanged(checked: boolean) {
this.setState({useSystemTheme: checked}); this.setState({useSystemTheme: checked});
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
dis.dispatch({action: 'recheck_theme'}); dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
}; };
_onFontSizeChanged = (size) => { private onFontSizeChanged(size: number) {
this.setState({fontSize: size}); this.setState({fontSize: size.toString()});
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size); SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size);
}; };
_onValidateFontSize = ({value}) => { private async onValidateFontSize({value}: Pick<IFieldState, "value">): Promise<IValidationResult> {
console.log({value});
const parsedSize = parseFloat(value); const parsedSize = parseFloat(value);
const min = FontWatcher.MIN_SIZE; const min = FontWatcher.MIN_SIZE;
const max = FontWatcher.MAX_SIZE; const max = FontWatcher.MAX_SIZE;
@ -127,17 +155,18 @@ export default class AppearanceUserSettingsTab extends React.Component {
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})}; return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
} }
_onAddCustomTheme = async () => { private async onAddCustomTheme() {
let currentThemes = SettingsStore.getValue("custom_themes"); let currentThemes: string[] = SettingsStore.getValue("custom_themes");
if (!currentThemes) currentThemes = []; if (!currentThemes) currentThemes = [];
currentThemes = currentThemes.map(c => c); // cheap clone currentThemes = currentThemes.map(c => c); // cheap clone
if (this._themeTimer) { if (this.themeTimer) {
clearTimeout(this._themeTimer); clearTimeout(this.themeTimer);
} }
try { try {
const r = await fetch(this.state.customThemeUrl); const r = await fetch(this.state.customThemeUrl);
// XXX: need some schema for this
const themeInfo = await r.json(); const themeInfo = await r.json();
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') { if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}}); this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
@ -153,42 +182,32 @@ export default class AppearanceUserSettingsTab extends React.Component {
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}}); this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
this._themeTimer = setTimeout(() => { this.themeTimer = setTimeout(() => {
this.setState({customThemeMessage: {text: "", isError: false}}); this.setState({customThemeMessage: {text: "", isError: false}});
}, 3000); }, 3000);
}; };
_onCustomThemeChange = (e) => { private onCustomThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) {
this.setState({customThemeUrl: e.target.value}); this.setState({customThemeUrl: e.target.value});
}; };
render() { private renderThemeSection() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
{this._renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this._renderFontSection() : null}
</div>
);
}
_renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
const themeWatcher = new ThemeWatcher(); const themeWatcher = new ThemeWatcher();
let systemThemeSection; let systemThemeSection: JSX.Element;
if (themeWatcher.isSystemThemeSupported()) { if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div> systemThemeSection = <div>
<LabelledToggleSwitch <LabelledToggleSwitch
value={this.state.useSystemTheme} value={this.state.useSystemTheme}
label={SettingsStore.getDisplayName("use_system_theme")} label={SettingsStore.getDisplayName("use_system_theme")}
onChange={this._onUseSystemThemeChanged} onChange={this.onUseSystemThemeChanged}
/> />
</div>; </div>;
} }
let customThemeForm; let customThemeForm: JSX.Element;
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
let messageElement = null; let messageElement = null;
if (this.state.customThemeMessage.text) { if (this.state.customThemeMessage.text) {
@ -200,17 +219,17 @@ export default class AppearanceUserSettingsTab extends React.Component {
} }
customThemeForm = ( customThemeForm = (
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
<form onSubmit={this._onAddCustomTheme}> <form onSubmit={this.onAddCustomTheme}>
<Field <Field
label={_t("Custom theme URL")} label={_t("Custom theme URL")}
type='text' type='text'
id='mx_GeneralUserSettingsTab_customThemeInput' id='mx_GeneralUserSettingsTab_customThemeInput'
autoComplete="off" autoComplete="off"
onChange={this._onCustomThemeChange} onChange={this.onCustomThemeChange}
value={this.state.customThemeUrl} value={this.state.customThemeUrl}
/> />
<AccessibleButton <AccessibleButton
onClick={this._onAddCustomTheme} onClick={this.onAddCustomTheme}
type="submit" kind="primary_sm" type="submit" kind="primary_sm"
disabled={!this.state.customThemeUrl.trim()} disabled={!this.state.customThemeUrl.trim()}
>{_t("Add theme")}</AccessibleButton> >{_t("Add theme")}</AccessibleButton>
@ -220,7 +239,8 @@ export default class AppearanceUserSettingsTab extends React.Component {
); );
} }
const themes = Object.entries(enumerateThemes()) // XXX: replace any type here
const themes = Object.entries<any>(enumerateThemes())
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability .map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p)) const customThemes = themes.filter(p => !builtInThemes.includes(p))
@ -232,7 +252,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
{systemThemeSection} {systemThemeSection}
<Field <Field
id="theme" label={_t("Theme")} element="select" 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} disabled={this.state.useSystemTheme}
> >
{orderedThemes.map(theme => { {orderedThemes.map(theme => {
@ -245,7 +265,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
); );
} }
_renderFontSection() { private renderFontSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling"> return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span> <span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
@ -253,9 +273,9 @@ export default class AppearanceUserSettingsTab extends React.Component {
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div> <div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
<Slider <Slider
values={[13, 15, 16, 18, 20]} values={[13, 15, 16, 18, 20]}
value={this.state.fontSize} value={parseInt(this.state.fontSize, 10)}
onSelectionChange={this._onFontSizeChanged} onSelectionChange={this.onFontSizeChanged}
displayFunc={value => {}} displayFunc={value => ""}
disabled={this.state.useCustomFontSize} disabled={this.state.useCustomFontSize}
/> />
<div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div> <div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div>
@ -263,7 +283,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
<SettingsFlag <SettingsFlag
name="useCustomFontSize" name="useCustomFontSize"
level={SettingLevel.ACCOUNT} level={SettingLevel.ACCOUNT}
onChange={(checked)=> this.setState({useCustomFontSize: checked})} onChange={(checked) => this.setState({useCustomFontSize: checked})}
/> />
<Field <Field
type="text" type="text"
@ -272,10 +292,20 @@ export default class AppearanceUserSettingsTab extends React.Component {
placeholder={this.state.fontSize.toString()} placeholder={this.state.fontSize.toString()}
value={this.state.fontSize.toString()} value={this.state.fontSize.toString()}
id="font_size_field" id="font_size_field"
onValidate={this._onValidateFontSize} onValidate={this.onValidateFontSize}
onChange={(value) => this.setState({fontSize: value.target.value})} onChange={(value) => this.setState({fontSize: value.target.value})}
disabled={!this.state.useCustomFontSize} disabled={!this.state.useCustomFontSize}
/> />
</div>; </div>;
} }
render() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
{this.renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
</div>
);
}
} }

View File

@ -43,5 +43,10 @@ export enum Action {
* Sets the current tooltip. Should be use with ViewTooltipPayload. * Sets the current tooltip. Should be use with ViewTooltipPayload.
*/ */
ViewTooltip = "view_tooltip", ViewTooltip = "view_tooltip",
/**
* Forces the theme to reload. No additional payload information required.
*/
RecheckTheme = "recheck_theme",
} }

View File

@ -0,0 +1,27 @@
/*
Copyright 2020 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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface RecheckThemePayload extends ActionPayload {
action: Action.RecheckTheme,
/**
* Optionally specify the exact theme which is to be loaded.
*/
forceTheme?: string;
}

View File

@ -758,10 +758,10 @@
"Invalid theme schema.": "Invalid theme schema.", "Invalid theme schema.": "Invalid theme schema.",
"Error downloading theme information.": "Error downloading theme information.", "Error downloading theme information.": "Error downloading theme information.",
"Theme added!": "Theme added!", "Theme added!": "Theme added!",
"Appearance": "Appearance",
"Custom theme URL": "Custom theme URL", "Custom theme URL": "Custom theme URL",
"Add theme": "Add theme", "Add theme": "Add theme",
"Theme": "Theme", "Theme": "Theme",
"Appearance": "Appearance",
"Flair": "Flair", "Flair": "Flair",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success", "Success": "Success",

View File

@ -14,38 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from './dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import SettingsStore, {SettingLevel} from './settings/SettingsStore'; import SettingsStore, {SettingLevel} from '../SettingsStore';
import IWatcher from "./Watcher";
import { toPx } from '../../utils/units';
export class FontWatcher { export class FontWatcher implements IWatcher {
static MIN_SIZE = 13; public static readonly MIN_SIZE = 13;
static MAX_SIZE = 20; public static readonly MAX_SIZE = 20;
private dispatcherRef: string;
constructor() { constructor() {
this._dispatcherRef = null; this.dispatcherRef = null;
} }
start() { public start() {
this._setRootFontSize(SettingsStore.getValue("fontSize")); this.setRootFontSize(SettingsStore.getValue("fontSize"));
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
stop() { public stop() {
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_onAction = (payload) => { private onAction = (payload) => {
if (payload.action === 'update-font-size') { if (payload.action === 'update-font-size') {
this._setRootFontSize(payload.size); this.setRootFontSize(payload.size);
} }
}; };
_setRootFontSize = (size) => { private setRootFontSize = (size) => {
const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE);
if (fontSize != size) { if (fontSize !== size) {
SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize);
} }
document.querySelector(":root").style.fontSize = fontSize + "px"; (<HTMLElement>document.querySelector(":root")).style.fontSize = toPx(fontSize);
}; };
} }

View File

@ -0,0 +1,138 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019, 2020 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 SettingsStore, { SettingLevel } from '../SettingsStore';
import dis from '../../dispatcher/dispatcher';
import { Action } from '../../dispatcher/actions';
import ThemeController from "../controllers/ThemeController";
import { setTheme } from "../../theme";
import { ActionPayload } from '../../dispatcher/payloads';
export default class ThemeWatcher {
// XXX: I think this is unused.
static _instance = null;
private themeWatchRef: string;
private systemThemeWatchRef: string;
private dispatcherRef: string;
private preferDark: MediaQueryList;
private preferLight: MediaQueryList;
private currentTheme: string;
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.currentTheme = this.getEffectiveTheme();
}
public start() {
this.themeWatchRef = SettingsStore.watchSetting("theme", null, this.onChange);
this.systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this.onChange);
if (this.preferDark.addEventListener) {
this.preferDark.addEventListener('change', this.onChange);
this.preferLight.addEventListener('change', this.onChange);
}
this.dispatcherRef = dis.register(this.onAction);
}
public stop() {
if (this.preferDark.addEventListener) {
this.preferDark.removeEventListener('change', this.onChange);
this.preferLight.removeEventListener('change', this.onChange);
}
SettingsStore.unwatchSetting(this.systemThemeWatchRef);
SettingsStore.unwatchSetting(this.themeWatchRef);
dis.unregister(this.dispatcherRef);
}
private onChange = () => {
this.recheck();
};
private onAction = (payload: ActionPayload) => {
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/riot-web/issues/11443
public recheck(forceTheme?: string) {
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 probablt 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) {
console.log("returning explicit system theme");
if (this.preferDark.matches) return 'dark';
if (this.preferLight.matches) return 'light';
}
// 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) {
console.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')) {
if (this.preferDark.matches) return 'dark';
if (this.preferLight.matches) return 'light';
}
console.log("returning theme value");
return SettingsStore.getValue('theme');
}
public isSystemThemeSupported() {
return this.preferDark.matches || this.preferLight.matches;
}
}

View File

@ -0,0 +1,20 @@
/*
Copyright 2020 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.
*/
export default interface IWatcher {
start(): void
stop(): void
}

View File

@ -19,114 +19,8 @@ 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/dispatcher"; import SettingsStore from "./settings/SettingsStore";
import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; import ThemeWatcher from "./settings/watchers/ThemeWatcher";
import ThemeController from "./settings/controllers/ThemeController";
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);
if (this._preferDark.addEventListener) {
this._preferDark.addEventListener('change', this._onChange);
this._preferLight.addEventListener('change', this._onChange);
}
this._dispatcherRef = dis.register(this._onAction);
}
stop() {
if (this._preferDark.addEventListener) {
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 added 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() {
// 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 probablt 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) {
console.log("returning explicit system theme");
if (this._preferDark.matches) return 'dark';
if (this._preferLight.matches) return 'light';
}
// 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) {
console.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')) {
if (this._preferDark.matches) return 'dark';
if (this._preferLight.matches) return 'light';
}
console.log("returning theme value");
return SettingsStore.getValue('theme');
}
isSystemThemeSupported() {
return this._preferDark.matches || this._preferLight.matches;
}
}
export function enumerateThemes() { export function enumerateThemes() {
const BUILTIN_THEMES = { const BUILTIN_THEMES = {