{this._renderDiscoverySection()}
{this._renderIntegrationManagerSection() /* Has its own title */}
diff --git a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js
new file mode 100644
index 0000000000..5c68f214d4
--- /dev/null
+++ b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js
@@ -0,0 +1,233 @@
+/*
+Copyright 2019 New Vector Ltd
+
+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 {_t} from "../../../../../languageHandler";
+import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
+import * as sdk from "../../../../../index";
+import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
+import Field from "../../../elements/Field";
+
+export default class StyleUserSettingsTab extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ fontSize: SettingsStore.getValue("font_size", null),
+ ...this._calculateThemeState(),
+ customThemeUrl: "",
+ customThemeMessage: {isError: false, text: ""},
+ }
+ }
+
+ _calculateThemeState() {
+ // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
+ // show the right values for things.
+
+ const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
+ const systemThemeExplicit = SettingsStore.getValueAt(
+ SettingLevel.DEVICE, "use_system_theme", null, false, true);
+ const themeExplicit = 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,
+ };
+ }
+
+ // Otherwise assume the defaults for the settings
+ return {
+ theme: themeChoice,
+ useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
+ };
+ }
+
+ _onThemeChange = (e) => {
+ const newTheme = e.target.value;
+ if (this.state.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.ACCOUNT, newTheme).catch(() => {
+ 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
+ // 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});
+ SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
+ dis.dispatch({action: 'recheck_theme'});
+ };
+
+ _onFontSizeChanged = (size) => {
+ let parsed_size = isNaN(parseInt(size))?SettingsStore.getDefaultValue("font_size"):parseFloat(size);
+ this.setState({fontSize: parsed_size})
+ SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, parsed_size)
+ };
+
+ _onAddCustomTheme = async () => {
+ let currentThemes = 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);
+ const themeInfo = await r.json();
+ if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
+ this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
+ return;
+ }
+ currentThemes.push(themeInfo);
+ } catch (e) {
+ console.error(e);
+ this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}});
+ return; // Don't continue on error
+ }
+
+ await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
+ this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
+
+ this._themeTimer = setTimeout(() => {
+ this.setState({customThemeMessage: {text: "", isError: false}});
+ }, 3000);
+ };
+
+ _onCustomThemeChange = (e) => {
+ this.setState({customThemeUrl: e.target.value});
+ };
+
+ render() {
+ return (
+
+ }
+
+}
diff --git a/src/theme.js b/src/theme.js
index 442a89e25f..3309acdd01 100644
--- a/src/theme.js
+++ b/src/theme.js
@@ -81,7 +81,7 @@ export class ThemeWatcher {
}
getEffectiveTheme() {
- // Dev note: Much of this logic is replicated in the GeneralUserSettingsTab
+ // Dev note: Much of this logic is replicated in the StyleUserSettingsTab
// 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