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

Commit 73581ed

Browse files
committed
Merge upstream
Signed-off-by: Elsie Hupp <9206310+elsiehupp@users.noreply.github.com>
2 parents 74376f9 + 0db7d1e commit 73581ed

File tree

9 files changed

+484
-432
lines changed

9 files changed

+484
-432
lines changed

src/components/structures/MatrixChat.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
891891
}
892892

893893
this.setStateForNewView(newState);
894-
ThemeController.isLogin = true;
895894
this.themeWatcher.recheck();
896895
this.notifyNewScreen('register');
897896
}
@@ -1020,7 +1019,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
10201019
view: Views.WELCOME,
10211020
});
10221021
this.notifyNewScreen('welcome');
1023-
ThemeController.isLogin = true;
10241022
this.themeWatcher.recheck();
10251023
}
10261024

@@ -1030,7 +1028,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
10301028
...otherState,
10311029
});
10321030
this.notifyNewScreen('login');
1033-
ThemeController.isLogin = true;
10341031
this.themeWatcher.recheck();
10351032
}
10361033

@@ -1043,7 +1040,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
10431040
});
10441041
this.setPage(PageType.HomePage);
10451042
this.notifyNewScreen('home');
1046-
ThemeController.isLogin = false;
10471043
this.themeWatcher.recheck();
10481044
}
10491045

@@ -1301,7 +1297,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
13011297
* Called when a new logged in session has started
13021298
*/
13031299
private async onLoggedIn() {
1304-
ThemeController.isLogin = false;
13051300
this.themeWatcher.recheck();
13061301
this.setStateForNewView({ view: Views.LOGGED_IN });
13071302
// If a specific screen is set to be shown after login, show that above

src/components/structures/UserMenu.tsx

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import FeedbackDialog from "../views/dialogs/FeedbackDialog";
3232
import Modal from "../../Modal";
3333
import LogoutDialog from "../views/dialogs/LogoutDialog";
3434
import SettingsStore from "../../settings/SettingsStore";
35-
import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme";
35+
import { Theme } from '../../Theme';
36+
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
3637
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
3738
import SdkConfig from "../../SdkConfig";
3839
import { getHomePageUrl } from "../../utils/pages";
@@ -87,8 +88,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
8788

8889
this.state = {
8990
contextMenuPosition: null,
90-
isDarkTheme: this.isUserOnDarkTheme(),
91-
isHighContrast: this.isUserOnHighContrastTheme(),
91+
isDarkTheme: ThemeWatcher.isDarkTheme(),
92+
isHighContrast: ThemeWatcher.isHighContrast(),
9293
pendingRoomJoin: new Set<string>(),
9394
};
9495

@@ -132,30 +133,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
132133
this.forceUpdate(); // we don't have anything useful in state to update
133134
};
134135

135-
private isUserOnDarkTheme(): boolean {
136-
if (SettingsStore.getValue("use_system_theme")) {
137-
return window.matchMedia("(prefers-color-scheme: dark)").matches;
138-
} else {
139-
const theme = SettingsStore.getValue("theme");
140-
if (theme.startsWith("custom-")) {
141-
return getCustomTheme(theme.substring("custom-".length)).is_dark;
142-
}
143-
return theme === "dark";
144-
}
145-
}
146-
147-
private isUserOnHighContrastTheme(): boolean {
148-
if (SettingsStore.getValue("use_system_theme")) {
149-
return window.matchMedia("(prefers-contrast: more)").matches;
150-
} else {
151-
const theme = SettingsStore.getValue("theme");
152-
if (theme.startsWith("custom-")) {
153-
return false;
154-
}
155-
return isHighContrastTheme(theme);
156-
}
157-
}
158-
159136
private onProfileUpdate = async () => {
160137
// the store triggered an update, so force a layout update. We don't
161138
// have any state to store here for that to magically happen.
@@ -169,8 +146,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
169146
private onThemeChanged = () => {
170147
this.setState(
171148
{
172-
isDarkTheme: this.isUserOnDarkTheme(),
173-
isHighContrast: this.isUserOnHighContrastTheme(),
149+
isDarkTheme: ThemeWatcher.isDarkTheme(),
150+
isHighContrast: ThemeWatcher.isHighContrast(),
174151
});
175152
};
176153

@@ -239,9 +216,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
239216
// Disable system theme matching if the user hits this button
240217
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
241218

242-
let newTheme = this.state.isDarkTheme ? "light" : "dark";
219+
let newTheme = this.state.isDarkTheme ? SettingsStore.getValue("light_theme") : SettingsStore.getValue("dark_theme");
243220
if (this.state.isHighContrast) {
244-
const hcTheme = findHighContrastTheme(newTheme);
221+
const hcTheme = new Theme(newTheme).highContrast;
245222
if (hcTheme) {
246223
newTheme = hcTheme;
247224
}

src/components/views/settings/ThemeChoicePanel.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
import React from 'react';
1818
import { _t } from "../../../languageHandler";
1919
import SettingsStore from "../../../settings/SettingsStore";
20-
import { enumerateThemes, findHighContrastTheme, findNonHighContrastTheme, isHighContrastTheme } from "../../../theme";
20+
import { Theme } from "../../../theme";
2121
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
2222
import AccessibleButton from "../elements/AccessibleButton";
2323
import dis from "../../../dispatcher/dispatcher";
@@ -162,13 +162,13 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
162162
private renderHighContrastCheckbox(): React.ReactElement<HTMLDivElement> {
163163
if (
164164
!this.state.useSystemTheme && (
165-
findHighContrastTheme(this.state.theme) ||
166-
isHighContrastTheme(this.state.theme)
165+
// Theme.findHighContrast(this.state.theme) ||
166+
ThemeWatcher.isHighContrast()
167167
)
168168
) {
169169
return <div>
170170
<StyledCheckbox
171-
checked={isHighContrastTheme(this.state.theme)}
171+
checked={ThemeWatcher.isHighContrast()}
172172
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
173173
>
174174
{ _t( "Use high contrast" ) }
@@ -178,21 +178,21 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
178178
}
179179

180180
private highContrastThemeChanged(checked: boolean): void {
181+
const theme = new Theme(this.state.theme)
181182
let newTheme: string;
182183
if (checked) {
183-
newTheme = findHighContrastTheme(this.state.theme);
184+
newTheme = theme.highContrast;
184185
} else {
185-
newTheme = findNonHighContrastTheme(this.state.theme);
186+
newTheme = theme.nonHighContrast;
186187
}
187188
if (newTheme) {
188189
this.onThemeChange(newTheme);
189190
}
190191
}
191192

192193
public render(): React.ReactElement<HTMLDivElement> {
193-
const themeWatcher = new ThemeWatcher();
194194
let systemThemeSection: JSX.Element;
195-
if (themeWatcher.isSystemThemeSupported()) {
195+
if (ThemeWatcher.supportsSystemTheme()) {
196196
systemThemeSection = <div>
197197
<StyledCheckbox
198198
checked={this.state.useSystemTheme}
@@ -241,7 +241,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
241241
// XXX: replace any type here
242242
const themes = Object.entries<any>(enumerateThemes())
243243
.map(p => ({ id: p[0], name: p[1] })) // convert pairs to objects for code readability
244-
.filter(p => !isHighContrastTheme(p.id));
244+
.filter(p => !ThemeWatcher.isHighContrast());
245245
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
246246
const customThemes = themes.filter(p => !builtInThemes.includes(p))
247247
.sort((a, b) => compare(a.name, b.name));

src/settings/Settings.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,14 @@ export const SETTINGS: {[setting: string]: ISetting} = {
459459
default: "light",
460460
controller: new ThemeController(),
461461
},
462+
"dark_theme": {
463+
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
464+
default: "dark", // Must be "dark" for compatibility with, e.g., Jitsi
465+
},
466+
"light_theme": {
467+
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
468+
default: "light", // Must be "light" for compatibility with, e.g., Jitsi
469+
},
462470
"custom_themes": {
463471
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
464472
default: [],

src/settings/controllers/ThemeController.ts

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18+
import { CustomTheme } from "../../Theme"
1819
import SettingController from "./SettingController";
19-
import { DEFAULT_THEME, enumerateThemes } from "../../theme";
2020
import { SettingLevel } from "../SettingLevel";
21+
import ThemeWatcher from "../watchers/ThemeWatcher"
2122

2223
export default class ThemeController extends SettingController {
23-
public static isLogin = false;
24+
25+
public constructor() {
26+
super()
27+
}
2428

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

33-
if (ThemeController.isLogin) return 'light';
37+
const themes = ThemeWatcher.availableThemes;
3438

35-
const themes = enumerateThemes();
3639
// Override in case some no longer supported theme is stored here
3740
if (!themes[calculatedValue]) {
38-
return DEFAULT_THEME;
41+
return ThemeWatcher.currentTheme();
3942
}
4043

4144
return null; // no override
4245
}
46+
47+
/**
48+
* Called whenever someone changes the theme
49+
* Async function that returns once the theme has been set
50+
* (ie. the CSS has been loaded)
51+
*
52+
* @param {string} theme new theme
53+
*/
54+
public static async setTheme(theme?: string): Promise<void> {
55+
if (!theme) {
56+
theme = ThemeWatcher.currentTheme();
57+
}
58+
ThemeController.clearCustomTheme();
59+
let stylesheetName = theme;
60+
if (theme.startsWith("custom-")) {
61+
stylesheetName = new CustomTheme(theme.substr(7)).is_dark ? "dark-custom" : "light-custom";
62+
}
63+
64+
// look for the stylesheet elements.
65+
// styleElements is a map from style name to HTMLLinkElement.
66+
const styleElements = Object.create(null);
67+
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
68+
themes.forEach(theme => {
69+
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
70+
});
71+
72+
if (!(stylesheetName in styleElements)) {
73+
throw new Error("Unknown theme " + stylesheetName);
74+
}
75+
76+
// disable all of them first, then enable the one we want. Chrome only
77+
// bothers to do an update on a true->false transition, so this ensures
78+
// that we get exactly one update, at the right time.
79+
//
80+
// ^ This comment was true when we used to use alternative stylesheets
81+
// for the CSS. Nowadays we just set them all as disabled in index.html
82+
// and enable them as needed. It might be cleaner to disable them all
83+
// at the same time to prevent loading two themes simultaneously and
84+
// having them interact badly... but this causes a flash of unstyled app
85+
// which is even uglier. So we don't.
86+
87+
styleElements[stylesheetName].disabled = false;
88+
89+
return new Promise((resolve) => {
90+
const switchTheme = function() {
91+
// we re-enable our theme here just in case we raced with another
92+
// theme set request as per https://github.com/vector-im/element-web/issues/5601.
93+
// We could alternatively lock or similar to stop the race, but
94+
// this is probably good enough for now.
95+
styleElements[stylesheetName].disabled = false;
96+
Object.values(styleElements).forEach((a: HTMLStyleElement) => {
97+
if (a == styleElements[stylesheetName]) return;
98+
a.disabled = true;
99+
});
100+
const bodyStyles = global.getComputedStyle(document.body);
101+
if (bodyStyles.backgroundColor) {
102+
const metaElement: HTMLMetaElement = document.querySelector('meta[name="theme-color"]');
103+
metaElement.content = bodyStyles.backgroundColor;
104+
}
105+
resolve();
106+
};
107+
108+
// turns out that Firefox preloads the CSS for link elements with
109+
// the disabled attribute, but Chrome doesn't.
110+
111+
let cssLoaded = false;
112+
113+
styleElements[stylesheetName].onload = () => {
114+
switchTheme();
115+
};
116+
117+
for (let i = 0; i < document.styleSheets.length; i++) {
118+
const ss = document.styleSheets[i];
119+
if (ss && ss.href === styleElements[stylesheetName].href) {
120+
cssLoaded = true;
121+
break;
122+
}
123+
}
124+
125+
if (cssLoaded) {
126+
styleElements[stylesheetName].onload = undefined;
127+
switchTheme();
128+
}
129+
});
130+
}
131+
132+
private static clearCustomTheme(): void {
133+
// remove all css variables, we assume these are there because of the custom theme
134+
const inlineStyleProps = Object.values(document.body.style);
135+
for (const prop of inlineStyleProps) {
136+
if (prop.startsWith("--")) {
137+
document.body.style.removeProperty(prop);
138+
}
139+
}
140+
const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']");
141+
if (customFontFaceStyle) {
142+
customFontFaceStyle.remove();
143+
}
144+
}
43145
}

0 commit comments

Comments
 (0)