diff --git a/docs/settings.md b/docs/settings.md index 1ba8981f84..9b780c27c9 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -15,7 +15,7 @@ order of prioirty, are: * `room-account` - The current user's account, but only when in a specific room * `account` - The current user's account * `room` - A specific room (setting for all members of the room) -* `config` - Values are defined by `config.json` +* `config` - Values are defined by the `settingDefaults` key (usually) in `config.json` * `default` - The hardcoded default for the settings Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 3b03fe0a2f..fc61395bf9 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -70,10 +70,10 @@ limitations under the License. .mx_RoomSubList_badge { flex: 0 0 auto; border-radius: 8px; - color: $accent-fg-color; font-weight: 600; font-size: 12px; padding: 0 5px; + color: $roomtile-badge-fg-color; background-color: $roomtile-name-color; cursor: pointer; } @@ -104,6 +104,7 @@ limitations under the License. } .mx_RoomSubList_badgeHighlight { + color: $accent-fg-color; background-color: $warning-color; } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 25330973b6..2acddc233c 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -110,7 +110,7 @@ limitations under the License. flex: 0 1 content; border-radius: 0.8em; padding: 0 0.4em; - color: $accent-fg-color; + color: $roomtile-badge-fg-color; font-weight: 600; font-size: 12px; } @@ -156,6 +156,7 @@ limitations under the License. .mx_RoomTile_highlight .mx_RoomTile_badge, .mx_RoomTile_badge.mx_RoomTile_badgeRed { + color: $accent-fg-color; background-color: $warning-color; } diff --git a/res/themes/dark-custom/css/dark-custom.scss b/res/themes/dark-custom/css/dark-custom.scss new file mode 100644 index 0000000000..aff647ce26 --- /dev/null +++ b/res/themes/dark-custom/css/dark-custom.scss @@ -0,0 +1,6 @@ +@import "../../light/css/_paths.scss"; +@import "../../light/css/_fonts.scss"; +@import "../../light/css/_light.scss"; +@import "../../dark/css/_dark.scss"; +@import "../../light-custom/css/_custom.scss"; +@import "../../../../res/css/_components.scss"; diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss new file mode 100644 index 0000000000..e4a08277f9 --- /dev/null +++ b/res/themes/light-custom/css/_custom.scss @@ -0,0 +1,126 @@ +/* +Copyright 2019 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. +*/ + +// +// --accent-color +$accent-color: var(--accent-color); +$button-bg-color: var(--accent-color); +$button-link-fg-color: var(--accent-color); +$button-primary-bg-color: var(--accent-color); +$input-valid-border-color: var(--accent-color); +$reaction-row-button-selected-border-color: var(--accent-color); +$roomsublist-chevron-color: var(--accent-color); +$tab-label-active-bg-color: var(--accent-color); +$togglesw-on-color: var(--accent-color); +$username-variant3-color: var(--accent-color); +$accent-color-50pct: var(--accent-color-50pct); //still needs alpha at .5 +// +// --timeline-background-color +$authpage-body-bg-color: var(--timeline-background-color); +$button-secondary-bg-color: var(--timeline-background-color); +$field-focused-label-bg-color: var(--timeline-background-color); +$lightbox-border-color: var(--timeline-background-color); +$menu-bg-color: var(--timeline-background-color); +$avatar-bg-color: var(--timeline-background-color); +$message-action-bar-bg-color: var(--timeline-background-color); +$primary-bg-color: var(--timeline-background-color); +$roomtile-focused-bg-color: var(--timeline-background-color); +$togglesw-ball-color: var(--timeline-background-color); +$droptarget-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .5 +$authpage-modal-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .59 +// +// --roomlist-highlights-color +$roomtile-selected-bg-color: var(--roomlist-highlights-color); +// +// --sidebar-color +$interactive-tooltip-bg-color: var(--sidebar-color); +$tagpanel-bg-color: var(--sidebar-color); +$tooltip-timeline-bg-color: var(--sidebar-color); +$dialog-backdrop-color: var(--sidebar-color-50pct); +// +// --roomlist-background-color +$event-selected-color: var(--roomlist-background-color); +$header-panel-bg-color: var(--roomlist-background-color); +$reaction-row-button-bg-color: var(--roomlist-background-color); +$panel-gradient: var(--roomlist-background-color-0pct), var(--roomlist-background-color); +// these were #f2f5f8 instead of #f3f8fd, but close enough +$dark-panel-bg-color: var(--roomlist-background-color); +$input-lighter-bg-color: var(--roomlist-background-color); +$plinth-bg-color: var(--roomlist-background-color); +$roomsublist-background: var(--roomlist-background-color); +$secondary-accent-color: var(--roomlist-background-color); +$selected-color: var(--roomlist-background-color); +$widget-menu-bar-bg-color: var(--roomlist-background-color); +$roomtile-badge-fg-color: var(--roomlist-background-color); +// +// --timeline-text-color +$message-action-bar-fg-color: var(--timeline-text-color); +$primary-fg-color: var(--timeline-text-color); +$settings-profile-overlay-placeholder-fg-color: var(--timeline-text-color); +$roomtopic-color: var(--timeline-text-color-50pct); +$tab-label-fg-color: var(--timeline-text-color); +$tab-label-icon-bg-color: var(--timeline-text-color); //was #454545 +// was #212121 +$topleftmenu-color: var(--timeline-text-color); +// was #45474a +$dialog-title-fg-color: var(--timeline-text-color); +$tab-label-fg-color: var(--timeline-text-color); +// was #4e5054 +$authpage-lang-color: var(--timeline-text-color); +$roomheader-color: var(--timeline-text-color); +// +// --roomlist-text-color +$roomtile-notified-color: var(--roomlist-text-color); +$roomtile-selected-color: var(--roomlist-text-color); +// +// --roomlist-text-secondary-color +$roomsublist-label-fg-color: var(--roomlist-text-secondary-color); +$roomtile-name-color: var(--roomlist-text-secondary-color); +// +// --roomlist-separator-color +$input-darker-bg-color: var(--roomlist-separator-color); +$panel-divider-color: var(--roomlist-separator-color);// originally #dee1f3, but close enough +$primary-hairline-color: var(--roomlist-separator-color);// originally #e5e5e5, but close enough +// +// --timeline-text-secondary-color +$authpage-secondary-color: var(--timeline-text-secondary-color); +$memberstatus-placeholder-color: var(--timeline-text-secondary-color); +$notice-secondary-color: var(--timeline-text-secondary-color); +$pinned-color: var(--timeline-text-secondary-color); +$settings-subsection-fg-color: var(--timeline-text-secondary-color); +$roomheader-addroom-bg-color: var(--timeline-text-secondary-color); +// was #747474 +$light-fg-color: var(--timeline-text-secondary-color); +// was #777777 +$blockquote-fg-color: var(--timeline-text-secondary-color); +// was #888888 +$greyed-fg-color: var(--timeline-text-secondary-color); +$info-plinth-fg-color: var(--timeline-text-secondary-color); +$preview-widget-fg-color: var(--timeline-text-secondary-color); +// +// --primary-color +$accent-color-alt: var(--primary-color); +$input-focused-border-color: var(--primary-color); +// +// --warning-color +$button-danger-bg-color: var(--warning-color); +$event-highlight-fg-color: var(--warning-color); +$input-invalid-border-color: var(--warning-color); +$mention-user-pill-bg-color: var(--warning-color); +$notice-primary-color: var(--warning-color); +$pinned-unread-color: var(--warning-color); +$warning-color: var(--warning-color); +$button-danger-disabled-bg-color: var(--warning-color-50pct); // still needs alpha at 0.5 diff --git a/res/themes/light-custom/css/light-custom.scss b/res/themes/light-custom/css/light-custom.scss new file mode 100644 index 0000000000..278ca5f0b1 --- /dev/null +++ b/res/themes/light-custom/css/light-custom.scss @@ -0,0 +1,5 @@ +@import "../../light/css/_paths.scss"; +@import "../../light/css/_fonts.scss"; +@import "../../light/css/_light.scss"; +@import "_custom.scss"; +@import "../../../../res/css/_components.scss"; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index db26c99155..b412261d10 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -167,6 +167,7 @@ $header-divider-color: #91A1C0; // ******************** $roomtile-name-color: #61708b; +$roomtile-badge-fg-color: $accent-fg-color; $roomtile-selected-color: #212121; $roomtile-notified-color: #212121; $roomtile-selected-bg-color: #fff; @@ -234,7 +235,7 @@ $tab-label-active-fg-color: #ffffff; $tab-label-bg-color: transparent; $tab-label-active-bg-color: $accent-color; $tab-label-icon-bg-color: #454545; -$tab-label-active-icon-bg-color: #ffffff; +$tab-label-active-icon-bg-color: $tab-label-active-fg-color; // Buttons $button-primary-fg-color: #ffffff; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6a03941bd9..2da219a28d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -59,6 +59,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. @@ -661,7 +662,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 @@ -1105,82 +1106,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..d479170792 --- /dev/null +++ b/src/theme.js @@ -0,0 +1,143 @@ +/* +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 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 {_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; + } + return Object.assign({}, customThemeNames, BUILTIN_THEMES); +} + +function setCustomThemeVars(customTheme) { + 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"); + } + } +} + +function getCustomTheme(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}`); + } + return customTheme; +} + +/** + * 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-")) { + const customTheme = getCustomTheme(theme.substr(7)); + stylesheetName = customTheme.is_dark ? "dark-custom" : "light-custom"; + setCustomThemeVars(customTheme); + } + + // 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 1896333844..0000000000 --- a/src/themes.js +++ /dev/null @@ -1,24 +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"), -};