Merge pull request #3503 from matrix-org/bwindels/custom-themes-mvp

Custom theming MVP
pull/21833/head
Bruno Windels 2019-10-02 09:14:07 +00:00 committed by GitHub
commit c8c4dc29d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 299 additions and 110 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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

View File

@ -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";

View File

@ -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;

View File

@ -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

View File

@ -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 {
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}>
{Object.entries(THEMES).map(([theme, text]) => {
return <option key={theme} value={theme}>{_t(text)}</option>;
{Object.entries(enumerateThemes()).map(([theme, text]) => {
return <option key={theme} value={theme}>{text}</option>;
})}
</Field>
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />

View File

@ -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'),

View File

@ -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;
}

143
src/theme.js Normal file
View File

@ -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();
}
}

View File

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