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)"), -};