diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index a185664038..9392a14ad4 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -56,6 +56,7 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
+import { setTheme } from "../../theme";
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
@@ -658,7 +659,7 @@ export default createReactClass({
break;
}
case 'set_theme':
- this._onSetTheme(payload.value);
+ setTheme(payload.value);
break;
case 'on_logging_in':
// We are now logging in, so set the state to reflect that
@@ -1102,82 +1103,6 @@ export default createReactClass({
});
},
- /**
- * Called whenever someone changes the theme
- *
- * @param {string} theme new theme
- */
- _onSetTheme: function(theme) {
- if (!theme) {
- theme = SettingsStore.getValue("theme");
- }
-
- // look for the stylesheet elements.
- // styleElements is a map from style name to HTMLLinkElement.
- const styleElements = Object.create(null);
- let a;
- for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
- const href = a.getAttribute("href");
- // shouldn't we be using the 'title' tag rather than the href?
- const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
- if (match) {
- styleElements[match[1]] = a;
- }
- }
-
- if (!(theme in styleElements)) {
- throw new Error("Unknown theme " + theme);
- }
-
- // disable all of them first, then enable the one we want. Chrome only
- // bothers to do an update on a true->false transition, so this ensures
- // that we get exactly one update, at the right time.
- //
- // ^ This comment was true when we used to use alternative stylesheets
- // for the CSS. Nowadays we just set them all as disabled in index.html
- // and enable them as needed. It might be cleaner to disable them all
- // at the same time to prevent loading two themes simultaneously and
- // having them interact badly... but this causes a flash of unstyled app
- // which is even uglier. So we don't.
-
- styleElements[theme].disabled = false;
-
- const switchTheme = function() {
- // we re-enable our theme here just in case we raced with another
- // theme set request as per https://github.com/vector-im/riot-web/issues/5601.
- // We could alternatively lock or similar to stop the race, but
- // this is probably good enough for now.
- styleElements[theme].disabled = false;
- Object.values(styleElements).forEach((a) => {
- if (a == styleElements[theme]) return;
- a.disabled = true;
- });
- Tinter.setTheme(theme);
- };
-
- // turns out that Firefox preloads the CSS for link elements with
- // the disabled attribute, but Chrome doesn't.
-
- let cssLoaded = false;
-
- styleElements[theme].onload = () => {
- switchTheme();
- };
-
- for (let i = 0; i < document.styleSheets.length; i++) {
- const ss = document.styleSheets[i];
- if (ss && ss.href === styleElements[theme].href) {
- cssLoaded = true;
- break;
- }
- }
-
- if (cssLoaded) {
- styleElements[theme].onload = undefined;
- switchTheme();
- }
- },
-
/**
* Starts a chat with the welcome user, if the user doesn't already have one
* @returns {string} The room ID of the new room, or null if no room was created
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index b9c566b22a..64aafe6046 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -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 {THEMES} from "../../../../../themes";
+import {enumerateThemes} from "../../../../../theme";
import PlatformPeg from "../../../../../PlatformPeg";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../..";
@@ -275,8 +275,8 @@ export default class GeneralUserSettingsTab extends React.Component {
{_t("Theme")}
- {Object.entries(THEMES).map(([theme, text]) => {
- return ;
+ {Object.entries(enumerateThemes()).map(([theme, text]) => {
+ return ;
})}
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index e0ff16c538..c82b7a0c71 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -245,6 +245,10 @@ export const SETTINGS = {
default: "light",
controller: new ThemeController(),
},
+ "custom_themes": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ default: [],
+ },
"webRtcAllowPeerToPeer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Allow Peer-to-Peer for 1:1 calls'),
diff --git a/src/settings/controllers/ThemeController.js b/src/settings/controllers/ThemeController.js
index da20521873..a15b4e78cd 100644
--- a/src/settings/controllers/ThemeController.js
+++ b/src/settings/controllers/ThemeController.js
@@ -16,12 +16,13 @@ limitations under the License.
*/
import SettingController from "./SettingController";
-import {DEFAULT_THEME, THEMES} from "../../themes";
+import {DEFAULT_THEME, enumerateThemes} from "../../theme";
export default class ThemeController extends SettingController {
getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) {
+ const themes = enumerateThemes();
// Override in case some no longer supported theme is stored here
- if (!THEMES[calculatedValue]) {
+ if (!themes[calculatedValue]) {
return DEFAULT_THEME;
}
diff --git a/src/theme.js b/src/theme.js
new file mode 100644
index 0000000000..c2baca67da
--- /dev/null
+++ b/src/theme.js
@@ -0,0 +1,139 @@
+/*
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+
+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 {_t} from "./languageHandler";
+
+export const DEFAULT_THEME = "light";
+import Tinter from "./Tinter";
+import SettingsStore from "./settings/SettingsStore";
+
+export function enumerateThemes() {
+ const BUILTIN_THEMES = {
+ "light": _t("Light theme"),
+ "dark": _t("Dark theme"),
+ };
+ const customThemes = SettingsStore.getValue("custom_themes");
+ const customThemeNames = {};
+ for (const {name} of customThemes) {
+ customThemeNames[`custom-${name}`] = name;
+ }
+ console.log("customThemeNames", customThemeNames);
+ return Object.assign({}, customThemeNames, BUILTIN_THEMES);
+}
+
+function setCustomThemeVars(themeName) {
+ // set css variables
+ const customThemes = SettingsStore.getValue("custom_themes");
+ if (!customThemes) {
+ throw new Error(`No custom themes set, can't set custom theme "${themeName}"`);
+ }
+ const customTheme = customThemes.find(t => t.name === themeName);
+ if (!customTheme) {
+ const knownNames = customThemes.map(t => t.name).join(", ");
+ throw new Error(`Can't find custom theme "${themeName}", only know ${knownNames}`);
+ }
+ const {style} = document.body;
+ if (customTheme.colors) {
+ for (const [name, hexColor] of Object.entries(customTheme.colors)) {
+ style.setProperty(`--${name}`, hexColor);
+ // uses #rrggbbaa to define the color with alpha values at 0% and 50%
+ style.setProperty(`--${name}-0pct`, hexColor + "00");
+ style.setProperty(`--${name}-50pct`, hexColor + "7F");
+ }
+ }
+}
+
+/**
+ * Called whenever someone changes the theme
+ *
+ * @param {string} theme new theme
+ */
+export function setTheme(theme) {
+ if (!theme) {
+ theme = SettingsStore.getValue("theme");
+ }
+ let stylesheetName = theme;
+ if (theme.startsWith("custom-")) {
+ stylesheetName = "light-custom";
+ const themeName = theme.substr(7);
+ setCustomThemeVars(themeName);
+ }
+
+ // look for the stylesheet elements.
+ // styleElements is a map from style name to HTMLLinkElement.
+ const styleElements = Object.create(null);
+ let a;
+ for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
+ const href = a.getAttribute("href");
+ // shouldn't we be using the 'title' tag rather than the href?
+ const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
+ if (match) {
+ styleElements[match[1]] = a;
+ }
+ }
+
+ if (!(stylesheetName in styleElements)) {
+ throw new Error("Unknown theme " + stylesheetName);
+ }
+
+ // disable all of them first, then enable the one we want. Chrome only
+ // bothers to do an update on a true->false transition, so this ensures
+ // that we get exactly one update, at the right time.
+ //
+ // ^ This comment was true when we used to use alternative stylesheets
+ // for the CSS. Nowadays we just set them all as disabled in index.html
+ // and enable them as needed. It might be cleaner to disable them all
+ // at the same time to prevent loading two themes simultaneously and
+ // having them interact badly... but this causes a flash of unstyled app
+ // which is even uglier. So we don't.
+
+ styleElements[stylesheetName].disabled = false;
+
+ const switchTheme = function() {
+ // we re-enable our theme here just in case we raced with another
+ // theme set request as per https://github.com/vector-im/riot-web/issues/5601.
+ // We could alternatively lock or similar to stop the race, but
+ // this is probably good enough for now.
+ styleElements[stylesheetName].disabled = false;
+ Object.values(styleElements).forEach((a) => {
+ if (a == styleElements[stylesheetName]) return;
+ a.disabled = true;
+ });
+ Tinter.setTheme(theme);
+ };
+
+ // turns out that Firefox preloads the CSS for link elements with
+ // the disabled attribute, but Chrome doesn't.
+
+ let cssLoaded = false;
+
+ styleElements[stylesheetName].onload = () => {
+ switchTheme();
+ };
+
+ for (let i = 0; i < document.styleSheets.length; i++) {
+ const ss = document.styleSheets[i];
+ if (ss && ss.href === styleElements[stylesheetName].href) {
+ cssLoaded = true;
+ break;
+ }
+ }
+
+ if (cssLoaded) {
+ styleElements[stylesheetName].onload = undefined;
+ switchTheme();
+ }
+}
diff --git a/src/themes.js b/src/themes.js
deleted file mode 100644
index 2529a04d89..0000000000
--- a/src/themes.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-
-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 {_td} from "./languageHandler";
-
-export const DEFAULT_THEME = "light";
-
-export const THEMES = {
- "light": _td("Light theme"),
- "dark": _td("Dark theme"),
- "light-custom": _td("Custom theme (light)"),
-};