From 38c377486916b94f1e134e919e4f9ac543dbbe06 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 31 May 2022 15:30:56 -0500 Subject: [PATCH 01/44] Import assets from the assets/ directory > Will be easier towards the future when adding more assets. Probably best to keep style.css for now for backwards compat though. > > *-- https://github.com/vector-im/hydrogen-web/pull/693#discussion_r853844282* --- doc/SDK.md | 4 ++-- scripts/sdk/base-manifest.json | 2 -- scripts/sdk/test/esm-entry.ts | 2 +- scripts/sdk/test/test-sdk-in-commonjs-env.js | 2 +- vite.sdk-assets-config.js | 4 ++-- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/doc/SDK.md b/doc/SDK.md index 3f5bdb09..c8f5197f 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -48,8 +48,8 @@ const assetPaths = { wasmBundle: olmJsPath } }; -import "hydrogen-view-sdk/theme-element-light.css"; -// OR import "hydrogen-view-sdk/theme-element-dark.css"; +import "hydrogen-view-sdk/assets/theme-element-light.css"; +// OR import "hydrogen-view-sdk/assets/theme-element-dark.css"; async function main() { const app = document.querySelector('#app')! diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index de940752..441d4189 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -10,8 +10,6 @@ }, "./paths/vite": "./paths/vite.js", "./style.css": "./asset-build/assets/theme-element-light.css", - "./theme-element-light.css": "./asset-build/assets/theme-element-light.css", - "./theme-element-dark.css": "./asset-build/assets/theme-element-dark.css", "./main.js": "./asset-build/assets/main.js", "./download-sandbox.html": "./asset-build/assets/download-sandbox.html", "./assets/*": "./asset-build/assets/*" diff --git a/scripts/sdk/test/esm-entry.ts b/scripts/sdk/test/esm-entry.ts index 1f3b7114..17bbd8bb 100644 --- a/scripts/sdk/test/esm-entry.ts +++ b/scripts/sdk/test/esm-entry.ts @@ -13,7 +13,7 @@ const assetPaths = { wasmBundle: olmJsPath } }; -import "hydrogen-view-sdk/theme-element-light.css"; +import "hydrogen-view-sdk/assets/theme-element-light.css"; console.log('hydrogenViewSdk', hydrogenViewSdk); console.log('assetPaths', assetPaths); diff --git a/scripts/sdk/test/test-sdk-in-commonjs-env.js b/scripts/sdk/test/test-sdk-in-commonjs-env.js index 3fd19d46..333f1573 100644 --- a/scripts/sdk/test/test-sdk-in-commonjs-env.js +++ b/scripts/sdk/test/test-sdk-in-commonjs-env.js @@ -6,7 +6,7 @@ const hydrogenViewSdk = require('hydrogen-view-sdk'); // Worker require.resolve('hydrogen-view-sdk/main.js'); // Styles -require.resolve('hydrogen-view-sdk/theme-element-light.css'); +require.resolve('hydrogen-view-sdk/assets/theme-element-light.css'); // Can access files in the assets/* directory require.resolve('hydrogen-view-sdk/assets/main.js'); diff --git a/vite.sdk-assets-config.js b/vite.sdk-assets-config.js index beb7bb37..7174b8db 100644 --- a/vite.sdk-assets-config.js +++ b/vite.sdk-assets-config.js @@ -3,8 +3,8 @@ const mergeOptions = require('merge-options'); const themeBuilder = require("./scripts/build-plugins/rollup-plugin-build-themes"); const {commonOptions, compiledVariables} = require("./vite.common-config.js"); -// These paths will be saved without their hash so they havea consisent path to -// reference +// These paths will be saved without their hash so they have a consisent path to +// reference in imports. const pathsToExport = [ "main.js", "download-sandbox.html", From 9d8a578dce4018afabecaaa79c38c415e62d0d46 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 31 May 2022 15:35:48 -0500 Subject: [PATCH 02/44] Better comment --- vite.sdk-assets-config.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vite.sdk-assets-config.js b/vite.sdk-assets-config.js index 7174b8db..d7d4d064 100644 --- a/vite.sdk-assets-config.js +++ b/vite.sdk-assets-config.js @@ -3,8 +3,9 @@ const mergeOptions = require('merge-options'); const themeBuilder = require("./scripts/build-plugins/rollup-plugin-build-themes"); const {commonOptions, compiledVariables} = require("./vite.common-config.js"); -// These paths will be saved without their hash so they have a consisent path to -// reference in imports. +// These paths will be saved without their hash so they have a consisent path +// that we can reference in our `package.json` `exports`. And so people can import +// them with a consistent path. const pathsToExport = [ "main.js", "download-sandbox.html", @@ -21,7 +22,8 @@ export default mergeOptions(commonOptions, { output: { assetFileNames: (chunkInfo) => { // Get rid of the hash so we can consistently reference these - // files in our `package.json` `exports` + // files in our `package.json` `exports`. And so people can + // import them with a consistent path. if(pathsToExport.includes(path.basename(chunkInfo.name))) { return "assets/[name].[ext]"; } From 69d8e6031ef25975b95864446c17f8e095fd586a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Jun 2022 19:26:59 +0530 Subject: [PATCH 03/44] This isn't used anywhere --- src/domain/session/settings/SettingsViewModel.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index eed18953..d844e21b 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -187,9 +187,5 @@ export class SettingsViewModel extends ViewModel { // emit so that radio-buttons become displayed/hidden this.emitChange("themeOption"); } - - get preferredColorScheme() { - return this.platform.themeLoader.preferredColorScheme; - } } From d00ea39dc4f8306c5b656574ce3e958f0d342e99 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Jun 2022 19:27:18 +0530 Subject: [PATCH 04/44] No need to throw here --- src/platform/web/ThemeLoader.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 6a73c565..8c9364bc 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -196,13 +196,12 @@ export class ThemeLoader { } } - get preferredColorScheme(): ColorSchemePreference { + 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; } - throw new Error("Cannot find preferred colorscheme!"); } } From 4ed7e01dfd96030733be826bc6a7fbfcc17c02bc Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Jun 2022 16:00:35 +0200 Subject: [PATCH 05/44] release v0.2.31 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9b14eb7..63218407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.30", + "version": "0.2.31", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From a644621889617f17caee3633122d6906a852e9f6 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 14 Jun 2022 18:46:02 +0200 Subject: [PATCH 06/44] basic support for sending rageshake in view model --- src/domain/rageshake.ts | 69 +++++++++++++++++++ .../session/settings/SettingsViewModel.js | 16 +++++ src/matrix/net/common.ts | 8 ++- src/platform/web/assets/config.json | 3 +- src/platform/web/dom/request/fetch.js | 16 ++++- 5 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/domain/rageshake.ts diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts new file mode 100644 index 00000000..e79422cd --- /dev/null +++ b/src/domain/rageshake.ts @@ -0,0 +1,69 @@ +/* +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. +*/ + +import {encodeBody} from "../matrix/net/common"; + +import type {BlobHandle} from "../platform/web/dom/BlobHandle"; +import type {RequestFunction} from "../platform/types/types"; + +// see https://github.com/matrix-org/rageshake#readme +type RageshakeData = { + // A textual description of the problem. Included in the details.log.gz file. + text: string | undefined; + // Application user-agent. Included in the details.log.gz file. + userAgent: string; + // Identifier for the application (eg 'riot-web'). Should correspond to a mapping configured in the configuration file for github issue reporting to work. + app: string; + // Application version. Included in the details.log.gz file. + version: string; + // Label to attach to the github issue, and include in the details file. + label: string | undefined; +}; + +export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise { + const formData = new Map(); + if (data.text) { + formData.set("text", data.text); + } + formData.set("user_agent", data.userAgent); + formData.set("app", data.app); + formData.set("version", data.version); + if (data.label) { + formData.set("label", data.label); + } + formData.set("file", {name: "logs.json", blob: logsBlob}); + const encoded = encodeBody(formData); + const headers: Map = new Map(); + headers.set("Accept", "application/json"); + //headers.set("Content-Type", encoded.mimeType); + const result = request(submitUrl, { + method: "POST", + body: encoded.body, + headers + }); + let response; + try { + response = await result.response(); + } catch (err) { + throw new Error(`Could not submit logs to ${submitUrl}, got error ${err.message}`); + } + const {status, body} = response; + if (status >= 200 && status < 300) { + return body.report_url; + } else { + throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`); + } +} diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index d844e21b..b3ca74ca 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -16,6 +16,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel"; import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; +import {submitLogsToRageshakeServer} from "../../../domain/rageshake"; class PushNotificationStatus { constructor() { @@ -152,6 +153,21 @@ export class SettingsViewModel extends ViewModel { this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`); } + async sendLogsToServer() { + const logExport = await this.logger.export(); + await submitLogsToRageshakeServer( + { + app: "hydrogen", + userAgent: "", + version: DEFINE_VERSION, + text: "Submit logs from settings", + }, + logExport.asBlob(), + this.platform.config.bugReportEndpointUrl, + this.platform.request + ); + } + async togglePushNotifications() { this.pushNotifications.updating = true; this.pushNotifications.enabledOnServer = null; diff --git a/src/matrix/net/common.ts b/src/matrix/net/common.ts index 4ba42395..0b35a3e3 100644 --- a/src/matrix/net/common.ts +++ b/src/matrix/net/common.ts @@ -19,7 +19,8 @@ import {BlobHandle} from "../../platform/web/dom/BlobHandle.js"; export type EncodedBody = { mimeType: string; - body: BlobHandle | string; + // the map gets transformed to a FormData object on the web + body: BlobHandle | string | Map; } export function encodeQueryParams(queryParams?: object): string { @@ -41,6 +42,11 @@ export function encodeBody(body: BlobHandle | object): EncodedBody { mimeType: blob.mimeType, body: blob // will be unwrapped in request fn }; + } else if (body instanceof Map) { + return { + mimeType: "multipart/form-data", + body: body + } } else if (typeof body === "object") { const json = JSON.stringify(body); return { diff --git a/src/platform/web/assets/config.json b/src/platform/web/assets/config.json index 703ae1e6..fd46fcbc 100644 --- a/src/platform/web/assets/config.json +++ b/src/platform/web/assets/config.json @@ -4,5 +4,6 @@ "gatewayUrl": "https://matrix.org", "applicationServerKey": "BC-gpSdVHEXhvHSHS0AzzWrQoukv2BE7KzpoPO_FfPacqOo3l1pdqz7rSgmB04pZCWaHPz7XRe6fjLaC-WPDopM" }, - "defaultHomeServer": "matrix.org" + "defaultHomeServer": "matrix.org", + "bugReportEndpointUrl": "https://element.io/bugreports/submit" } diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index adf833ef..1d2ef3c0 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -64,12 +64,26 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { if (requestOptions?.uploadProgress) { return xhrRequest(url, requestOptions); } - let {method, headers, body, timeout, format, cache = false} = requestOptions; + let {method, headers, body, formData, timeout, format, cache = false} = requestOptions; const controller = typeof AbortController === "function" ? new AbortController() : null; // if a BlobHandle, take native blob if (body?.nativeBlob) { body = body.nativeBlob; } + if (body instanceof Map) { + const formData = new FormData(); + for (const [name, value] of body) { + let filename; + // Special case {name: string, blob: BlobHandle} to set a filename. + // This is the format returned by platform.openFile + if (value.blob?.nativeBlob && value.name) { + formData.set(name, value.blob.nativeBlob, value.name); + } else { + formData.set(name, value); + } + } + body = formData; + } let options = {method, body}; if (controller) { options = Object.assign(options, { From d0375141f88fe4ec6d02b820343820dc9b1cd420 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Jun 2022 12:11:15 +0530 Subject: [PATCH 07/44] WIP - write type for manifest --- src/platform/types/theme.ts | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/platform/types/theme.ts diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts new file mode 100644 index 00000000..40c60f75 --- /dev/null +++ b/src/platform/types/theme.ts @@ -0,0 +1,65 @@ +/* +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 type ThemeManifest = { + // Version number of theme; must be incremented on each change! + version: number; + // A user-facing string that is the name for this theme-collection. + name: string; + /** + * This is produced during the build process and includes data + * that is needed to load themes at runtime. + */ + source?: { + /** + * This is mapping from theme-id to location of css file relative to build-output root. + * eg: {"element-light": "assets/theme-element-light.10f9bb22.css", ...} + * + * Here theme-id is 'theme-variant' where 'theme' is the key used to specify the manifest + * location for this theme-collection in vite.config.js (where the themeBuilder plugin is + * initialized) and 'variant' is the key used to specify the variant details in the values + * section below. + */ + "built-asset": Record; + // Location of css file that will be used for themes derived from this theme. + "runtime-asset": string; + // Array of derived-variables + "derived-variables": Array; + }; + values: { + /** + * Mapping from variant key to details pertaining to this theme-variant. + * This variant key is used for forming theme-id as mentioned above. + */ + variants: Record; + }; +}; + +type Variant = { + base: boolean; + /** + * If true, this variant is used a default dark/light variant and will be the selected theme + * when "Match system theme" is selected for this theme collection in settings. + */ + default: boolean; + // A user-facing string that is the name for this variant. + name: string; + /** + * Mapping from css variable to its value. + * eg: {"background-color-primary": "#21262b", ...} + * */ + variables: Record; +} From 4caabae895948379afc1f9540260401c64457af9 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 10:15:15 +0200 Subject: [PATCH 08/44] extract map -> formdata conversion and also suppor this for xhr --- src/platform/web/dom/request/common.js | 15 +++++++++++++++ src/platform/web/dom/request/fetch.js | 15 ++------------- src/platform/web/dom/request/xhr.js | 5 ++++- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/platform/web/dom/request/common.js b/src/platform/web/dom/request/common.js index d97456aa..c073cf6b 100644 --- a/src/platform/web/dom/request/common.js +++ b/src/platform/web/dom/request/common.js @@ -27,6 +27,21 @@ export function addCacheBuster(urlStr, random = Math.random) { return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`; } +export function mapAsFormData(map) { + const formData = new FormData(); + for (const [name, value] of map) { + let filename; + // Special case {name: string, blob: BlobHandle} to set a filename. + // This is the format returned by platform.openFile + if (value.blob?.nativeBlob && value.name) { + formData.set(name, value.blob.nativeBlob, value.name); + } else { + formData.set(name, value); + } + } + return formData; +} + export function tests() { return { "add cache buster": assert => { diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index 1d2ef3c0..d50f6194 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -20,7 +20,7 @@ import { ConnectionError } from "../../../../matrix/error.js"; import {abortOnTimeout} from "../../../../utils/timeout"; -import {addCacheBuster} from "./common.js"; +import {addCacheBuster, mapAsFormData} from "./common.js"; import {xhrRequest} from "./xhr.js"; class RequestResult { @@ -71,18 +71,7 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { body = body.nativeBlob; } if (body instanceof Map) { - const formData = new FormData(); - for (const [name, value] of body) { - let filename; - // Special case {name: string, blob: BlobHandle} to set a filename. - // This is the format returned by platform.openFile - if (value.blob?.nativeBlob && value.name) { - formData.set(name, value.blob.nativeBlob, value.name); - } else { - formData.set(name, value); - } - } - body = formData; + body = mapAsFormData(body); } let options = {method, body}; if (controller) { diff --git a/src/platform/web/dom/request/xhr.js b/src/platform/web/dom/request/xhr.js index c3ad8ae8..fba7123e 100644 --- a/src/platform/web/dom/request/xhr.js +++ b/src/platform/web/dom/request/xhr.js @@ -18,7 +18,7 @@ import { AbortError, ConnectionError } from "../../../../matrix/error.js"; -import {addCacheBuster} from "./common.js"; +import {addCacheBuster, mapAsFormData} from "./common.js"; class RequestResult { constructor(promise, xhr) { @@ -94,6 +94,9 @@ export function xhrRequest(url, options) { if (body?.nativeBlob) { body = body.nativeBlob; } + if (body instanceof Map) { + body = mapAsFormData(body); + } xhr.send(body || null); return new RequestResult(promise, xhr); From 2129a97588346a87b9bf02a24abab7b2813696cd Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:12:49 +0200 Subject: [PATCH 09/44] remove unused param --- src/domain/rageshake.ts | 4 +--- src/platform/web/dom/request/fetch.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts index e79422cd..b1468bf5 100644 --- a/src/domain/rageshake.ts +++ b/src/domain/rageshake.ts @@ -61,9 +61,7 @@ export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: throw new Error(`Could not submit logs to ${submitUrl}, got error ${err.message}`); } const {status, body} = response; - if (status >= 200 && status < 300) { - return body.report_url; - } else { + if (status < 200 || status >= 300) { throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`); } } diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index d50f6194..497ad553 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -64,7 +64,7 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { if (requestOptions?.uploadProgress) { return xhrRequest(url, requestOptions); } - let {method, headers, body, formData, timeout, format, cache = false} = requestOptions; + let {method, headers, body, timeout, format, cache = false} = requestOptions; const controller = typeof AbortController === "function" ? new AbortController() : null; // if a BlobHandle, take native blob if (body?.nativeBlob) { From 69ada73dd4a4234aa375e7d782cf6632ba929120 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:13:05 +0200 Subject: [PATCH 10/44] cleanup rageshake code --- src/domain/rageshake.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts index b1468bf5..a1f3fd39 100644 --- a/src/domain/rageshake.ts +++ b/src/domain/rageshake.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {encodeBody} from "../matrix/net/common"; - import type {BlobHandle} from "../platform/web/dom/BlobHandle"; import type {RequestFunction} from "../platform/types/types"; @@ -45,13 +43,11 @@ export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: formData.set("label", data.label); } formData.set("file", {name: "logs.json", blob: logsBlob}); - const encoded = encodeBody(formData); const headers: Map = new Map(); headers.set("Accept", "application/json"); - //headers.set("Content-Type", encoded.mimeType); const result = request(submitUrl, { method: "POST", - body: encoded.body, + body: formData, headers }); let response; @@ -64,4 +60,6 @@ export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: if (status < 200 || status >= 300) { throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`); } + // we don't bother with reading report_url from the body as the rageshake server doesn't always return it + // and would have to have CORS setup properly for us to be able to read it. } From 375d8b066c3fef937a7adf2904d8edceeb6a3f52 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:13:46 +0200 Subject: [PATCH 11/44] complete settings view model for logs ui --- .../session/settings/SettingsViewModel.js | 55 +++++++++++++++---- src/matrix/net/common.ts | 4 +- src/platform/types/types.ts | 4 +- src/platform/web/Platform.js | 4 ++ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index b3ca74ca..4dcdb111 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -52,6 +52,7 @@ export class SettingsViewModel extends ViewModel { this.maxSentImageSizeLimit = 4000; this.pushNotifications = new PushNotificationStatus(); this._activeTheme = undefined; + this._logsFeedbackMessage = undefined; } get _session() { @@ -153,19 +154,49 @@ export class SettingsViewModel extends ViewModel { this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`); } + get canSendLogsToServer() { + return !!this.platform.config.bugReportEndpointUrl; + } + + get logsServer() { + const {bugReportEndpointUrl} = this.platform.config; + try { + if (bugReportEndpointUrl) { + return new URL(bugReportEndpointUrl).hostname; + } + } catch (e) {} + return ""; + } + async sendLogsToServer() { - const logExport = await this.logger.export(); - await submitLogsToRageshakeServer( - { - app: "hydrogen", - userAgent: "", - version: DEFINE_VERSION, - text: "Submit logs from settings", - }, - logExport.asBlob(), - this.platform.config.bugReportEndpointUrl, - this.platform.request - ); + const {bugReportEndpointUrl} = this.platform.config; + if (bugReportEndpointUrl) { + this._logsFeedbackMessage = this.i18n`Sending logs…`; + this.emitChange(); + try { + const logExport = await this.logger.export(); + await submitLogsToRageshakeServer( + { + app: "hydrogen", + userAgent: this.platform.description, + version: DEFINE_VERSION, + text: `Submit logs from settings for user ${this._session.userId} on device ${this._session.deviceId}`, + }, + logExport.asBlob(), + bugReportEndpointUrl, + this.platform.request + ); + this._logsFeedbackMessage = this.i18n`Logs sent succesfully!`; + this.emitChange(); + } catch (err) { + this._logsFeedbackMessage = err.message; + this.emitChange(); + } + } + } + + get logsFeedbackMessage() { + return this._logsFeedbackMessage; } async togglePushNotifications() { diff --git a/src/matrix/net/common.ts b/src/matrix/net/common.ts index 0b35a3e3..9f0fade4 100644 --- a/src/matrix/net/common.ts +++ b/src/matrix/net/common.ts @@ -17,10 +17,12 @@ limitations under the License. import {BlobHandle} from "../../platform/web/dom/BlobHandle.js"; +export type RequestBody = BlobHandle | string | Map; + export type EncodedBody = { mimeType: string; // the map gets transformed to a FormData object on the web - body: BlobHandle | string | Map; + body: RequestBody } export function encodeQueryParams(queryParams?: object): string { diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index da8ec8e7..1d359a09 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -15,13 +15,13 @@ limitations under the License. */ import type {RequestResult} from "../web/dom/request/fetch.js"; -import type {EncodedBody} from "../../matrix/net/common"; +import type {RequestBody} from "../../matrix/net/common"; import type {ILogItem} from "../../logging/types"; export interface IRequestOptions { uploadProgress?: (loadedBytes: number) => void; timeout?: number; - body?: EncodedBody; + body?: RequestBody; headers?: Map; cache?: boolean; method?: string; diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index dfb26b0f..e2ca2028 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -345,6 +345,10 @@ export class Platform { head.appendChild(styleTag); } + get description() { + return navigator.userAgent ?? ""; + } + dispose() { this._disposables.dispose(); } From 8fe8981ffa914957037c75a41f1714821509e3b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:14:06 +0200 Subject: [PATCH 12/44] add options to send logs to server in settings ui --- src/platform/web/ui/general/utils.ts | 8 ++++++++ src/platform/web/ui/session/settings/SettingsView.js | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/utils.ts b/src/platform/web/ui/general/utils.ts index f8d407e9..b310571f 100644 --- a/src/platform/web/ui/general/utils.ts +++ b/src/platform/web/ui/general/utils.ts @@ -54,3 +54,11 @@ export function insertAt(parentNode: Element, idx: number, childNode: Node): voi export function removeChildren(parentNode: Element): void { parentNode.innerHTML = ''; } + +export function disableTargetCallback(callback: (evt: Event) => Promise): (evt: Event) => Promise { + return async (evt: Event) => { + (evt.target as HTMLElement)?.setAttribute("disabled", "disabled"); + await callback(evt); + (evt.target as HTMLElement)?.removeAttribute("disabled"); + } +} diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index cf09ddb5..c4405e82 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; +import {disableTargetCallback} from "../../general/utils"; import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js" export class SettingsView extends TemplateView { @@ -101,11 +102,17 @@ export class SettingsView extends TemplateView { return row(t, vm.i18n`Use the following theme`, this._themeOptions(t, vm)); }), ); + const logButtons = []; + if (vm.canSendLogsToServer) { + logButtons.push(t.button({onClick: disableTargetCallback(() => vm.sendLogsToServer())}, `Submit logs to ${vm.logsServer}`)); + } + logButtons.push(t.button({onClick: () => vm.exportLogs()}, "Download logs")); settingNodes.push( t.h3("Application"), row(t, vm.i18n`Version`, version), row(t, vm.i18n`Storage usage`, vm => `${vm.storageUsage} / ${vm.storageQuota}`), - row(t, vm.i18n`Debug logs`, t.button({onClick: () => vm.exportLogs()}, "Export")), + row(t, vm.i18n`Debug logs`, logButtons), + t.p({className: {hidden: vm => !vm.logsFeedbackMessage}}, vm => vm.logsFeedbackMessage), t.p(["Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, the usernames of other users and the names of files you send. They do not contain messages. For more information, review our ", t.a({href: "https://element.io/privacy", target: "_blank", rel: "noopener"}, "privacy policy"), "."]), ); From 3b66ed8c17cc26fe6771bd3fb24b01053c5f97a2 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:24:16 +0200 Subject: [PATCH 13/44] fix type --- src/domain/rageshake.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/rageshake.ts b/src/domain/rageshake.ts index a1f3fd39..cb06e638 100644 --- a/src/domain/rageshake.ts +++ b/src/domain/rageshake.ts @@ -31,7 +31,7 @@ type RageshakeData = { label: string | undefined; }; -export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise { +export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise { const formData = new Map(); if (data.text) { formData.set("text", data.text); From 623939c671bbaffb9fde60aa36089f3f71f73d2e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:29:29 +0200 Subject: [PATCH 14/44] release v0.2.32 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63218407..eed0b229 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.31", + "version": "0.2.32", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From 9fbe8a4e324476d62d22de31fdded079d9f5a7a9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Jun 2022 15:02:15 +0530 Subject: [PATCH 15/44] Change description of version key --- src/platform/types/theme.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index 40c60f75..84203c78 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -15,7 +15,10 @@ limitations under the License. */ export type ThemeManifest = { - // Version number of theme; must be incremented on each change! + /** + * Version number of the theme manifest. + * This must be incremented when backwards incompatible changes are introduced. + */ version: number; // A user-facing string that is the name for this theme-collection. name: string; From b00bbc7daf3401957fcd24df28795832cd58562a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Jun 2022 15:03:41 +0530 Subject: [PATCH 16/44] Fix formatting --- src/platform/types/theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index 84203c78..9b433910 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -63,6 +63,6 @@ type Variant = { /** * Mapping from css variable to its value. * eg: {"background-color-primary": "#21262b", ...} - * */ + */ variables: Record; } From 48da6c782c25901d696a9177719c95dece4f39e6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Jun 2022 15:04:12 +0530 Subject: [PATCH 17/44] Remove base key --- src/platform/types/theme.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index 9b433910..a0578f10 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -52,7 +52,6 @@ export type ThemeManifest = { }; type Variant = { - base: boolean; /** * If true, this variant is used a default dark/light variant and will be the selected theme * when "Match system theme" is selected for this theme collection in settings. From 7a3eabf39cc313a759c980ff8fa84e2014c89ef8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Jun 2022 15:04:33 +0530 Subject: [PATCH 18/44] Formatting fix --- src/platform/types/theme.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index a0578f10..b01e6d5b 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -30,11 +30,11 @@ export type ThemeManifest = { /** * This is mapping from theme-id to location of css file relative to build-output root. * eg: {"element-light": "assets/theme-element-light.10f9bb22.css", ...} - * + * * Here theme-id is 'theme-variant' where 'theme' is the key used to specify the manifest - * location for this theme-collection in vite.config.js (where the themeBuilder plugin is + * location for this theme-collection in vite.config.js (where the themeBuilder plugin is * initialized) and 'variant' is the key used to specify the variant details in the values - * section below. + * section below. */ "built-asset": Record; // Location of css file that will be used for themes derived from this theme. From f658dc2e4b88abce5366fa8e83d8ff46caf6e1f9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Jun 2022 15:06:16 +0530 Subject: [PATCH 19/44] Make comment clearer --- src/platform/types/theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index b01e6d5b..3e587e30 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -23,7 +23,7 @@ export type ThemeManifest = { // A user-facing string that is the name for this theme-collection. name: string; /** - * This is produced during the build process and includes data + * This is added to the manifest during the build process and includes data * that is needed to load themes at runtime. */ source?: { From d322f380ada0d77ab2e30ae6bb7672b7c7b7c332 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 16 Jun 2022 21:26:16 +0530 Subject: [PATCH 20/44] Fix typo here This was causing the icons section to be omitted from the source section of the manifest. --- vite.common-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.common-config.js b/vite.common-config.js index 8a82a9da..5d65f8e2 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -40,7 +40,7 @@ const commonOptions = { postcss: { plugins: [ compileVariables({derive, compiledVariables}), - urlVariables({compileVariables}), + urlVariables({compiledVariables}), urlProcessor({replacer}), // cssvariables({ // preserve: (declaration) => { From cfd347335ba80cccf5ff99675ff42d6841a9ff06 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 16 Jun 2022 21:29:33 +0530 Subject: [PATCH 21/44] Move scope of variables down This was causing icons to be repeated in the css-file --- scripts/postcss/css-url-to-variables.js | 27 +++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 1d4666f4..82ddae82 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -20,11 +20,9 @@ const valueParser = require("postcss-value-parser"); * This plugin extracts content inside url() into css variables and adds the variables to the root section. * This plugin is used in conjunction with css-url-processor plugin to colorize svg icons. */ -let counter; -let urlVariables; const idToPrepend = "icon-url"; -function findAndReplaceUrl(decl) { +function findAndReplaceUrl(decl, urlVariables, counter) { const value = decl.value; const parsed = valueParser(value); parsed.walk(node => { @@ -35,7 +33,8 @@ function findAndReplaceUrl(decl) { if (!url.match(/\.svg\?primary=.+/)) { return; } - const variableName = `${idToPrepend}-${counter++}`; + const count = counter.next().value; + const variableName = `${idToPrepend}-${count}`; urlVariables.set(variableName, url); node.value = "var"; node.nodes = [{ type: "word", value: `--${variableName}` }]; @@ -43,7 +42,7 @@ function findAndReplaceUrl(decl) { decl.assign({prop: decl.prop, value: parsed.toString()}) } -function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { +function addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add derived css variables to :root urlVariables.forEach((value, key) => { @@ -53,29 +52,35 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { root.append(newRule); } -function populateMapWithIcons(map, cssFileLocation) { +function populateMapWithIcons(map, cssFileLocation, urlVariables) { const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; const sharedObject = map.get(location); sharedObject["icon"] = Object.fromEntries(urlVariables); } +function *createCounter() { + for (let i = 0; ; ++i) { + yield i; + } +} + /* * * @type {import('postcss').PluginCreator} */ module.exports = (opts = {}) => { - urlVariables = new Map(); - counter = 0; return { postcssPlugin: "postcss-url-to-variable", Once(root, { Rule, Declaration }) { - root.walkDecls(decl => findAndReplaceUrl(decl)); + const urlVariables = new Map(); + const counter = createCounter(); + root.walkDecls(decl => findAndReplaceUrl(decl, urlVariables, counter)); if (urlVariables.size) { - addResolvedVariablesToRootSelector(root, { Rule, Declaration }); + addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables); } if (opts.compiledVariables){ const cssFileLocation = root.source.input.from; - populateMapWithIcons(opts.compiledVariables, cssFileLocation); + populateMapWithIcons(opts.compiledVariables, cssFileLocation, urlVariables); } }, }; From 09b2437e726109d881bd014e12ef24ae83adeb59 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 17 Jun 2022 16:35:18 +0530 Subject: [PATCH 22/44] Move scope of variables down in compile-variables --- scripts/postcss/css-compile-variables.js | 36 +++++++++++------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 0f58a635..63aef97f 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -30,12 +30,7 @@ const valueParser = require("postcss-value-parser"); * The actual derivation is done outside the plugin in a callback. */ -let aliasMap; -let resolvedMap; -let baseVariables; -let isDark; - -function getValueFromAlias(alias) { +function getValueFromAlias(alias, {aliasMap, baseVariables, resolvedMap}) { const derivedVariable = aliasMap.get(alias); return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable); } @@ -68,14 +63,15 @@ function parseDeclarationValue(value) { return variables; } -function resolveDerivedVariable(decl, derive) { +function resolveDerivedVariable(decl, derive, maps, isDark) { + const { baseVariables, resolvedMap } = maps; const RE_VARIABLE_VALUE = /(?:--)?((.+)--(.+)-(.+))/; const variableCollection = parseDeclarationValue(decl.value); for (const variable of variableCollection) { const matches = variable.match(RE_VARIABLE_VALUE); if (matches) { const [, wholeVariable, baseVariable, operation, argument] = matches; - const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable); + const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable, maps); if (!value) { throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`); } @@ -85,7 +81,7 @@ function resolveDerivedVariable(decl, derive) { } } -function extract(decl) { +function extract(decl, {aliasMap, baseVariables}) { if (decl.variable) { // see if right side is of form "var(--foo)" const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1]; @@ -100,7 +96,7 @@ function extract(decl) { } } -function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { +function addResolvedVariablesToRootSelector(root, {Rule, Declaration}, {resolvedMap}) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add derived css variables to :root resolvedMap.forEach((value, key) => { @@ -110,7 +106,7 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { root.append(newRule); } -function populateMapWithDerivedVariables(map, cssFileLocation) { +function populateMapWithDerivedVariables(map, cssFileLocation, {resolvedMap, aliasMap}) { const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; const derivedVariables = [ ...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))), @@ -133,10 +129,10 @@ function populateMapWithDerivedVariables(map, cssFileLocation) { * @param {Map} opts.compiledVariables - A map that stores derived variables so that manifest source sections can be produced */ module.exports = (opts = {}) => { - aliasMap = new Map(); - resolvedMap = new Map(); - baseVariables = new Map(); - isDark = false; + const aliasMap = new Map(); + const resolvedMap = new Map(); + const baseVariables = new Map(); + const maps = { aliasMap, resolvedMap, baseVariables }; return { postcssPlugin: "postcss-compile-variables", @@ -147,16 +143,16 @@ module.exports = (opts = {}) => { // If this is a runtime theme, don't derive variables. return; } - isDark = cssFileLocation.includes("dark=true"); + const isDark = cssFileLocation.includes("dark=true"); /* Go through the CSS file once to extract all aliases and base variables. We use these when resolving derived variables later. */ - root.walkDecls(decl => extract(decl)); - root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive)); - addResolvedVariablesToRootSelector(root, {Rule, Declaration}); + root.walkDecls(decl => extract(decl, maps)); + root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive, maps, isDark)); + addResolvedVariablesToRootSelector(root, {Rule, Declaration}, maps); if (opts.compiledVariables){ - populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation); + populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation, maps); } // Also produce a mapping from alias to completely resolved color const resolvedAliasMap = new Map(); From cc29dc045d1106cf3fc9154268aec7a839bf9913 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 17 Jun 2022 16:38:13 +0530 Subject: [PATCH 23/44] Move scope down in css-url-processor --- scripts/postcss/css-url-processor.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/postcss/css-url-processor.js b/scripts/postcss/css-url-processor.js index f58818f1..8308e106 100644 --- a/scripts/postcss/css-url-processor.js +++ b/scripts/postcss/css-url-processor.js @@ -16,7 +16,6 @@ limitations under the License. const valueParser = require("postcss-value-parser"); const resolve = require("path").resolve; -let cssPath; function colorsFromURL(url, colorMap) { const params = new URL(`file://${url}`).searchParams; @@ -36,7 +35,7 @@ function colorsFromURL(url, colorMap) { return [primaryColor, secondaryColor]; } -function processURL(decl, replacer, colorMap) { +function processURL(decl, replacer, colorMap, cssPath) { const value = decl.value; const parsed = valueParser(value); parsed.walk(node => { @@ -84,8 +83,8 @@ module.exports = (opts = {}) => { Go through each declaration and if it contains an URL, replace the url with the result of running replacer(url) */ - cssPath = root.source?.input.file.replace(/[^/]*$/, ""); - root.walkDecls(decl => processURL(decl, opts.replacer, colorMap)); + const cssPath = root.source?.input.file.replace(/[^/]*$/, ""); + root.walkDecls(decl => processURL(decl, opts.replacer, colorMap, cssPath)); }, }; }; From 34eac94da3d2c5cf4c700f76ab0dc294b61bd02a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Jun 2022 21:27:02 +0530 Subject: [PATCH 24/44] Make everything optional Now typescript will force us to validate everything. --- src/platform/types/theme.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index 3e587e30..1c8ee7c3 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -export type ThemeManifest = { +export type ThemeManifest = Partial<{ /** * Version number of the theme manifest. * This must be incremented when backwards incompatible changes are introduced. @@ -26,7 +26,7 @@ export type ThemeManifest = { * This is added to the manifest during the build process and includes data * that is needed to load themes at runtime. */ - source?: { + source: { /** * This is mapping from theme-id to location of css file relative to build-output root. * eg: {"element-light": "assets/theme-element-light.10f9bb22.css", ...} @@ -49,9 +49,9 @@ export type ThemeManifest = { */ variants: Record; }; -}; +}>; -type Variant = { +type Variant = Partial<{ /** * If true, this variant is used a default dark/light variant and will be the selected theme * when "Match system theme" is selected for this theme collection in settings. @@ -64,4 +64,4 @@ type Variant = { * eg: {"background-color-primary": "#21262b", ...} */ variables: Record; -} +}>; From 0dfd24af22954c557cf2cc685ce00a2614159c64 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Jun 2022 12:52:10 +0530 Subject: [PATCH 25/44] Update info on path path is now relative to the manifest! --- src/platform/types/theme.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index 1c8ee7c3..9a984277 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -28,8 +28,9 @@ export type ThemeManifest = Partial<{ */ source: { /** - * This is mapping from theme-id to location of css file relative to build-output root. - * eg: {"element-light": "assets/theme-element-light.10f9bb22.css", ...} + * This is a mapping from theme-id to location of css file relative to the location of the + * manifest. + * eg: {"element-light": "theme-element-light.10f9bb22.css", ...} * * Here theme-id is 'theme-variant' where 'theme' is the key used to specify the manifest * location for this theme-collection in vite.config.js (where the themeBuilder plugin is From 9cb7d89097d337247876adfac7d48b291e27bf3c Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 24 Jun 2022 13:27:09 +0100 Subject: [PATCH 26/44] Require node 15. We use replaceAll in scripts/postcss/svg-colorizer.js which is a ES2021 feature. https://node.green/#ES2021-features--String-prototype-replaceAll --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index eed0b229..f4f4273a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "directories": { "doc": "doc" }, + "enginesStrict": { + "node": ">=15" + }, "scripts": { "lint": "eslint --cache src/", "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", From 84bac0afe95e95ce49cb38f583ac933a1ae8b0ba Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Sat, 25 Jun 2022 19:33:47 +0200 Subject: [PATCH 27/44] Also allow undefined, which means at the end of the paginated direction we already detect the end by chunk.length===0, so we just need to not throw --- src/matrix/room/timeline/persistence/GapWriter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/persistence/GapWriter.js b/src/matrix/room/timeline/persistence/GapWriter.js index 3e520608..4458e1c5 100644 --- a/src/matrix/room/timeline/persistence/GapWriter.js +++ b/src/matrix/room/timeline/persistence/GapWriter.js @@ -163,7 +163,7 @@ export class GapWriter { if (!Array.isArray(chunk)) { throw new Error("Invalid chunk in response"); } - if (typeof end !== "string") { + if (typeof end !== "string" && typeof end !== "undefined") { throw new Error("Invalid end token in response"); } From 7430aa7aab7f9652c38672ab11539ace9503dfbd Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Sat, 25 Jun 2022 20:14:32 +0200 Subject: [PATCH 28/44] allow download media in media view model --- .../room/timeline/tiles/BaseMediaTile.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMediaTile.js b/src/domain/session/room/timeline/tiles/BaseMediaTile.js index 0ba5b9a9..aa53661c 100644 --- a/src/domain/session/room/timeline/tiles/BaseMediaTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMediaTile.js @@ -27,6 +27,29 @@ export class BaseMediaTile extends BaseMessageTile { this._decryptedFile = null; this._isVisible = false; this._error = null; + this._downloading = false; + this._downloadError = null; + } + + async downloadMedia() { + if (this._downloading || this.isPending) { + return; + } + const content = this._getContent(); + const filename = content.body; + this._downloading = true; + this.emitChange("status"); + let blob; + try { + blob = await this._mediaRepository.downloadAttachment(content); + this.platform.saveFileAs(blob, filename); + } catch (err) { + this._downloadError = err; + } finally { + blob?.dispose(); + this._downloading = false; + } + this.emitChange("status"); } get isUploading() { @@ -38,7 +61,7 @@ export class BaseMediaTile extends BaseMessageTile { return pendingEvent && Math.round((pendingEvent.attachmentsSentBytes / pendingEvent.attachmentsTotalBytes) * 100); } - get sendStatus() { + get status() { const {pendingEvent} = this._entry; switch (pendingEvent?.status) { case SendStatus.Waiting: @@ -53,6 +76,12 @@ export class BaseMediaTile extends BaseMessageTile { case SendStatus.Error: return this.i18n`Error: ${pendingEvent.error.message}`; default: + if (this._downloadError) { + return `Download failed`; + } + if (this._downloading) { + return this.i18n`Downloading…`; + } return ""; } } From 3369bda2f09857c53a51171d402893d7c37e2646 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Sat, 25 Jun 2022 20:14:55 +0200 Subject: [PATCH 29/44] offer menu options to download media also always show status (before sendStatus), not just when isPending as we are recycling it to show download status as well --- .../web/ui/css/themes/element/timeline.css | 4 +-- .../ui/session/room/timeline/BaseMediaView.js | 30 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index bac4b4a5..43c57d19 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -233,7 +233,7 @@ only loads when the top comes into view*/ align-self: stretch; } -.Timeline_messageBody .media > .sendStatus { +.Timeline_messageBody .media > .status { align-self: end; justify-self: start; font-size: 0.8em; @@ -251,7 +251,7 @@ only loads when the top comes into view*/ } .Timeline_messageBody .media > time, -.Timeline_messageBody .media > .sendStatus { +.Timeline_messageBody .media > .status { color: var(--text-color); display: block; padding: 2px; diff --git a/src/platform/web/ui/session/room/timeline/BaseMediaView.js b/src/platform/web/ui/session/room/timeline/BaseMediaView.js index c52fbaed..16b5de14 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMediaView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMediaView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {BaseMessageView} from "./BaseMessageView.js"; +import {Menu} from "../../../general/Menu.js"; export class BaseMediaView extends BaseMessageView { renderMessageBody(t, vm) { @@ -35,24 +36,39 @@ export class BaseMediaView extends BaseMessageView { this.renderMedia(t, vm), t.time(vm.date + " " + vm.time), ]; + const status = t.div({ + className: { + status: true, + hidden: vm => !vm.status + }, + }, vm => vm.status); + children.push(status); if (vm.isPending) { - const sendStatus = t.div({ - className: { - sendStatus: true, - hidden: vm => !vm.sendStatus - }, - }, vm => vm.sendStatus); const progress = t.progress({ min: 0, max: 100, value: vm => vm.uploadPercentage, className: {hidden: vm => !vm.isUploading} }); - children.push(sendStatus, progress); + children.push(progress); } return t.div({className: "Timeline_messageBody"}, [ t.div({className: "media", style: `max-width: ${vm.width}px`}, children), t.if(vm => vm.error, t => t.p({className: "error"}, vm.error)) ]); } + + createMenuOptions(vm) { + const options = super.createMenuOptions(vm); + if (!vm.isPending) { + let label; + switch (vm.shape) { + case "image": label = vm.i18n`Download image`; break; + case "video": label = vm.i18n`Download video`; break; + default: label = vm.i18n`Download media`; break; + } + options.push(Menu.option(label, () => vm.downloadMedia())); + } + return options; + } } From 5b54280ac298ef022db61c5644b17111fdd3b422 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 28 Jun 2022 12:08:24 +0200 Subject: [PATCH 30/44] Ignore macOS metadata .DS_Store (#770) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 089600eb..78f9f348 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.sublime-project *.sublime-workspace +.DS_Store node_modules fetchlogs sessionexports From ccfd63dfebcab29e1cf4d49790c690ac725850a6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 28 Jun 2022 16:35:30 +0200 Subject: [PATCH 31/44] Restore backwards compatible theme paths See https://github.com/vector-im/hydrogen-web/pull/746#discussion_r901347536 --- scripts/sdk/base-manifest.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index 441d4189..de940752 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -10,6 +10,8 @@ }, "./paths/vite": "./paths/vite.js", "./style.css": "./asset-build/assets/theme-element-light.css", + "./theme-element-light.css": "./asset-build/assets/theme-element-light.css", + "./theme-element-dark.css": "./asset-build/assets/theme-element-dark.css", "./main.js": "./asset-build/assets/main.js", "./download-sandbox.html": "./asset-build/assets/download-sandbox.html", "./assets/*": "./asset-build/assets/*" From c59f65e43b03894782a79d3513fb82bb61dc8bb7 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 29 Jun 2022 12:56:20 +0200 Subject: [PATCH 32/44] Add a couple consistent selectors to reference in tests Using `data-testid` because it seems generic out of the list from: - https://docs.cypress.io/guides/core-concepts/cypress-app#Uniqueness - https://docs.cypress.io/guides/references/best-practices#How-It-Works --- src/platform/web/ui/avatar.js | 2 +- src/platform/web/ui/session/room/timeline/BaseMediaView.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 5bc019cb..547d4b51 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -31,7 +31,7 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { avatarClasses += ` ${extraClasses}`; } const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); - const avatar = tag.div({className: avatarClasses}, [avatarContent]); + const avatar = tag.div({className: avatarClasses, "data-testid": "avatar"}, [avatarContent]); if (hasAvatar) { setAttribute(avatar, "data-avatar-letter", vm.avatarLetter); setAttribute(avatar, "data-avatar-color", vm.avatarColorNumber); diff --git a/src/platform/web/ui/session/room/timeline/BaseMediaView.js b/src/platform/web/ui/session/room/timeline/BaseMediaView.js index 16b5de14..9d534fd1 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMediaView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMediaView.js @@ -53,7 +53,7 @@ export class BaseMediaView extends BaseMessageView { children.push(progress); } return t.div({className: "Timeline_messageBody"}, [ - t.div({className: "media", style: `max-width: ${vm.width}px`}, children), + t.div({className: "media", style: `max-width: ${vm.width}px`, "data-testid": "media"}, children), t.if(vm => vm.error, t => t.p({className: "error"}, vm.error)) ]); } From 4929839fe92a1ffac9332a4e7af8922e0e5c7e14 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Jun 2022 10:51:11 +0200 Subject: [PATCH 33/44] release v0.2.33 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4f4273a..c54be1ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.32", + "version": "0.2.33", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From 73cd96fe3ac903cda46d79cb849480e3bf2b0208 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Jun 2022 10:54:00 +0200 Subject: [PATCH 34/44] abort release script on error --- scripts/release.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.sh b/scripts/release.sh index e11bdf14..3357a42b 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,3 +1,4 @@ +set -e if [ -z "$1" ]; then echo "provide a new version, current version is $(jq '.version' package.json)" exit 1 From bb923b8eb971b89cfcce62bd10a0447a4a037e48 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 30 Jun 2022 10:54:11 +0200 Subject: [PATCH 35/44] bump sdk version --- scripts/sdk/base-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index de940752..a62888a7 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.0.12", + "version": "0.0.13", "main": "./lib-build/hydrogen.cjs.js", "exports": { ".": { From e90e573bf90f5298ab281335c17f200b42d35629 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 17 Jun 2022 12:45:19 +0530 Subject: [PATCH 36/44] Add doc --- doc/THEMING.md | 171 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 doc/THEMING.md diff --git a/doc/THEMING.md b/doc/THEMING.md new file mode 100644 index 00000000..ef4cdc7f --- /dev/null +++ b/doc/THEMING.md @@ -0,0 +1,171 @@ +# Theming Documentation +## Basic Architecture +A **theme collection** in Hydrogen is represented by a `manifest.json` file and a `theme.css` file. +The manifest specifies variants (eg: dark,light ...) each of which is a **theme** and maps to a single css file in the build output. + +Each such theme is produced by changing the values of variables in the base `theme.css` file with those specified in the variant section of the manifest: + +![](https://i.imgur.com/xepi7sx.png) + +More in depth explanations can be found in later sections. + +## Structure of `manifest.json` +Link to hydrogen-web/theme.ts + +## Variables +CSS variables specific to a particular variant are specified in the `variants` section of the manifest: +```json= + "variants": { + "light": { + ... + "variables": { + "background-color-primary": "#fff", + "text-color": "#2E2F32", + } + }, + "dark": { + ... + "variables": { + "background-color-primary": "#21262b", + "text-color": "#fff", + } + } + } +``` + +These variables will appear in the css file (theme.css): +```css= +body { + background-color: var(--background-color-primary); + color: var(--text-color); +} +``` + +During the build process, this would result in the creation of two css files (one for each variant) where the variables are substitued with the corresponding values specified in the manifest: + +*element-light.css*: +```css= +body { + background-color: #fff; + color: #2E2F32; +} +``` + +*element-dark.css*: +```css= +body { + background-color: #21262b; + color: #fff; +} +``` + +## Derived Variables +In addition to simple substituion of variables in the stylesheet, it is also possible to instruct the build system to first produce a new value from the base variable value before the substitution. + +Such derived variables have the form `base_css_variable--operation-arg` and can be read as: +apply `operation` to `base_css_variable` with argument `arg`. + +Continuing with the previous example, it possible to specify: +```css= +.left-panel { + /* background color should be 20% more darker + than background-color-primary */ + background-color: var(--background-color-primary--darker-20); +} +``` + +Currently supported operations are: + +| Operation | Argument | Operates On | +| -------- | -------- | -------- | +| darker | percentage | color | +| lighter | percentage | color | + +## Aliases +It is possible give aliases to variables in the `theme.css` file: +```css= +:root { + font-size: 10px; + /* Theme aliases */ + --icon-color: var(--background-color-secondary--darker-40); +} +``` +It is possible to derive from these aliased variables: +```css= +div { + background: var(--icon-color--darker-20); + --my-alias: var(--icon-color--darker-20); + /* Derive from aliased variable */ + color: var(--my-alias--lighter-15); +} +``` + + +## Colorizing svgs +Along with a change in color-scheme, it may be necessary to change the colors in the svg icons and images. +This can be done by supplying the preferred colors with query parameters: +`my-awesome-logo.svg?primary=base-variable-1&secondary=base-variable-2` + +This instructs the build system to colorize the svg with the given primary and secondary colors. +`base-variable-1` and `base-variable-2` are the css-variables specified in the `variables` section of the manifest. + +For colorizing svgs, the source svg must use `#ff00ff` as the primary color and `#00ffff` as the secondary color: + + + +| ![](https://i.imgur.com/Mda5YmQ.png) | ![](https://i.imgur.com/TENQIeK.png) | +| :--: |:--: | +| **original source image** | **transformation process** | + +## Creating your own theme variant in Hydrogen +If you're looking to change the color-scheme of the existing Element theme, you only need to add your own variant to the existing `manifest.json`. + +The steps are fairly simple: +1. Copy over an existing variant to the variants section of the manifest. +2. Change `dark`, `default` and `name` fields. +3. Give new values to each variable in the `variables` section. +4. Build hydrogen. + +## Creating your own theme collection in Hydrogen +If a theme variant does not solve your needs, you can create a new theme collection with a different base `theme.css` file. +1. Create a directory for your new theme-collection under `src/platform/web/ui/css/themes/`. +2. Create `manifest.json` and `theme.css` files within the newly created directory. +3. Populate `manifest.json` with the base css variables you wish to use. +4. Write styles in your `theme.css` file using the base variables, derived variables and colorized svg icons. +5. Tell the build system where to find this theme-collection by providing the location of this directory to the `themeBuilder` plugin in `vite.config.js`: +```json= +... +themeBuilder({ + themeConfig: { + themes: { + element: "./src/platform/web/ui/css/themes/element", + awesome: "path/to/theme-directory" + }, + default: "element", + }, + compiledVariables, +}), +... +``` +6. Build Hydrogen. + +## Changing the default theme +To change the default theme used in Hydrogen, modify the `defaultTheme` field in `config.json` file (which can be found in the build output): +```json= +"defaultTheme": { + "light": theme-id, + "dark": theme-id +} +``` + +Here *theme-id* is of the form `theme-variant` where `theme` is the key used when specifying the manifest location of the theme collection in `vite.config.js` and `variant` is the key used in variants section of the manifest. + +Some examples of theme-ids are `element-dark` and `element-light`. + +To find the theme-id of some theme, you can look at the built-asset section of the manifest in the build output. + +This default theme will render as "Default" option in the theme-chooser dropdown. If the device preference is for dark theme, the dark default is selected and vice versa. + +**You'll need to reload twice so that Hydrogen picks up the config changes!** + + From b319c0acb07b3ae25d774664b38439297fa277b2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 17 Jun 2022 12:47:12 +0530 Subject: [PATCH 37/44] Remvoe stray newlines --- doc/THEMING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/THEMING.md b/doc/THEMING.md index ef4cdc7f..38ccd2dd 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -167,5 +167,3 @@ To find the theme-id of some theme, you can look at the built-asset section of t This default theme will render as "Default" option in the theme-chooser dropdown. If the device preference is for dark theme, the dark default is selected and vice versa. **You'll need to reload twice so that Hydrogen picks up the config changes!** - - From da8747099603846dc586030af43994b8b686d17f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 20 Jun 2022 21:35:56 +0530 Subject: [PATCH 38/44] Store images in source tree --- doc/THEMING.md | 4 ++-- doc/images/coloring-process.png | Bin 0 -> 7939 bytes doc/images/svg-icon-example.png | Bin 0 -> 4318 bytes doc/images/theming-architecture.png | Bin 0 -> 19667 bytes 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 doc/images/coloring-process.png create mode 100644 doc/images/svg-icon-example.png create mode 100644 doc/images/theming-architecture.png diff --git a/doc/THEMING.md b/doc/THEMING.md index 38ccd2dd..0dd05973 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -5,7 +5,7 @@ The manifest specifies variants (eg: dark,light ...) each of which is a **theme* Each such theme is produced by changing the values of variables in the base `theme.css` file with those specified in the variant section of the manifest: -![](https://i.imgur.com/xepi7sx.png) +![](images/theming-architecture.png) More in depth explanations can be found in later sections. @@ -113,7 +113,7 @@ For colorizing svgs, the source svg must use `#ff00ff` as the primary color and -| ![](https://i.imgur.com/Mda5YmQ.png) | ![](https://i.imgur.com/TENQIeK.png) | +| ![](images/svg-icon-example.png) | ![](images/coloring-process.png) | | :--: |:--: | | **original source image** | **transformation process** | diff --git a/doc/images/coloring-process.png b/doc/images/coloring-process.png new file mode 100644 index 0000000000000000000000000000000000000000..5de0d79a1f40f55df9626fdccaf065778d4f0b1c GIT binary patch literal 7939 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 z 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 KvNh5c;r|B{_$Jl> literal 0 HcmV?d00001 diff --git a/doc/images/svg-icon-example.png b/doc/images/svg-icon-example.png new file mode 100644 index 0000000000000000000000000000000000000000..daf402587230c163a72ecac6aca8f53b53937ed1 GIT binary patch literal 4318 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 literal 0 HcmV?d00001 From d448ee1722fc560662647fdd4e5dba0e9083f90b Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Mon, 20 Jun 2022 21:37:51 +0530 Subject: [PATCH 39/44] Fix typo Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com> --- doc/THEMING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/THEMING.md b/doc/THEMING.md index 0dd05973..5c44ae59 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -60,7 +60,7 @@ body { ``` ## Derived Variables -In addition to simple substituion of variables in the stylesheet, it is also possible to instruct the build system to first produce a new value from the base variable value before the substitution. +In addition to simple substitution of variables in the stylesheet, it is also possible to instruct the build system to first produce a new value from the base variable value before the substitution. Such derived variables have the form `base_css_variable--operation-arg` and can be read as: apply `operation` to `base_css_variable` with argument `arg`. From b9f316e7c3d712ffb7bc119ba4c4c4044bfc9f24 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Mon, 20 Jun 2022 21:38:14 +0530 Subject: [PATCH 40/44] Better sentence structure Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com> --- doc/THEMING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/THEMING.md b/doc/THEMING.md index 5c44ae59..b463598f 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -90,7 +90,7 @@ It is possible give aliases to variables in the `theme.css` file: --icon-color: var(--background-color-secondary--darker-40); } ``` -It is possible to derive from these aliased variables: +It is possible to further derive from these aliased variables: ```css= div { background: var(--icon-color--darker-20); From a3c6d744f586f436e884c615bcffb4d03142e808 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 17:18:50 +0530 Subject: [PATCH 41/44] Add link to ts file --- doc/THEMING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/THEMING.md b/doc/THEMING.md index b463598f..b6af27cf 100644 --- a/doc/THEMING.md +++ b/doc/THEMING.md @@ -10,7 +10,7 @@ Each such theme is produced by changing the values of variables in the base `the More in depth explanations can be found in later sections. ## Structure of `manifest.json` -Link to hydrogen-web/theme.ts +[See theme.ts](../src/platform/types/theme.ts) ## Variables CSS variables specific to a particular variant are specified in the `variants` section of the manifest: From b76fd1d79284cd61d904b788acebddf90ad3c1ae Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 4 Jul 2022 15:39:11 +0200 Subject: [PATCH 42/44] update olm to 3.2.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c54be1ef..82588f10 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "xxhashjs": "^0.2.2" }, "dependencies": { - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", "dompurify": "^2.3.0", From c0445f218294c707a753fb4545bacdba62a224aa Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 4 Jul 2022 15:40:17 +0200 Subject: [PATCH 43/44] update lock file --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9d882bd2..28efc3bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,9 +52,9 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": - version "3.2.3" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz": + version "3.2.8" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856" "@nodelib/fs.scandir@2.1.5": version "2.1.5" From 34ce8a8e3c6fe0332aa403b90aa68d940d5ef736 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:15:59 +0200 Subject: [PATCH 44/44] fix lint --- src/platform/web/dom/request/common.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/web/dom/request/common.js b/src/platform/web/dom/request/common.js index c073cf6b..d6ed5074 100644 --- a/src/platform/web/dom/request/common.js +++ b/src/platform/web/dom/request/common.js @@ -30,7 +30,6 @@ export function addCacheBuster(urlStr, random = Math.random) { export function mapAsFormData(map) { const formData = new FormData(); for (const [name, value] of map) { - let filename; // Special case {name: string, blob: BlobHandle} to set a filename. // This is the format returned by platform.openFile if (value.blob?.nativeBlob && value.name) {