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