(roomMemberStore.openCursor(memberRange), member => {
- if (member.membership === "join") {
- joinedUserIds.push(member.userId);
- }
- return NOT_DONE;
- });
- log.set("joinedUserIds", joinedUserIds.length);
- for (const userId of joinedUserIds) {
- const identity = await reqAsPromise(userIdentitiesStore.get(userId));
- const originalRoomCount = identity?.roomIds?.length;
- const updatedIdentity = addRoomToIdentity(identity, userId, roomId);
- if (updatedIdentity) {
- log.log({l: `fixing up`, id: userId,
- roomsBefore: originalRoomCount, roomsAfter: updatedIdentity.roomIds.length});
- userIdentitiesStore.put(updatedIdentity);
- foundMissing = true;
- }
- }
- log.set("foundMissing", foundMissing);
- if (foundMissing) {
- // clear outbound megolm session,
- // so we'll create a new one on the next message that will be properly shared
- outboundGroupSessionsStore.delete(roomId);
- }
- });
- }
-}
+//v11 doesn't change the schema,
+// but ensured all userIdentities have all the roomIds they should (see #470)
+
+// 2022-07-20: The fix dated from August 2021, and have removed it now because of a
+// refactoring needed in the device tracker, which made it inconvenient to expose addRoomToIdentity
+function fixMissingRoomsInUserIdentities() {}
// v12 move ssssKey to e2ee:ssssKey so it will get backed up in the next step
async function changeSSSSKeyPrefix(db: IDBDatabase, txn: IDBTransaction) {
diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js
index 00c91f27..9a858f2b 100644
--- a/src/matrix/well-known.js
+++ b/src/matrix/well-known.js
@@ -41,6 +41,8 @@ async function getWellKnownResponse(homeserver, request) {
export async function lookupHomeserver(homeserver, request) {
homeserver = normalizeHomeserver(homeserver);
+ let issuer = null;
+ let account = null;
const wellKnownResponse = await getWellKnownResponse(homeserver, request);
if (wellKnownResponse && wellKnownResponse.status === 200) {
const {body} = wellKnownResponse;
@@ -48,6 +50,16 @@ export async function lookupHomeserver(homeserver, request) {
if (typeof wellKnownHomeserver === "string") {
homeserver = normalizeHomeserver(wellKnownHomeserver);
}
+
+ const wellKnownIssuer = body["org.matrix.msc2965.authentication"]?.["issuer"];
+ if (typeof wellKnownIssuer === "string") {
+ issuer = wellKnownIssuer;
+ }
+
+ const wellKnownAccount = body["org.matrix.msc2965.authentication"]?.["account"];
+ if (typeof wellKnownAccount === "string") {
+ account = wellKnownAccount;
+ }
}
- return homeserver;
+ return {homeserver, issuer, account};
}
diff --git a/src/observable/value/MappedObservableValue.ts b/src/observable/value/MappedObservableValue.ts
new file mode 100644
index 00000000..7105dfb0
--- /dev/null
+++ b/src/observable/value/MappedObservableValue.ts
@@ -0,0 +1,46 @@
+/*
+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 { BaseObservableValue } from "./BaseObservableValue";
+import { SubscriptionHandle } from "../BaseObservable";
+
+export class MappedObservableValue extends BaseObservableValue {
+ private sourceSubscription?: SubscriptionHandle;
+
+ constructor(
+ private readonly source: BaseObservableValue,
+ private readonly mapper: (value: P) => C
+ ) {
+ super();
+ }
+
+ onUnsubscribeLast() {
+ super.onUnsubscribeLast();
+ this.sourceSubscription = this.sourceSubscription!();
+ }
+
+ onSubscribeFirst() {
+ super.onSubscribeFirst();
+ this.sourceSubscription = this.source.subscribe(() => {
+ this.emit(this.get());
+ });
+ }
+
+ get(): C {
+ const sourceValue = this.source.get();
+ return this.mapper(sourceValue);
+ }
+}
\ No newline at end of file
diff --git a/src/platform/types/config.ts b/src/platform/types/config.ts
new file mode 100644
index 00000000..8a5eabf2
--- /dev/null
+++ b/src/platform/types/config.ts
@@ -0,0 +1,64 @@
+/*
+Copyright 2022 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.
+*/
+
+export type Config = {
+ /**
+ * The default homeserver used by Hydrogen; auto filled in the login UI.
+ * eg: https://matrix.org
+ * REQUIRED
+ */
+ defaultHomeServer: string;
+ /**
+ * The submit endpoint for your preferred rageshake server.
+ * eg: https://element.io/bugreports/submit
+ * Read more about rageshake at https://github.com/matrix-org/rageshake
+ * OPTIONAL
+ */
+ bugReportEndpointUrl?: string;
+ /**
+ * Paths to theme-manifests
+ * eg: ["assets/theme-element.json", "assets/theme-awesome.json"]
+ * REQUIRED
+ */
+ themeManifests: string[];
+ /**
+ * This configures the default theme(s) used by Hydrogen.
+ * These themes appear as "Default" option in the theme chooser UI and are also
+ * used as a fallback when other themes fail to load.
+ * Whether the dark or light variant is used depends on the system preference.
+ * OPTIONAL
+ */
+ defaultTheme?: {
+ // id of light theme
+ light: string;
+ // id of dark theme
+ dark: string;
+ };
+ /**
+ * Configuration for push notifications.
+ * See https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3pushersset
+ * and https://github.com/matrix-org/sygnal/blob/main/docs/applications.md#webpush
+ * OPTIONAL
+ */
+ push?: {
+ // See app_id in the request body in above link
+ appId: string;
+ // The host used for pushing notification
+ gatewayUrl: string;
+ // See pushkey in above link
+ applicationServerKey: string;
+ };
+};
diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts
index 9a984277..9dd969de 100644
--- a/src/platform/types/theme.ts
+++ b/src/platform/types/theme.ts
@@ -22,6 +22,13 @@ export type ThemeManifest = Partial<{
version: number;
// A user-facing string that is the name for this theme-collection.
name: string;
+ // An identifier for this theme
+ id: string;
+ /**
+ * Id of the theme that this theme derives from.
+ * Only present for derived/runtime themes.
+ */
+ extends: string;
/**
* This is added to the manifest during the build process and includes data
* that is needed to load themes at runtime.
@@ -42,6 +49,12 @@ export type ThemeManifest = Partial<{
"runtime-asset": string;
// Array of derived-variables
"derived-variables": Array;
+ /**
+ * Mapping from icon variable to location of icon in build output with query parameters
+ * indicating how it should be colored for this particular theme.
+ * eg: "icon-url-1": "element-logo.86bc8565.svg?primary=accent-color"
+ */
+ icon: Record;
};
values: {
/**
@@ -60,6 +73,8 @@ type Variant = Partial<{
default: boolean;
// A user-facing string that is the name for this variant.
name: string;
+ // A boolean indicating whether this is a dark theme or not
+ dark: boolean;
/**
* Mapping from css variable to its value.
* eg: {"background-color-primary": "#21262b", ...}
diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts
index c5f96f8b..f55b9867 100644
--- a/src/platform/types/types.ts
+++ b/src/platform/types/types.ts
@@ -26,6 +26,7 @@ export interface IRequestOptions {
cache?: boolean;
method?: string;
format?: string;
+ accessTokenOverride?: string;
}
export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult;
diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js
index ed912ee5..821b23d5 100644
--- a/src/platform/web/Platform.js
+++ b/src/platform/web/Platform.js
@@ -41,7 +41,7 @@ import {parseHTML} from "./parsehtml.js";
import {handleAvatarError} from "./ui/avatar";
import {MediaDevicesWrapper} from "./dom/MediaDevices";
import {DOMWebRTC} from "./dom/WebRTC";
-import {ThemeLoader} from "./ThemeLoader";
+import {ThemeLoader} from "./theming/ThemeLoader";
function addScript(src) {
return new Promise(function (resolve, reject) {
@@ -345,7 +345,7 @@ export class Platform {
document.querySelectorAll(".theme").forEach(e => e.remove());
// add new theme
const styleTag = document.createElement("link");
- styleTag.href = `./${newPath}`;
+ styleTag.href = newPath;
styleTag.rel = "stylesheet";
styleTag.type = "text/css";
styleTag.className = "theme";
diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts
deleted file mode 100644
index 8c9364bc..00000000
--- a/src/platform/web/ThemeLoader.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
-Copyright 2020 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 type {ILogItem} from "../../logging/types.js";
-import type {Platform} from "./Platform.js";
-
-type NormalVariant = {
- id: string;
- cssLocation: string;
-};
-
-type DefaultVariant = {
- dark: {
- id: string;
- cssLocation: string;
- variantName: string;
- };
- light: {
- id: string;
- cssLocation: string;
- variantName: string;
- };
- default: {
- id: string;
- cssLocation: string;
- variantName: string;
- };
-}
-
-type ThemeInformation = NormalVariant | DefaultVariant;
-
-export enum ColorSchemePreference {
- Dark,
- Light
-};
-
-export class ThemeLoader {
- private _platform: Platform;
- private _themeMapping: Record;
-
- constructor(platform: Platform) {
- this._platform = platform;
- }
-
- async init(manifestLocations: string[], log?: ILogItem): Promise {
- await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => {
- this._themeMapping = {};
- const results = await Promise.all(
- manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response())
- );
- results.forEach(({ body }) => this._populateThemeMap(body, log));
- });
- }
-
- private _populateThemeMap(manifest, log: ILogItem) {
- log.wrap("populateThemeMap", (l) => {
- /*
- After build has finished, the source section of each theme manifest
- contains `built-assets` which is a mapping from the theme-id to
- cssLocation of theme
- */
- const builtAssets: Record = manifest.source?.["built-assets"];
- const themeName = manifest.name;
- let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
- for (const [themeId, cssLocation] of Object.entries(builtAssets)) {
- const variant = themeId.match(/.+-(.+)/)?.[1];
- const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!];
- const themeDisplayName = `${themeName} ${variantName}`;
- if (isDefault) {
- /**
- * This is a default variant!
- * We'll add these to the themeMapping (separately) keyed with just the
- * theme-name (i.e "Element" instead of "Element Dark").
- * We need to be able to distinguish them from other variants!
- *
- * This allows us to render radio-buttons with "dark" and
- * "light" options.
- */
- const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant;
- defaultVariant.variantName = variantName;
- defaultVariant.id = themeId
- defaultVariant.cssLocation = cssLocation;
- continue;
- }
- // Non-default variants are keyed in themeMapping with "theme_name variant_name"
- // eg: "Element Dark"
- this._themeMapping[themeDisplayName] = {
- cssLocation,
- id: themeId
- };
- }
- if (defaultDarkVariant.id && defaultLightVariant.id) {
- /**
- * As mentioned above, if there's both a default dark and a default light variant,
- * add them to themeMapping separately.
- */
- const defaultVariant = this.preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant;
- this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant };
- }
- else {
- /**
- * If only one default variant is found (i.e only dark default or light default but not both),
- * treat it like any other variant.
- */
- const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant;
- this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation };
- }
- //Add the default-theme as an additional option to the mapping
- const defaultThemeId = this.getDefaultTheme();
- if (defaultThemeId) {
- const themeDetails = this._findThemeDetailsFromId(defaultThemeId);
- if (themeDetails) {
- this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation };
- }
- }
- l.log({ l: "Default Theme", theme: defaultThemeId});
- l.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" });
- l.log({ l: "Result", themeMapping: this._themeMapping });
- });
- }
-
- setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) {
- this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => {
- let cssLocation: string;
- let themeDetails = this._themeMapping[themeName];
- if ("id" in themeDetails) {
- cssLocation = themeDetails.cssLocation;
- }
- else {
- if (!themeVariant) {
- throw new Error("themeVariant is undefined!");
- }
- cssLocation = themeDetails[themeVariant].cssLocation;
- }
- this._platform.replaceStylesheet(cssLocation);
- this._platform.settingsStorage.setString("theme-name", themeName);
- if (themeVariant) {
- this._platform.settingsStorage.setString("theme-variant", themeVariant);
- }
- else {
- this._platform.settingsStorage.remove("theme-variant");
- }
- });
- }
-
- /** Maps theme display name to theme information */
- get themeMapping(): Record {
- return this._themeMapping;
- }
-
- async getActiveTheme(): Promise<{themeName: string, themeVariant?: string}> {
- let themeName = await this._platform.settingsStorage.getString("theme-name");
- let themeVariant = await this._platform.settingsStorage.getString("theme-variant");
- if (!themeName || !this._themeMapping[themeName]) {
- themeName = "Default" in this._themeMapping ? "Default" : Object.keys(this._themeMapping)[0];
- if (!this._themeMapping[themeName][themeVariant]) {
- themeVariant = "default" in this._themeMapping[themeName] ? "default" : undefined;
- }
- }
- return { themeName, themeVariant };
- }
-
- getDefaultTheme(): string | undefined {
- switch (this.preferredColorScheme) {
- case ColorSchemePreference.Dark:
- return this._platform.config["defaultTheme"]?.dark;
- case ColorSchemePreference.Light:
- return this._platform.config["defaultTheme"]?.light;
- }
- }
-
- private _findThemeDetailsFromId(themeId: string): {themeName: string, cssLocation: string, variant?: string} | undefined {
- for (const [themeName, themeData] of Object.entries(this._themeMapping)) {
- if ("id" in themeData && themeData.id === themeId) {
- return { themeName, cssLocation: themeData.cssLocation };
- }
- else if ("light" in themeData && themeData.light?.id === themeId) {
- return { themeName, cssLocation: themeData.light.cssLocation, variant: "light" };
- }
- else if ("dark" in themeData && themeData.dark?.id === themeId) {
- return { themeName, cssLocation: themeData.dark.cssLocation, variant: "dark" };
- }
- }
- }
-
- get preferredColorScheme(): ColorSchemePreference | undefined {
- if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
- return ColorSchemePreference.Dark;
- }
- else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
- return ColorSchemePreference.Light;
- }
- }
-}
diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js
index 96576626..c1169a3d 100644
--- a/src/platform/web/dom/History.js
+++ b/src/platform/web/dom/History.js
@@ -17,6 +17,12 @@ limitations under the License.
import {BaseObservableValue} from "../../../observable/value/BaseObservableValue";
export class History extends BaseObservableValue {
+
+ constructor() {
+ super();
+ this._lastSessionHash = undefined;
+ }
+
handleEvent(event) {
if (event.type === "hashchange") {
this.emit(this.get());
@@ -65,6 +71,7 @@ export class History extends BaseObservableValue {
}
onSubscribeFirst() {
+ this._lastSessionHash = window.localStorage?.getItem("hydrogen_last_url_hash");
window.addEventListener('hashchange', this);
}
@@ -76,7 +83,7 @@ export class History extends BaseObservableValue {
window.localStorage?.setItem("hydrogen_last_url_hash", hash);
}
- getLastUrl() {
- return window.localStorage?.getItem("hydrogen_last_url_hash");
+ getLastSessionUrl() {
+ return this._lastSessionHash;
}
}
diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js
index 497ad553..eb4caab6 100644
--- a/src/platform/web/dom/request/fetch.js
+++ b/src/platform/web/dom/request/fetch.js
@@ -115,6 +115,9 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) {
} else if (format === "buffer") {
body = await response.arrayBuffer();
}
+ else if (format === "text") {
+ body = await response.text();
+ }
} catch (err) {
// some error pages return html instead of json, ignore error
if (!(err.name === "SyntaxError" && status >= 400)) {
diff --git a/src/platform/web/main.js b/src/platform/web/main.js
index edc2cf14..83644456 100644
--- a/src/platform/web/main.js
+++ b/src/platform/web/main.js
@@ -17,7 +17,7 @@ limitations under the License.
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
import {RootViewModel} from "../../domain/RootViewModel.js";
-import {createNavigation, createRouter} from "../../domain/navigation/index.js";
+import {createNavigation, createRouter} from "../../domain/navigation/index";
// Don't use a default export here, as we use multiple entries during legacy build,
// which does not support default exports,
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts
new file mode 100644
index 00000000..ca46a8fd
--- /dev/null
+++ b/src/platform/web/theming/DerivedVariables.ts
@@ -0,0 +1,131 @@
+/*
+Copyright 2020 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 {derive} from "./shared/color.mjs";
+
+export class DerivedVariables {
+ private _baseVariables: Record;
+ private _variablesToDerive: string[]
+ private _isDark: boolean
+ private _aliases: Record = {};
+ private _derivedAliases: string[] = [];
+
+ constructor(baseVariables: Record, variablesToDerive: string[], isDark: boolean) {
+ this._baseVariables = baseVariables;
+ this._variablesToDerive = variablesToDerive;
+ this._isDark = isDark;
+ }
+
+ toVariables(): Record {
+ const resolvedVariables: any = {};
+ this._detectAliases();
+ for (const variable of this._variablesToDerive) {
+ const resolvedValue = this._derive(variable);
+ if (resolvedValue) {
+ resolvedVariables[variable] = resolvedValue;
+ }
+ }
+ for (const [alias, variable] of Object.entries(this._aliases) as any) {
+ resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable];
+ }
+ for (const variable of this._derivedAliases) {
+ const resolvedValue = this._deriveAlias(variable, resolvedVariables);
+ if (resolvedValue) {
+ resolvedVariables[variable] = resolvedValue;
+ }
+ }
+ return resolvedVariables;
+ }
+
+ private _detectAliases(): void {
+ const newVariablesToDerive: string[] = [];
+ for (const variable of this._variablesToDerive) {
+ const [alias, value] = variable.split("=");
+ if (value) {
+ this._aliases[alias] = value;
+ }
+ else {
+ newVariablesToDerive.push(variable);
+ }
+ }
+ this._variablesToDerive = newVariablesToDerive;
+ }
+
+ private _derive(variable: string): string | undefined {
+ const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/;
+ const matches = variable.match(RE_VARIABLE_VALUE);
+ if (matches) {
+ const [, baseVariable, operation, argument] = matches;
+ const value = this._baseVariables[baseVariable];
+ if (!value ) {
+ if (this._aliases[baseVariable]) {
+ this._derivedAliases.push(variable);
+ return;
+ }
+ else {
+ throw new Error(`Cannot find value for base variable "${baseVariable}"!`);
+ }
+ }
+ const resolvedValue = derive(value, operation, argument, this._isDark);
+ return resolvedValue;
+ }
+ }
+
+ private _deriveAlias(variable: string, resolvedVariables: Record): string | undefined {
+ const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/;
+ const matches = variable.match(RE_VARIABLE_VALUE);
+ if (matches) {
+ const [, baseVariable, operation, argument] = matches;
+ const value = resolvedVariables[baseVariable];
+ if (!value ) {
+ throw new Error(`Cannot find value for alias "${baseVariable}" when trying to derive ${variable}!`);
+ }
+ const resolvedValue = derive(value, operation, argument, this._isDark);
+ return resolvedValue;
+ }
+ }
+}
+
+import * as pkg from "off-color";
+// @ts-ignore
+const offColor = pkg.offColor ?? pkg.default.offColor;
+
+export function tests() {
+ return {
+ "Simple variable derivation": assert => {
+ const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], false);
+ const result = deriver.toVariables();
+ const resultColor = offColor("#ff00ff").darken(5/100).hex();
+ assert.deepEqual(result, {"background-color--darker-5": resultColor});
+ },
+
+ "For dark themes, lighten and darken are inverted": assert => {
+ const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], true);
+ const result = deriver.toVariables();
+ const resultColor = offColor("#ff00ff").lighten(5/100).hex();
+ assert.deepEqual(result, {"background-color--darker-5": resultColor});
+ },
+
+ "Aliases can be derived": assert => {
+ const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["my-awesome-alias=background-color","my-awesome-alias--darker-5"], false);
+ const result = deriver.toVariables();
+ const resultColor = offColor("#ff00ff").darken(5/100).hex();
+ assert.deepEqual(result, {
+ "my-awesome-alias": "#ff00ff",
+ "my-awesome-alias--darker-5": resultColor,
+ });
+ },
+ }
+}
diff --git a/src/platform/web/theming/IconColorizer.ts b/src/platform/web/theming/IconColorizer.ts
new file mode 100644
index 00000000..e02c0971
--- /dev/null
+++ b/src/platform/web/theming/IconColorizer.ts
@@ -0,0 +1,79 @@
+/*
+Copyright 2020 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 type {Platform} from "../Platform.js";
+import {getColoredSvgString} from "./shared/svg-colorizer.mjs";
+
+type ParsedStructure = {
+ [variableName: string]: {
+ svg: Promise<{ status: number; body: string }>;
+ primary: string | null;
+ secondary: string | null;
+ };
+};
+
+export class IconColorizer {
+ private _iconVariables: Record;
+ private _resolvedVariables: Record;
+ private _manifestLocation: string;
+ private _platform: Platform;
+
+ constructor(platform: Platform, iconVariables: Record, resolvedVariables: Record, manifestLocation: string) {
+ this._platform = platform;
+ this._iconVariables = iconVariables;
+ this._resolvedVariables = resolvedVariables;
+ this._manifestLocation = manifestLocation;
+ }
+
+ async toVariables(): Promise> {
+ const { parsedStructure, promises } = await this._fetchAndParseIcons();
+ await Promise.all(promises);
+ return this._produceColoredIconVariables(parsedStructure);
+ }
+
+ private async _fetchAndParseIcons(): Promise<{ parsedStructure: ParsedStructure, promises: any[] }> {
+ const promises: any[] = [];
+ const parsedStructure: ParsedStructure = {};
+ for (const [variable, url] of Object.entries(this._iconVariables)) {
+ const urlObject = new URL(`https://${url}`);
+ const pathWithoutQueryParams = urlObject.hostname;
+ const relativePath = new URL(pathWithoutQueryParams, new URL(this._manifestLocation, window.location.origin));
+ const responsePromise = this._platform.request(relativePath, { method: "GET", format: "text", cache: true, }).response()
+ promises.push(responsePromise);
+ const searchParams = urlObject.searchParams;
+ parsedStructure[variable] = {
+ svg: responsePromise,
+ primary: searchParams.get("primary"),
+ secondary: searchParams.get("secondary")
+ };
+ }
+ return { parsedStructure, promises };
+ }
+
+ private async _produceColoredIconVariables(parsedStructure: ParsedStructure): Promise> {
+ let coloredVariables: Record = {};
+ for (const [variable, { svg, primary, secondary }] of Object.entries(parsedStructure)) {
+ const { body: svgCode } = await svg;
+ if (!primary) {
+ throw new Error(`Primary color variable ${primary} not in list of variables!`);
+ }
+ const primaryColor = this._resolvedVariables[primary], secondaryColor = this._resolvedVariables[secondary!];
+ const coloredSvgCode = getColoredSvgString(svgCode, primaryColor, secondaryColor);
+ const dataURI = `url('data:image/svg+xml;utf8,${encodeURIComponent(coloredSvgCode)}')`;
+ coloredVariables[variable] = dataURI;
+ }
+ return coloredVariables;
+ }
+}
diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts
new file mode 100644
index 00000000..be1bafc0
--- /dev/null
+++ b/src/platform/web/theming/ThemeLoader.ts
@@ -0,0 +1,188 @@
+/*
+Copyright 2020 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 type {ILogItem} from "../../../logging/types";
+import type {Platform} from "../Platform.js";
+import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser";
+import type {Variant, ThemeInformation} from "./parsers/types";
+import {ColorSchemePreference} from "./parsers/types";
+import {BuiltThemeParser} from "./parsers/BuiltThemeParser";
+
+export class ThemeLoader {
+ private _platform: Platform;
+ private _themeMapping: Record;
+ private _injectedVariables?: Record;
+
+ constructor(platform: Platform) {
+ this._platform = platform;
+ }
+
+ async init(manifestLocations: string[], log?: ILogItem): Promise {
+ await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => {
+ const results = await Promise.all(
+ manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response())
+ );
+ const runtimeThemeParser = new RuntimeThemeParser(this._platform, this.preferredColorScheme);
+ const builtThemeParser = new BuiltThemeParser(this.preferredColorScheme);
+ const runtimeThemePromises: Promise[] = [];
+ for (let i = 0; i < results.length; ++i) {
+ const { body } = results[i];
+ try {
+ if (body.extends) {
+ const indexOfBaseManifest = results.findIndex(manifest => manifest.body.id === body.extends);
+ if (indexOfBaseManifest === -1) {
+ throw new Error(`Base manifest for derived theme at ${manifestLocations[i]} not found!`);
+ }
+ const {body: baseManifest} = results[indexOfBaseManifest];
+ const baseManifestLocation = manifestLocations[indexOfBaseManifest];
+ const promise = runtimeThemeParser.parse(body, baseManifest, baseManifestLocation, log);
+ runtimeThemePromises.push(promise);
+ }
+ else {
+ builtThemeParser.parse(body, manifestLocations[i], log);
+ }
+ }
+ catch(e) {
+ console.error(e);
+ }
+ }
+ await Promise.all(runtimeThemePromises);
+ this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping };
+ Object.assign(this._themeMapping, builtThemeParser.themeMapping, runtimeThemeParser.themeMapping);
+ this._addDefaultThemeToMapping(log);
+ log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" });
+ log.log({ l: "Result", themeMapping: this._themeMapping });
+ });
+ }
+
+ setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) {
+ this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => {
+ let cssLocation: string, variables: Record;
+ let themeDetails = this._themeMapping[themeName];
+ if ("id" in themeDetails) {
+ cssLocation = themeDetails.cssLocation;
+ variables = themeDetails.variables;
+ }
+ else {
+ if (!themeVariant) {
+ throw new Error("themeVariant is undefined!");
+ }
+ cssLocation = themeDetails[themeVariant].cssLocation;
+ variables = themeDetails[themeVariant].variables;
+ }
+ this._platform.replaceStylesheet(cssLocation);
+ if (variables) {
+ log?.log({l: "Derived Theme", variables});
+ this._injectCSSVariables(variables);
+ }
+ else {
+ this._removePreviousCSSVariables();
+ }
+ this._platform.settingsStorage.setString("theme-name", themeName);
+ if (themeVariant) {
+ this._platform.settingsStorage.setString("theme-variant", themeVariant);
+ }
+ else {
+ this._platform.settingsStorage.remove("theme-variant");
+ }
+ });
+ }
+
+ private _injectCSSVariables(variables: Record): void {
+ const root = document.documentElement;
+ for (const [variable, value] of Object.entries(variables)) {
+ root.style.setProperty(`--${variable}`, value);
+ }
+ this._injectedVariables = variables;
+ }
+
+ private _removePreviousCSSVariables(): void {
+ if (!this._injectedVariables) {
+ return;
+ }
+ const root = document.documentElement;
+ for (const variable of Object.keys(this._injectedVariables)) {
+ root.style.removeProperty(`--${variable}`);
+ }
+ this._injectedVariables = undefined;
+ }
+
+ /** Maps theme display name to theme information */
+ get themeMapping(): Record {
+ return this._themeMapping;
+ }
+
+ async getActiveTheme(): Promise<{themeName: string, themeVariant?: string}> {
+ let themeName = await this._platform.settingsStorage.getString("theme-name");
+ let themeVariant = await this._platform.settingsStorage.getString("theme-variant");
+ if (!themeName || !this._themeMapping[themeName]) {
+ themeName = "Default" in this._themeMapping ? "Default" : Object.keys(this._themeMapping)[0];
+ if (!this._themeMapping[themeName][themeVariant]) {
+ themeVariant = "default" in this._themeMapping[themeName] ? "default" : undefined;
+ }
+ }
+ return { themeName, themeVariant };
+ }
+
+ getDefaultTheme(): string | undefined {
+ switch (this.preferredColorScheme) {
+ case ColorSchemePreference.Dark:
+ return this._platform.config["defaultTheme"]?.dark;
+ case ColorSchemePreference.Light:
+ return this._platform.config["defaultTheme"]?.light;
+ }
+ }
+
+ private _findThemeDetailsFromId(themeId: string): {themeName: string, themeData: Partial} | undefined {
+ for (const [themeName, themeData] of Object.entries(this._themeMapping)) {
+ if ("id" in themeData && themeData.id === themeId) {
+ return { themeName, themeData };
+ }
+ else if ("light" in themeData && themeData.light?.id === themeId) {
+ return { themeName, themeData: themeData.light };
+ }
+ else if ("dark" in themeData && themeData.dark?.id === themeId) {
+ return { themeName, themeData: themeData.dark };
+ }
+ }
+ }
+
+ private _addDefaultThemeToMapping(log: ILogItem) {
+ log.wrap("addDefaultThemeToMapping", l => {
+ const defaultThemeId = this.getDefaultTheme();
+ if (defaultThemeId) {
+ const themeDetails = this._findThemeDetailsFromId(defaultThemeId);
+ if (themeDetails) {
+ this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! };
+ const variables = themeDetails.themeData.variables;
+ if (variables) {
+ this._themeMapping["Default"].variables = variables;
+ }
+ }
+ }
+ l.log({ l: "Default Theme", theme: defaultThemeId});
+ });
+ }
+
+ get preferredColorScheme(): ColorSchemePreference | undefined {
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
+ return ColorSchemePreference.Dark;
+ }
+ else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
+ return ColorSchemePreference.Light;
+ }
+ }
+}
diff --git a/src/platform/web/theming/parsers/BuiltThemeParser.ts b/src/platform/web/theming/parsers/BuiltThemeParser.ts
new file mode 100644
index 00000000..fbafadb8
--- /dev/null
+++ b/src/platform/web/theming/parsers/BuiltThemeParser.ts
@@ -0,0 +1,106 @@
+/*
+Copyright 2020 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 type {ThemeInformation} from "./types";
+import type {ThemeManifest} from "../../../types/theme";
+import type {ILogItem} from "../../../../logging/types";
+import {ColorSchemePreference} from "./types";
+
+export class BuiltThemeParser {
+ private _themeMapping: Record = {};
+ private _preferredColorScheme?: ColorSchemePreference;
+
+ constructor(preferredColorScheme?: ColorSchemePreference) {
+ this._preferredColorScheme = preferredColorScheme;
+ }
+
+ parse(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) {
+ log.wrap("BuiltThemeParser.parse", () => {
+ /*
+ After build has finished, the source section of each theme manifest
+ contains `built-assets` which is a mapping from the theme-id to
+ cssLocation of theme
+ */
+ const builtAssets: Record = manifest.source?.["built-assets"];
+ const themeName = manifest.name;
+ if (!themeName) {
+ throw new Error(`Theme name not found in manifest at ${manifestLocation}`);
+ }
+ let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
+ for (let [themeId, cssLocation] of Object.entries(builtAssets)) {
+ try {
+ /**
+ * This cssLocation is relative to the location of the manifest file.
+ * So we first need to resolve it relative to the root of this hydrogen instance.
+ */
+ cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href;
+ }
+ catch {
+ continue;
+ }
+ const variant = themeId.match(/.+-(.+)/)?.[1];
+ const variantDetails = manifest.values?.variants[variant!];
+ if (!variantDetails) {
+ throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`);
+ }
+ const { name: variantName, default: isDefault, dark } = variantDetails;
+ const themeDisplayName = `${themeName} ${variantName}`;
+ if (isDefault) {
+ /**
+ * This is a default variant!
+ * We'll add these to the themeMapping (separately) keyed with just the
+ * theme-name (i.e "Element" instead of "Element Dark").
+ * We need to be able to distinguish them from other variants!
+ *
+ * This allows us to render radio-buttons with "dark" and
+ * "light" options.
+ */
+ const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant;
+ defaultVariant.variantName = variantName;
+ defaultVariant.id = themeId
+ defaultVariant.cssLocation = cssLocation;
+ continue;
+ }
+ // Non-default variants are keyed in themeMapping with "theme_name variant_name"
+ // eg: "Element Dark"
+ this._themeMapping[themeDisplayName] = {
+ cssLocation,
+ id: themeId
+ };
+ }
+ if (defaultDarkVariant.id && defaultLightVariant.id) {
+ /**
+ * As mentioned above, if there's both a default dark and a default light variant,
+ * add them to themeMapping separately.
+ */
+ const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant;
+ this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant };
+ }
+ else {
+ /**
+ * If only one default variant is found (i.e only dark default or light default but not both),
+ * treat it like any other variant.
+ */
+ const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant;
+ this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation };
+ }
+ });
+ }
+
+ get themeMapping(): Record {
+ return this._themeMapping;
+ }
+}
diff --git a/src/platform/web/theming/parsers/RuntimeThemeParser.ts b/src/platform/web/theming/parsers/RuntimeThemeParser.ts
new file mode 100644
index 00000000..9471740a
--- /dev/null
+++ b/src/platform/web/theming/parsers/RuntimeThemeParser.ts
@@ -0,0 +1,98 @@
+/*
+Copyright 2020 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 type {ThemeInformation} from "./types";
+import type {Platform} from "../../Platform.js";
+import type {ThemeManifest} from "../../../types/theme";
+import {ColorSchemePreference} from "./types";
+import {IconColorizer} from "../IconColorizer";
+import {DerivedVariables} from "../DerivedVariables";
+import {ILogItem} from "../../../../logging/types";
+
+export class RuntimeThemeParser {
+ private _themeMapping: Record = {};
+ private _preferredColorScheme?: ColorSchemePreference;
+ private _platform: Platform;
+
+ constructor(platform: Platform, preferredColorScheme?: ColorSchemePreference) {
+ this._preferredColorScheme = preferredColorScheme;
+ this._platform = platform;
+ }
+
+ async parse(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise {
+ await log.wrap("RuntimeThemeParser.parse", async () => {
+ const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log);
+ const themeName = manifest.name;
+ if (!themeName) {
+ throw new Error(`Theme name not found in manifest!`);
+ }
+ let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
+ for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) {
+ try {
+ const themeId = `${manifest.id}-${variant}`;
+ const { name: variantName, default: isDefault, dark, variables } = variantDetails;
+ const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables();
+ Object.assign(variables, resolvedVariables);
+ const iconVariables = await new IconColorizer(this._platform, icons, variables, baseManifestLocation).toVariables();
+ Object.assign(variables, resolvedVariables, iconVariables);
+ const themeDisplayName = `${themeName} ${variantName}`;
+ if (isDefault) {
+ const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant;
+ Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables });
+ continue;
+ }
+ this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, };
+ }
+ catch (e) {
+ console.error(e);
+ continue;
+ }
+ }
+ if (defaultDarkVariant.id && defaultLightVariant.id) {
+ const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant;
+ this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant };
+ }
+ else {
+ const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant;
+ this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation };
+ }
+ });
+ }
+
+ private _getSourceData(manifest: ThemeManifest, location: string, log: ILogItem)
+ : { cssLocation: string, derivedVariables: string[], icons: Record} {
+ return log.wrap("getSourceData", () => {
+ const runtimeCSSLocation = manifest.source?.["runtime-asset"];
+ if (!runtimeCSSLocation) {
+ throw new Error(`Run-time asset not found in source section for theme at ${location}`);
+ }
+ const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href;
+ const derivedVariables = manifest.source?.["derived-variables"];
+ if (!derivedVariables) {
+ throw new Error(`Derived variables not found in source section for theme at ${location}`);
+ }
+ const icons = manifest.source?.["icon"];
+ if (!icons) {
+ throw new Error(`Icon mapping not found in source section for theme at ${location}`);
+ }
+ return { cssLocation, derivedVariables, icons };
+ });
+ }
+
+ get themeMapping(): Record {
+ return this._themeMapping;
+ }
+
+}
diff --git a/src/platform/web/theming/parsers/types.ts b/src/platform/web/theming/parsers/types.ts
new file mode 100644
index 00000000..b357cf2c
--- /dev/null
+++ b/src/platform/web/theming/parsers/types.ts
@@ -0,0 +1,38 @@
+/*
+Copyright 2020 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.
+*/
+
+export type NormalVariant = {
+ id: string;
+ cssLocation: string;
+ variables?: any;
+};
+
+export type Variant = NormalVariant & {
+ variantName: string;
+};
+
+export type DefaultVariant = {
+ dark: Variant;
+ light: Variant;
+ default: Variant;
+}
+
+export type ThemeInformation = NormalVariant | DefaultVariant;
+
+export enum ColorSchemePreference {
+ Dark,
+ Light
+};
diff --git a/scripts/postcss/color.js b/src/platform/web/theming/shared/color.mjs
similarity index 89%
rename from scripts/postcss/color.js
rename to src/platform/web/theming/shared/color.mjs
index b1ef7073..8af76b6b 100644
--- a/scripts/postcss/color.js
+++ b/src/platform/web/theming/shared/color.mjs
@@ -13,10 +13,10 @@ 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 * as pkg from 'off-color';
+const offColor = pkg.offColor ?? pkg.default.offColor;
-const offColor = require("off-color").offColor;
-
-module.exports.derive = function (value, operation, argument, isDark) {
+export function derive(value, operation, argument, isDark) {
const argumentAsNumber = parseInt(argument);
if (isDark) {
// For dark themes, invert the operation
diff --git a/src/platform/web/theming/shared/svg-colorizer.mjs b/src/platform/web/theming/shared/svg-colorizer.mjs
new file mode 100644
index 00000000..cb291726
--- /dev/null
+++ b/src/platform/web/theming/shared/svg-colorizer.mjs
@@ -0,0 +1,24 @@
+/*
+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.
+*/
+
+export function getColoredSvgString(svgString, primaryColor, secondaryColor) {
+ let coloredSVGCode = svgString.replaceAll("#ff00ff", primaryColor);
+ coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor);
+ if (svgString === coloredSVGCode) {
+ throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive).");
+ }
+ return coloredSVGCode;
+}
diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css
index deb16b02..ae706242 100644
--- a/src/platform/web/ui/css/login.css
+++ b/src/platform/web/ui/css/login.css
@@ -68,13 +68,13 @@ limitations under the License.
--size: 20px;
}
-.StartSSOLoginView {
+.StartSSOLoginView, .StartOIDCLoginView {
display: flex;
flex-direction: column;
padding: 0 0.4em 0;
}
-.StartSSOLoginView_button {
+.StartSSOLoginView_button, .StartOIDCLoginView_button {
flex: 1;
margin-top: 12px;
}
diff --git a/src/platform/web/ui/css/themes/element/manifest.json b/src/platform/web/ui/css/themes/element/manifest.json
index e183317c..cb21eaad 100644
--- a/src/platform/web/ui/css/themes/element/manifest.json
+++ b/src/platform/web/ui/css/themes/element/manifest.json
@@ -1,6 +1,7 @@
{
"version": 1,
"name": "Element",
+ "id": "element",
"values": {
"variants": {
"light": {
diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css
index 6403bb60..eb51c7bf 100644
--- a/src/platform/web/ui/css/themes/element/theme.css
+++ b/src/platform/web/ui/css/themes/element/theme.css
@@ -522,6 +522,62 @@ a {
.RoomView_error {
color: var(--error-color);
+ background : #efefef;
+ height : 0px;
+ font-weight : bold;
+ transition : 0.25s all ease-out;
+ padding-right : 20px;
+ padding-left : 20px;
+}
+
+.RoomView_error div{
+ overflow : hidden;
+ height: 100%;
+ width: 100%;
+ position : relative;
+ display : flex;
+ align-items : center;
+}
+
+.RoomView_error:not(:empty) {
+ height : auto;
+ padding-top : 20px;
+ padding-bottom : 20px;
+}
+
+.RoomView_error p {
+ position : relative;
+ display : block;
+ width : 100%;
+ height : auto;
+ margin : 0;
+}
+
+.RoomView_error button {
+ width : 40px;
+ padding-top : 20px;
+ padding-bottom : 20px;
+ background : none;
+ border : none;
+ position : relative;
+ border-radius : 5px;
+ transition: 0.1s all ease-out;
+ cursor: pointer;
+}
+
+.RoomView_error button:hover {
+ background : #cfcfcf;
+}
+
+.RoomView_error button:before {
+ content:"\274c";
+ position : absolute;
+ top : 15px;
+ left: 9px;
+ width : 20px;
+ height : 10px;
+ font-size : 10px;
+ align-self : middle;
}
.MessageComposer_replyPreview .Timeline_message {
@@ -895,12 +951,12 @@ button.link {
width: 100%;
}
-.RoomArchivedView {
+.DisabledComposerView {
padding: 12px;
background-color: var(--background-color-secondary);
}
-.RoomArchivedView h3 {
+.DisabledComposerView h3 {
margin: 0;
}
diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js
index 88002625..b44de2e4 100644
--- a/src/platform/web/ui/login/LoginView.js
+++ b/src/platform/web/ui/login/LoginView.js
@@ -57,6 +57,7 @@ export class LoginView extends TemplateView {
t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null),
t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)),
t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null),
+ t.mapView(vm => vm.startOIDCLoginViewModel, vm => vm ? new StartOIDCLoginView(vm) : null),
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null),
// use t.mapView rather than t.if to create a new view when the view model changes too
t.p(hydrogenGithubLink(t))
@@ -76,3 +77,16 @@ class StartSSOLoginView extends TemplateView {
);
}
}
+
+class StartOIDCLoginView extends TemplateView {
+ render(t, vm) {
+ return t.div({ className: "StartOIDCLoginView" },
+ t.a({
+ className: "StartOIDCLoginView_button button-action primary",
+ type: "button",
+ onClick: () => vm.startOIDCLogin(),
+ disabled: vm => vm.isBusy
+ }, vm.i18n`Continue`)
+ );
+ }
+}
diff --git a/src/platform/web/ui/session/room/RoomArchivedView.js b/src/platform/web/ui/session/room/DisabledComposerView.js
similarity index 82%
rename from src/platform/web/ui/session/room/RoomArchivedView.js
rename to src/platform/web/ui/session/room/DisabledComposerView.js
index 1db1c2d2..caa8eeb9 100644
--- a/src/platform/web/ui/session/room/RoomArchivedView.js
+++ b/src/platform/web/ui/session/room/DisabledComposerView.js
@@ -16,8 +16,8 @@ limitations under the License.
import {TemplateView} from "../../general/TemplateView";
-export class RoomArchivedView extends TemplateView {
+export class DisabledComposerView extends TemplateView {
render(t) {
- return t.div({className: "RoomArchivedView"}, t.h3(vm => vm.description));
+ return t.div({className: "DisabledComposerView"}, t.h3(vm => vm.description));
}
}
diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js
index 4f9e40d9..41bb5dd5 100644
--- a/src/platform/web/ui/session/room/RoomView.js
+++ b/src/platform/web/ui/session/room/RoomView.js
@@ -21,7 +21,7 @@ import {Menu} from "../../general/Menu.js";
import {TimelineView} from "./TimelineView";
import {TimelineLoadingView} from "./TimelineLoadingView.js";
import {MessageComposer} from "./MessageComposer.js";
-import {RoomArchivedView} from "./RoomArchivedView.js";
+import {DisabledComposerView} from "./DisabledComposerView.js";
import {AvatarView} from "../../AvatarView.js";
import {CallView} from "./CallView";
@@ -33,12 +33,6 @@ export class RoomView extends TemplateView {
}
render(t, vm) {
- let bottomView;
- if (vm.composerViewModel.kind === "composer") {
- bottomView = new MessageComposer(vm.composerViewModel, this._viewClassForTile);
- } else if (vm.composerViewModel.kind === "archived") {
- bottomView = new RoomArchivedView(vm.composerViewModel);
- }
return t.main({className: "RoomView middle"}, [
t.div({className: "RoomHeader middle-header"}, [
t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}),
@@ -53,18 +47,32 @@ export class RoomView extends TemplateView {
})
]),
t.div({className: "RoomView_body"}, [
- t.div({className: "RoomView_error"}, vm => vm.error),
+ t.div({className: "RoomView_error"}, [
+ t.if(vm => vm.error, t => t.div(
+ [
+ t.p({}, vm => vm.error),
+ t.button({ className: "RoomView_error_closerButton", onClick: evt => vm.dismissError(evt) })
+ ])
+ )]),
t.mapView(vm => vm.callViewModel, callViewModel => callViewModel ? new CallView(callViewModel) : null),
t.mapView(vm => vm.timelineViewModel, timelineViewModel => {
return timelineViewModel ?
new TimelineView(timelineViewModel, this._viewClassForTile) :
new TimelineLoadingView(vm); // vm is just needed for i18n
}),
- t.view(bottomView),
+ t.mapView(vm => vm.composerViewModel,
+ composerViewModel => {
+ switch (composerViewModel?.kind) {
+ case "composer":
+ return new MessageComposer(vm.composerViewModel, this._viewClassForTile);
+ case "disabled":
+ return new DisabledComposerView(vm.composerViewModel);
+ }
+ }),
])
]);
}
-
+
_toggleOptionsMenu(evt) {
if (this._optionsPopup && this._optionsPopup.isOpen) {
this._optionsPopup.close();
diff --git a/src/platform/web/ui/session/room/timeline/ImageView.js b/src/platform/web/ui/session/room/timeline/ImageView.js
index 1668b09c..19591606 100644
--- a/src/platform/web/ui/session/room/timeline/ImageView.js
+++ b/src/platform/web/ui/session/room/timeline/ImageView.js
@@ -24,6 +24,6 @@ export class ImageView extends BaseMediaView {
title: vm => vm.label,
style: `max-width: ${vm.width}px; max-height: ${vm.height}px;`
});
- return vm.isPending ? img : t.a({href: vm.lightboxUrl}, img);
+ return vm.isPending || !vm.lightboxUrl ? img : t.a({href: vm.lightboxUrl}, img);
}
}
diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js
index d7c48351..0b2b3d8e 100644
--- a/src/platform/web/ui/session/settings/SettingsView.js
+++ b/src/platform/web/ui/session/settings/SettingsView.js
@@ -47,6 +47,13 @@ export class SettingsView extends TemplateView {
disabled: vm => vm.isLoggingOut
}, vm.i18n`Log out`)),
);
+
+ settingNodes.push(
+ t.if(vm => vm.accountManagementUrl, t => {
+ return t.p([vm.i18n`You can manage your account `, t.a({href: vm.accountManagementUrl, target: "_blank"}, vm.i18n`here`), "."]);
+ }),
+ );
+
settingNodes.push(
t.h3("Key backup"),
t.view(new KeyBackupSettingsView(vm.keyBackupViewModel))
diff --git a/vite.common-config.js b/vite.common-config.js
index 5d65f8e2..2fa09d46 100644
--- a/vite.common-config.js
+++ b/vite.common-config.js
@@ -8,8 +8,8 @@ const path = require("path");
const manifest = require("./package.json");
const version = manifest.version;
const compiledVariables = new Map();
-const derive = require("./scripts/postcss/color").derive;
-const replacer = require("./scripts/postcss/svg-colorizer").buildColorizedSVG;
+import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs";
+import {derive} from "./src/platform/web/theming/shared/color.mjs";
const commonOptions = {
logLevel: "warn",
diff --git a/vite.config.js b/vite.config.js
index e784a4ad..0bbeb4d4 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -33,9 +33,7 @@ export default defineConfig(({mode}) => {
plugins: [
themeBuilder({
themeConfig: {
- themes: {
- element: "./src/platform/web/ui/css/themes/element",
- },
+ themes: ["./src/platform/web/ui/css/themes/element"],
default: "element",
},
compiledVariables,
diff --git a/vite.sdk-assets-config.js b/vite.sdk-assets-config.js
index d7d4d064..5c1f3196 100644
--- a/vite.sdk-assets-config.js
+++ b/vite.sdk-assets-config.js
@@ -36,7 +36,7 @@ export default mergeOptions(commonOptions, {
plugins: [
themeBuilder({
themeConfig: {
- themes: { element: "./src/platform/web/ui/css/themes/element" },
+ themes: ["./src/platform/web/ui/css/themes/element"],
default: "element",
},
compiledVariables,
diff --git a/yarn.lock b/yarn.lock
index 0408a6e0..44f07a81 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -82,6 +82,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@trysound/sax@0.2.0":
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
+ integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+
"@types/json-schema@^7.0.7":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@@ -352,6 +357,11 @@ commander@^6.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
+commander@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+ integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -387,11 +397,26 @@ css-select@^4.1.3:
domutils "^2.6.0"
nth-check "^2.0.0"
+css-tree@^1.1.2, css-tree@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+ integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+ dependencies:
+ mdn-data "2.0.14"
+ source-map "^0.6.1"
+
css-what@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
+csso@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+ integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+ dependencies:
+ css-tree "^1.1.2"
+
cuint@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
@@ -1202,6 +1227,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+mdn-data@2.0.14:
+ version "2.0.14"
+ resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+ integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
mdn-polyfills@^5.20.0:
version "5.20.0"
resolved "https://registry.yarnpkg.com/mdn-polyfills/-/mdn-polyfills-5.20.0.tgz#ca8247edf20a4f60dec6804372229812b348260b"
@@ -1505,7 +1535,7 @@ source-map-js@^1.0.2:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
-source-map@~0.6.1:
+source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -1515,6 +1545,11 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+stable@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+ integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
string-width@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
@@ -1555,6 +1590,19 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+svgo@^2.8.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
+ integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
+ dependencies:
+ "@trysound/sax" "0.2.0"
+ commander "^7.2.0"
+ css-select "^4.1.3"
+ css-tree "^1.1.3"
+ csso "^4.2.0"
+ picocolors "^1.0.0"
+ stable "^0.1.8"
+
table@^6.0.9:
version "6.7.1"
resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2"
@@ -1622,10 +1670,10 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
-typescript@^4.4:
- version "4.6.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c"
- integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==
+typescript@^4.7.0:
+ version "4.7.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+ integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typeson-registry@^1.0.0-alpha.20:
version "1.0.0-alpha.39"