Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Simplify theme logic
Browse files Browse the repository at this point in the history
Signed-off-by: Elsie Hupp <[email protected]>
  • Loading branch information
elsiehupp committed Nov 10, 2021
1 parent 9141887 commit 0db7d1e
Show file tree
Hide file tree
Showing 10 changed files with 727 additions and 330 deletions.
6 changes: 1 addition & 5 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ interface IState {
pendingInitialSync?: boolean;
justRegistered?: boolean;
roomJustCreatedOpts?: IOpts;
forceTimeline?: boolean; // see props
}

@replaceableComponent("structures.MatrixChat")
Expand Down Expand Up @@ -878,7 +879,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}

this.setStateForNewView(newState);
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.notifyNewScreen('register');
}
Expand Down Expand Up @@ -1006,7 +1006,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
view: Views.WELCOME,
});
this.notifyNewScreen('welcome');
ThemeController.isLogin = true;
this.themeWatcher.recheck();
}

Expand All @@ -1016,7 +1015,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
...otherState,
});
this.notifyNewScreen('login');
ThemeController.isLogin = true;
this.themeWatcher.recheck();
}

Expand All @@ -1029,7 +1027,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
this.setPage(PageType.HomePage);
this.notifyNewScreen('home');
ThemeController.isLogin = false;
this.themeWatcher.recheck();
}

Expand Down Expand Up @@ -1287,7 +1284,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* Called when a new logged in session has started
*/
private async onLoggedIn() {
ThemeController.isLogin = false;
this.themeWatcher.recheck();
this.setStateForNewView({ view: Views.LOGGED_IN });
// If a specific screen is set to be shown after login, show that above
Expand Down
33 changes: 17 additions & 16 deletions src/components/structures/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore";
import { getCustomTheme } from "../../theme";
import { Theme } from '../../Theme';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import { getHomePageUrl } from "../../utils/pages";
Expand Down Expand Up @@ -69,6 +70,7 @@ type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
interface IState {
contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean;
isHighContrast: boolean;
selectedSpace?: Room;
pendingRoomJoin: Set<string>;
}
Expand All @@ -86,7 +88,8 @@ export default class UserMenu extends React.Component<IProps, IState> {

this.state = {
contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(),
isDarkTheme: ThemeWatcher.isDarkTheme(),
isHighContrast: ThemeWatcher.isHighContrast(),
pendingRoomJoin: new Set<string>(),
};

Expand Down Expand Up @@ -130,18 +133,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.forceUpdate(); // we don't have anything useful in state to update
};

private isUserOnDarkTheme(): boolean {
if (SettingsStore.getValue("use_system_theme")) {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
} else {
const theme = SettingsStore.getValue("theme");
if (theme.startsWith("custom-")) {
return getCustomTheme(theme.substring("custom-".length)).is_dark;
}
return theme === "dark";
}
}

private onProfileUpdate = async () => {
// the store triggered an update, so force a layout update. We don't
// have any state to store here for that to magically happen.
Expand All @@ -153,7 +144,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
};

private onThemeChanged = () => {
this.setState({ isDarkTheme: this.isUserOnDarkTheme() });
this.setState(
{
isDarkTheme: ThemeWatcher.isDarkTheme,
isHighContrast: ThemeWatcher.isHighContrast,
});
};

private onAction = (ev: ActionPayload) => {
Expand Down Expand Up @@ -221,7 +216,13 @@ export default class UserMenu extends React.Component<IProps, IState> {
// Disable system theme matching if the user hits this button
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);

const newTheme = this.state.isDarkTheme ? "light" : "dark";
let newTheme = this.state.isDarkTheme ? SettingsStore.getValue("light_theme") : SettingsStore.getValue("dark_theme");
if (this.state.isHighContrast) {
const hcTheme = new Theme(newTheme).highContrast;
if (hcTheme) {
newTheme = hcTheme;
}
}
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
};

Expand Down
64 changes: 64 additions & 0 deletions src/components/views/settings/ThemeChoicePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2021 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 React from 'react';
import { _t } from "../../../languageHandler";
import { Theme } from "../../../Theme";
import StyledCheckbox from '../elements/StyledCheckbox';
import { replaceableComponent } from "../../../utils/replaceableComponent";

@replaceableComponent("views.settings.tabs.user.ThemeChoicePanel")
export default class ThemeChoicePanel extends React.Component<IProps, IState> {
private renderHighContrastCheckbox(): React.ReactElement<HTMLDivElement> {
const theme = new Theme(this.state.theme);
if (
!this.state.useSystemTheme && (
theme.highContrast ||
theme.isHighContrast
)
) {
return <div>
<StyledCheckbox
checked={theme.isHighContrast}
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
>
{ _t( "Use high contrast" ) }
</StyledCheckbox>
</div>;
}
}

private highContrastThemeChanged(checked: boolean): void {
const theme = new Theme(this.state.theme)
let newTheme: string;
if (checked) {
newTheme = theme.highContrast;
} else {
newTheme = theme.nonHighContrast;
}
if (newTheme) {
this.onThemeChange(newTheme);
}
}

apparentSelectedThemeId() {
if (this.state.useSystemTheme) {
return undefined;
}
const nonHighContrast = new Theme(this.state.theme).nonHighContrast;
return nonHighContrast ? nonHighContrast : this.state.theme;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import LayoutSwitcher from "../../LayoutSwitcher";

import { logger } from "matrix-js-sdk/src/logger";

import ThemeChoicePanel from '../../ThemeChoicePanel';

interface IProps {
}

Expand Down Expand Up @@ -119,7 +121,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
}

private calculateThemeState(): IThemeState {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// We have to mirror the logic from ThemeWatcher.currentTheme() so we
// show the right values for things.

const themeChoice: string = SettingsStore.getValue("theme");
Expand Down
8 changes: 8 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: "light",
controller: new ThemeController(),
},
"dark_theme": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: "dark", // Must be "dark" for compatibility with, e.g., Jitsi
},
"light_theme": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: "light", // Must be "light" for compatibility with, e.g., Jitsi
},
"custom_themes": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: [],
Expand Down
112 changes: 107 additions & 5 deletions src/settings/controllers/ThemeController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { CustomTheme } from "../../Theme"
import SettingController from "./SettingController";
import { DEFAULT_THEME, enumerateThemes } from "../../theme";
import { SettingLevel } from "../SettingLevel";
import ThemeWatcher from "../watchers/ThemeWatcher"

export default class ThemeController extends SettingController {
public static isLogin = false;

public constructor() {
super()
}

public getValueOverride(
level: SettingLevel,
Expand All @@ -30,14 +34,112 @@ export default class ThemeController extends SettingController {
): any {
if (!calculatedValue) return null; // Don't override null themes

if (ThemeController.isLogin) return 'light';
const themes = ThemeWatcher.availableThemes;

const themes = enumerateThemes();
// Override in case some no longer supported theme is stored here
if (!themes[calculatedValue]) {
return DEFAULT_THEME;
return ThemeWatcher.currentTheme();
}

return null; // no override
}

/**
* Called whenever someone changes the theme
* Async function that returns once the theme has been set
* (ie. the CSS has been loaded)
*
* @param {string} theme new theme
*/
public static async setTheme(theme?: string): Promise<void> {
if (!theme) {
theme = ThemeWatcher.currentTheme();
}
ThemeController.clearCustomTheme();
let stylesheetName = theme;
if (theme.startsWith("custom-")) {
stylesheetName = new CustomTheme(theme.substr(7)).is_dark ? "dark-custom" : "light-custom";
}

// look for the stylesheet elements.
// styleElements is a map from style name to HTMLLinkElement.
const styleElements = Object.create(null);
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
themes.forEach(theme => {
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
});

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;

return new Promise((resolve) => {
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/element-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: HTMLStyleElement) => {
if (a == styleElements[stylesheetName]) return;
a.disabled = true;
});
const bodyStyles = global.getComputedStyle(document.body);
if (bodyStyles.backgroundColor) {
const metaElement: HTMLMetaElement = document.querySelector('meta[name="theme-color"]');
metaElement.content = bodyStyles.backgroundColor;
}
resolve();
};

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

private static clearCustomTheme(): void {
// remove all css variables, we assume these are there because of the custom theme
const inlineStyleProps = Object.values(document.body.style);
for (const prop of inlineStyleProps) {
if (prop.startsWith("--")) {
document.body.style.removeProperty(prop);
}
}
const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']");
if (customFontFaceStyle) {
customFontFaceStyle.remove();
}
}
}
Loading

0 comments on commit 0db7d1e

Please sign in to comment.