From 61ce2f9e3d1a408d8c8f6b0494d5b0e0687d0beb Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Thu, 3 Mar 2022 15:36:25 +0530 Subject: [PATCH 01/53] Add observeNavigation in ViewModel --- src/domain/ViewModel.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index cfe22326..04fec86e 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -58,6 +58,14 @@ export class ViewModel extends EventEmitter<{change return this._options[name]; } + observeNavigation(type: string, onChange: (value: true | string, type: string) => void) { + const segmentObservable = this.navigation.observe(type); + const unsubscribe = segmentObservable.subscribe((value: true | string) => { + onChange(value, type); + }) + this.track(unsubscribe); + } + track(disposable: D): D { if (!this.disposables) { this.disposables = new Disposables(); From e07abfa02a543f1ddc9515d039f3fe56f81b5b88 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:33:51 +0530 Subject: [PATCH 02/53] Add missing type --- src/domain/ViewModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 04fec86e..8b8581ae 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -58,9 +58,9 @@ export class ViewModel extends EventEmitter<{change return this._options[name]; } - observeNavigation(type: string, onChange: (value: true | string, type: string) => void) { + observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void) { const segmentObservable = this.navigation.observe(type); - const unsubscribe = segmentObservable.subscribe((value: true | string) => { + const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => { onChange(value, type); }) this.track(unsubscribe); From e8bd1f33900851e972c06f8f0e01caa8f5b81caf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 27 Mar 2022 20:06:26 +0530 Subject: [PATCH 03/53] Pass result as message --- scripts/postcss/css-compile-variables.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 3ed34513..b48fc8fa 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -112,7 +112,7 @@ module.exports = (opts = {}) => { return { postcssPlugin: "postcss-compile-variables", - Once(root, {Rule, Declaration}) { + Once(root, {Rule, Declaration, result}) { /* Go through the CSS file once to extract all aliases and base variables. We use these when resolving derived variables later. @@ -120,6 +120,13 @@ module.exports = (opts = {}) => { root.walkDecls(decl => extract(decl)); root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive)); addResolvedVariablesToRootSelector(root, {Rule, Declaration}); + // Publish both the base-variables and derived-variables to the other postcss-plugins + const combinedMap = new Map([...baseVariables, ...resolvedMap]); + result.messages.push({ + type: "resolved-variable-map", + plugin: "postcss-compile-variables", + colorMap: combinedMap, + }) }, }; }; From 2015fa2d7a892777991f8326ca56d1d6e15534a1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 27 Mar 2022 20:18:42 +0530 Subject: [PATCH 04/53] Move postcss-value-parser to dev dependency --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 12c73994..d506d872 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "node-html-parser": "^4.0.0", "postcss-css-variables": "^0.18.0", "postcss-flexbugs-fixes": "^5.0.2", + "postcss-value-parser": "^4.2.0", "regenerator-runtime": "^0.13.7", "text-encoding": "^0.7.0", "typescript": "^4.3.5", @@ -54,7 +55,6 @@ "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", "dompurify": "^2.3.0", - "off-color": "^2.0.0", - "postcss-value-parser": "^4.2.0" + "off-color": "^2.0.0" } } From 4350d2f264927bf655793a178e37ecdff8213494 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 1 Apr 2022 16:20:58 +0530 Subject: [PATCH 05/53] Don't derive variables for runtime theme --- scripts/postcss/css-compile-variables.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index b48fc8fa..f7d63fab 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -113,6 +113,11 @@ module.exports = (opts = {}) => { postcssPlugin: "postcss-compile-variables", Once(root, {Rule, Declaration, result}) { + const cssFileLocation = root.source.input.from; + if (cssFileLocation.includes("type=runtime")) { + // If this is a runtime theme, don't derive variables. + return; + } /* Go through the CSS file once to extract all aliases and base variables. We use these when resolving derived variables later. From 918a3e42b1cf809af11bf6f6feb076f48700fd73 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 1 Apr 2022 16:23:33 +0530 Subject: [PATCH 06/53] Populate compiled variables map --- scripts/postcss/css-compile-variables.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index f7d63fab..263fbe36 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -94,6 +94,23 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { root.append(newRule); } +function populateMapWithDerivedVariables(map, cssFileLocation) { + const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; + if (map.has(location)) { + /** + * This postcss plugin is going to run on all theme variants of a single theme. + * But we only really need to populate the map once since theme variants only differ + * by the values of the base-variables and we don't care about values here. + */ + return; + } + const derivedVariables = new Set([ + ...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))), + ...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`)) + ]); + map.set(location, { "derived-variables": derivedVariables }); +} + /** * @callback derive * @param {string} value - The base value on which an operation is applied @@ -104,6 +121,7 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { * * @param {Object} opts - Options for the plugin * @param {derive} opts.derive - The callback which contains the logic for resolving derived variables + * @param {Map} opts.compiledVariables - A map that stores derived variables so that manifest source sections can be produced */ module.exports = (opts = {}) => { aliasMap = new Map(); @@ -125,13 +143,16 @@ module.exports = (opts = {}) => { root.walkDecls(decl => extract(decl)); root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive)); addResolvedVariablesToRootSelector(root, {Rule, Declaration}); + if (opts.compiledVariables){ + populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation); + } // Publish both the base-variables and derived-variables to the other postcss-plugins const combinedMap = new Map([...baseVariables, ...resolvedMap]); result.messages.push({ type: "resolved-variable-map", plugin: "postcss-compile-variables", colorMap: combinedMap, - }) + }); }, }; }; From 859449ed60bd7ce7476d0fedac958fd1a62c3a17 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 1 Apr 2022 16:41:00 +0530 Subject: [PATCH 07/53] Write test for map population --- scripts/postcss/test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/postcss/test.js b/scripts/postcss/test.js index 36ff9282..d07f07e4 100644 --- a/scripts/postcss/test.js +++ b/scripts/postcss/test.js @@ -96,6 +96,7 @@ module.exports.tests = function tests() { `; await run( inputCSS, outputCSS, { }, assert); }, + "multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => { const inputCSS = ` :root { @@ -116,6 +117,23 @@ module.exports.tests = function tests() { } `; await run( inputCSS, outputCSS, { }, assert); + }, + + "compiledVariables map is populated": async (assert) => { + const compiledVariables = new Map(); + const inputCSS = ` + :root { + --icon-color: #fff; + } + div { + background: var(--icon-color--darker-20); + --my-alias: var(--icon-color--darker-20); + color: var(--my-alias--lighter-15); + }`; + await postcss([plugin({ derive, compiledVariables })]).process(inputCSS, { from: "/foo/bar/test.css", }); + const actualSet = compiledVariables.get("/foo/bar")["derived-variables"]; + const expectedSet = new Set(["icon-color--darker-20", "my-alias=icon-color--darker-20", "my-alias--lighter-15"]); + assert.deepEqual(actualSet, expectedSet); } }; }; From 76789eacf154a7e172bd3572480b94a8604cdf16 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 1 Apr 2022 20:43:42 +0530 Subject: [PATCH 08/53] Use array instead of Set --- scripts/postcss/css-compile-variables.js | 4 ++-- scripts/postcss/test.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 263fbe36..7152535e 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -104,10 +104,10 @@ function populateMapWithDerivedVariables(map, cssFileLocation) { */ return; } - const derivedVariables = new Set([ + const derivedVariables = [ ...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))), ...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`)) - ]); + ]; map.set(location, { "derived-variables": derivedVariables }); } diff --git a/scripts/postcss/test.js b/scripts/postcss/test.js index d07f07e4..8d1412ea 100644 --- a/scripts/postcss/test.js +++ b/scripts/postcss/test.js @@ -131,9 +131,9 @@ module.exports.tests = function tests() { color: var(--my-alias--lighter-15); }`; await postcss([plugin({ derive, compiledVariables })]).process(inputCSS, { from: "/foo/bar/test.css", }); - const actualSet = compiledVariables.get("/foo/bar")["derived-variables"]; - const expectedSet = new Set(["icon-color--darker-20", "my-alias=icon-color--darker-20", "my-alias--lighter-15"]); - assert.deepEqual(actualSet, expectedSet); + const actualArray = compiledVariables.get("/foo/bar")["derived-variables"]; + const expectedArray = ["icon-color--darker-20", "my-alias=icon-color--darker-20", "my-alias--lighter-15"]; + assert.deepStrictEqual(actualArray.sort(), expectedArray.sort()); } }; }; From 454345c9b2bb9e3e965cd9e36374da95973164fb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 5 Apr 2022 15:08:35 +0530 Subject: [PATCH 09/53] Always set map --- scripts/postcss/css-compile-variables.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 7152535e..7302f8d4 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -96,14 +96,6 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) { function populateMapWithDerivedVariables(map, cssFileLocation) { const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; - if (map.has(location)) { - /** - * This postcss plugin is going to run on all theme variants of a single theme. - * But we only really need to populate the map once since theme variants only differ - * by the values of the base-variables and we don't care about values here. - */ - return; - } const derivedVariables = [ ...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))), ...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`)) From 48d0242c80fe63073277f47ab36fc7a14dfb256b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 6 Apr 2022 12:23:55 +0530 Subject: [PATCH 10/53] Also derive variables in URLs --- scripts/postcss/css-compile-variables.js | 23 ++++++++++++++++++---- scripts/postcss/test.js | 25 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/scripts/postcss/css-compile-variables.js b/scripts/postcss/css-compile-variables.js index 7302f8d4..fa584caa 100644 --- a/scripts/postcss/css-compile-variables.js +++ b/scripts/postcss/css-compile-variables.js @@ -43,17 +43,32 @@ function parseDeclarationValue(value) { const parsed = valueParser(value); const variables = []; parsed.walk(node => { - if (node.type !== "function" && node.value !== "var") { + if (node.type !== "function") { return; } - const variable = node.nodes[0]; - variables.push(variable.value); + switch (node.value) { + case "var": { + const variable = node.nodes[0]; + variables.push(variable.value); + break; + } + case "url": { + const url = node.nodes[0].value; + // resolve url with some absolute url so that we get the query params without using regex + const params = new URL(url, "file://foo/bar/").searchParams; + const primary = params.get("primary"); + const secondary = params.get("secondary"); + if (primary) { variables.push(primary); } + if (secondary) { variables.push(secondary); } + break; + } + } }); return variables; } function resolveDerivedVariable(decl, derive) { - const RE_VARIABLE_VALUE = /--((.+)--(.+)-(.+))/; + const RE_VARIABLE_VALUE = /(?:--)?((.+)--(.+)-(.+))/; const variableCollection = parseDeclarationValue(decl.value); for (const variable of variableCollection) { const matches = variable.match(RE_VARIABLE_VALUE); diff --git a/scripts/postcss/test.js b/scripts/postcss/test.js index 8d1412ea..cccb3ea7 100644 --- a/scripts/postcss/test.js +++ b/scripts/postcss/test.js @@ -134,6 +134,31 @@ module.exports.tests = function tests() { const actualArray = compiledVariables.get("/foo/bar")["derived-variables"]; const expectedArray = ["icon-color--darker-20", "my-alias=icon-color--darker-20", "my-alias--lighter-15"]; assert.deepStrictEqual(actualArray.sort(), expectedArray.sort()); + }, + + "derived variable are supported in urls": async (assert) => { + const inputCSS = ` + :root { + --foo-color: #ff0; + } + div { + background-color: var(--foo-color--lighter-50); + background: url("./foo/bar/icon.svg?primary=foo-color--darker-5"); + } + a { + background: url("foo/bar/icon.svg"); + }`; + const transformedColorLighter = offColor("#ff0").lighten(0.5); + const transformedColorDarker = offColor("#ff0").darken(0.05); + const outputCSS = + inputCSS + + ` + :root { + --foo-color--lighter-50: ${transformedColorLighter.hex()}; + --foo-color--darker-5: ${transformedColorDarker.hex()}; + } + `; + await run( inputCSS, outputCSS, {}, assert); } }; }; From 1f6efb4db3faa62cdf499c77c42db2d8fd86f5f0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 11:35:27 +0530 Subject: [PATCH 11/53] Write plugin code --- .../rollup-plugin-build-themes.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 scripts/build-plugins/rollup-plugin-build-themes.js diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js new file mode 100644 index 00000000..dd52ef77 --- /dev/null +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -0,0 +1,64 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +async function readCSSSource(location) { + const fs = require("fs").promises; + const path = require("path"); + const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`); + const data = await fs.readFile(resolvedLocation); + return data; +} + +async function appendVariablesToCSS(variables, cssSource) { + return cssSource + `:root{\n${Object.entries(variables).reduce((acc, [key, value]) => acc + `--${key}: ${value};\n`, "")} }\n\n`; +} + +module.exports = function buildThemes(options) { + let manifest, variants; + + return { + name: "build-themes", + enforce: "pre", + + async buildStart() { + const { manifestLocations } = options; + for (const location of manifestLocations) { + manifest = require(`${location}/manifest.json`); + variants = manifest.values.variants; + for (const [variant] of Object.entries(variants)) { + const themeName = manifest.name; + // emit the css as built theme bundle + this.emitFile({ + type: "chunk", + id: `${location}/theme.css?variant=${variant}`, + fileName: `theme-${themeName}-${variant}.css`, + }); + } + } + }, + + async load(id) { + const result = id.match(/(.+)\/theme.css\?variant=(.+)/); + if (result) { + const [, location, variant] = result; + const cssSource = await readCSSSource(location); + const config = variants[variant]; + return await appendVariablesToCSS(config.variables, cssSource); + } + return null; + }, + } +} From 32eb95734a6ca17241fda7bf577eaf9c2641cb69 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 28 Mar 2022 18:02:53 +0530 Subject: [PATCH 12/53] Add default themes to index html --- .../rollup-plugin-build-themes.js | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index dd52ef77..770f48b7 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -27,7 +27,7 @@ async function appendVariablesToCSS(variables, cssSource) { } module.exports = function buildThemes(options) { - let manifest, variants; + let manifest, variants, defaultDark, defaultLight; return { name: "build-themes", @@ -38,13 +38,23 @@ module.exports = function buildThemes(options) { for (const location of manifestLocations) { manifest = require(`${location}/manifest.json`); variants = manifest.values.variants; - for (const [variant] of Object.entries(variants)) { + for (const [variant, details] of Object.entries(variants)) { const themeName = manifest.name; + const fileName = `theme-${themeName}-${variant}.css`; + if (details.default) { + // This theme is the default for when Hydrogen launches for the first time + if (details.dark) { + defaultDark = fileName; + } + else { + defaultLight = fileName; + } + } // emit the css as built theme bundle this.emitFile({ type: "chunk", id: `${location}/theme.css?variant=${variant}`, - fileName: `theme-${themeName}-${variant}.css`, + fileName, }); } } @@ -60,5 +70,37 @@ module.exports = function buildThemes(options) { } return null; }, + + transformIndexHtml(_, ctx) { + let darkThemeLocation, lightThemeLocation; + for (const [, bundle] of Object.entries(ctx.bundle)) { + if (bundle.name === defaultDark) { + darkThemeLocation = bundle.fileName; + } + if (bundle.name === defaultLight) { + lightThemeLocation = bundle.fileName; + } + } + return [ + { + tag: "link", + attrs: { + rel: "stylesheet", + type: "text/css", + media: "(prefers-color-scheme: dark)", + href: `./${darkThemeLocation}`, + } + }, + { + tag: "link", + attrs: { + rel: "stylesheet", + type: "text/css", + media: "(prefers-color-scheme: light)", + href: `./${lightThemeLocation}`, + } + }, + ]; + } } } From 86c45b5b998685febc6f444e78b6e1da70937308 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 29 Mar 2022 11:46:06 +0530 Subject: [PATCH 13/53] Emit runtime bundle --- scripts/build-plugins/rollup-plugin-build-themes.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 770f48b7..0b9ec4bf 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -38,8 +38,8 @@ module.exports = function buildThemes(options) { for (const location of manifestLocations) { manifest = require(`${location}/manifest.json`); variants = manifest.values.variants; + const themeName = manifest.name; for (const [variant, details] of Object.entries(variants)) { - const themeName = manifest.name; const fileName = `theme-${themeName}-${variant}.css`; if (details.default) { // This theme is the default for when Hydrogen launches for the first time @@ -57,6 +57,12 @@ module.exports = function buildThemes(options) { fileName, }); } + // emit the css as runtime theme bundle + this.emitFile({ + type: "chunk", + id: `${location}/theme.css`, + fileName: `theme-${themeName}-runtime.css`, + }); } }, From d5b5e10230cd8d9c5272d9ef67c7bd0f7e0285b6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 1 Apr 2022 14:27:24 +0530 Subject: [PATCH 14/53] Produce manifest.jsom --- .../rollup-plugin-build-themes.js | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 0b9ec4bf..cd4dc495 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -26,6 +26,46 @@ async function appendVariablesToCSS(variables, cssSource) { return cssSource + `:root{\n${Object.entries(variables).reduce((acc, [key, value]) => acc + `--${key}: ${value};\n`, "")} }\n\n`; } +function parseBundle(bundle) { + const chunkMap = new Map(); + const assetMap = new Map(); + let runtimeThemeChunk; + for (const [fileName, info] of Object.entries(bundle)) { + if (!fileName.endsWith(".css")) { + continue; + } + if (info.type === "asset") { + /** + * So this is the css assetInfo that contains the asset hashed file name. + * We'll store it in a separate map indexed via fileName (unhashed) to avoid + * searching through the bundle array later. + */ + assetMap.set(info.name, info); + continue; + } + if (info.facadeModuleId?.includes("type=runtime")) { + /** + * We have a separate field in manifest.source just for the runtime theme, + * so store this separately. + */ + runtimeThemeChunk = info; + continue; + } + const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; + if (!location) { + throw new Error("Cannot find location of css chunk!"); + } + const array = chunkMap.get(location); + if (!array) { + chunkMap.set(location, [info]); + } + else { + array.push(info); + } + } + return { chunkMap, assetMap, runtimeThemeChunk }; +} + module.exports = function buildThemes(options) { let manifest, variants, defaultDark, defaultLight; @@ -60,7 +100,7 @@ module.exports = function buildThemes(options) { // emit the css as runtime theme bundle this.emitFile({ type: "chunk", - id: `${location}/theme.css`, + id: `${location}/theme.css?type=runtime`, fileName: `theme-${themeName}-runtime.css`, }); } @@ -107,6 +147,23 @@ module.exports = function buildThemes(options) { } }, ]; + }, + + generateBundle(_, bundle) { + const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); + for (const [location, chunkArray] of chunkMap) { + const manifest = require(`${location}/manifest.json`); + manifest.source = { + "built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName), + "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, + }; + const name = `theme-${manifest.name}.json`; + this.emitFile({ + type: "asset", + name, + source: JSON.stringify(manifest), + }); + } } } } From b0f082e81f555c8ba2faeefb565eb6098ea261a6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 1 Apr 2022 20:27:33 +0530 Subject: [PATCH 15/53] Add derived variables to source section --- scripts/build-plugins/rollup-plugin-build-themes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index cd4dc495..b5768e8c 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -153,9 +153,11 @@ module.exports = function buildThemes(options) { const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); for (const [location, chunkArray] of chunkMap) { const manifest = require(`${location}/manifest.json`); + const derivedVariables = options.compiledVariables.get(location)["derived-variables"]; manifest.source = { "built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName), "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, + "derived-variables": derivedVariables, }; const name = `theme-${manifest.name}.json`; this.emitFile({ From 7f9af5b5fa0ff932231c09247a043914e733890b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 3 Apr 2022 16:30:35 +0530 Subject: [PATCH 16/53] Add icon to manifest --- scripts/build-plugins/rollup-plugin-build-themes.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index b5768e8c..2355a136 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -153,11 +153,14 @@ module.exports = function buildThemes(options) { const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); for (const [location, chunkArray] of chunkMap) { const manifest = require(`${location}/manifest.json`); - const derivedVariables = options.compiledVariables.get(location)["derived-variables"]; + const compiledVariables = options.compiledVariables.get(location); + const derivedVariables = compiledVariables["derived-variables"]; + const icon = compiledVariables["icon"]; manifest.source = { "built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName), "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, "derived-variables": derivedVariables, + "icon": icon }; const name = `theme-${manifest.name}.json`; this.emitFile({ From f75ee86c0e30288aa5af9cf1906fe76a0ccfbd2c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 5 Apr 2022 16:27:23 +0530 Subject: [PATCH 17/53] Change comment --- scripts/build-plugins/rollup-plugin-build-themes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 2355a136..74fe4daf 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -82,7 +82,7 @@ module.exports = function buildThemes(options) { for (const [variant, details] of Object.entries(variants)) { const fileName = `theme-${themeName}-${variant}.css`; if (details.default) { - // This theme is the default for when Hydrogen launches for the first time + // This is one of the default variants for this theme. if (details.dark) { defaultDark = fileName; } From f897e5132c29532034d7fe3d59a1dc53e02c62a3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 14 Mar 2022 12:11:46 +0530 Subject: [PATCH 18/53] Implement url to variables plugin --- package.json | 2 +- scripts/postcss/css-url-to-variables.js | 54 +++++++++++++++++++ .../css-compile-variables.test.js} | 4 +- .../tests/css-url-to-variables.test.js | 48 +++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 scripts/postcss/css-url-to-variables.js rename scripts/postcss/{test.js => tests/css-compile-variables.test.js} (98%) create mode 100644 scripts/postcss/tests/css-url-to-variables.test.js diff --git a/package.json b/package.json index d506d872..af863ced 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "lint-ci": "eslint src/", "test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/", - "test:postcss": "impunity --entry-point scripts/postcss/test.js ", + "test:postcss": "impunity --entry-point scripts/postcss/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js", "start": "vite --port 3000", "build": "vite build", "build:sdk": "./scripts/sdk/build.sh" diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js new file mode 100644 index 00000000..a9370117 --- /dev/null +++ b/scripts/postcss/css-url-to-variables.js @@ -0,0 +1,54 @@ +const valueParser = require("postcss-value-parser"); +let counter = 0; +const variableMap = new Map(); +const format = "icon-url" + +function extractUrl(decl) { + const value = decl.value; + const parsed = valueParser(value); + const variables = []; + parsed.walk(node => { + if (node.type !== "function" || node.value !== "url") { + return; + } + const urlStringNode = node.nodes[0]; + const variableName = `--${format}-${counter++}`; + variableMap.set(variableName, `"${urlStringNode.value}"`); + const varNode = { + type: "function", + value: "var", + nodes: [{ type: "word", value: variableName }], + }; + //replace the url-string node with this var-node + node.nodes[0] = varNode; + }); + decl.assign({prop: decl.prop, value: parsed.toString()}) + return variables; +} + +function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { + const newRule = new Rule({ selector: ":root", source: root.source }); + // Add derived css variables to :root + variableMap.forEach((value, key) => { + const declaration = new Declaration({ prop: key, value }); + newRule.append(declaration); + }); + root.append(newRule); +} + +/* * + * @type {import('postcss').PluginCreator} + */ +module.exports = (opts = {}) => { + return { + postcssPlugin: "postcss-url-to-variable", + + Once(root, { Rule, Declaration }) { + root.walkDecls(decl => extractUrl(decl)); + addResolvedVariablesToRootSelector(root, { Rule, Declaration }); + }, + }; +}; + +module.exports.postcss = true; + diff --git a/scripts/postcss/test.js b/scripts/postcss/tests/css-compile-variables.test.js similarity index 98% rename from scripts/postcss/test.js rename to scripts/postcss/tests/css-compile-variables.test.js index cccb3ea7..945d5d4d 100644 --- a/scripts/postcss/test.js +++ b/scripts/postcss/tests/css-compile-variables.test.js @@ -16,8 +16,8 @@ limitations under the License. const offColor = require("off-color").offColor; const postcss = require("postcss"); -const plugin = require("./css-compile-variables"); -const derive = require("./color").derive; +const plugin = require("../css-compile-variables"); +const derive = require("../color").derive; async function run(input, output, opts = {}, assert) { let result = await postcss([plugin({ ...opts, derive })]).process(input, { from: undefined, }); diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js new file mode 100644 index 00000000..36a38345 --- /dev/null +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -0,0 +1,48 @@ +/* +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. +*/ + +const postcss = require("postcss"); +const plugin = require("../css-url-to-variables"); + +async function run(input, output, opts = {}, assert) { + let result = await postcss([plugin(opts)]).process(input, { from: undefined, }); + assert.strictEqual( + result.css.replaceAll(/\s/g, ""), + output.replaceAll(/\s/g, "") + ); + assert.strictEqual(result.warnings().length, 0); +} + +module.exports.tests = function tests() { + return { + "url is replaced with variable": async (assert) => { + const inputCSS = `div { + background: no-repeat center/80% url("../img/image.png"); + }`; + const outputCSS = + `div { + background: no-repeat center/80% url(var(--icon-url-0)); + }`+ + ` + :root { + --icon-url-0: "../img/image.png"; + } + `; + await run( inputCSS, outputCSS, { }, assert); + }, + }; +}; + From 3ae2b4dab4d2fe1a2cba37f1832541fb938909f3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 24 Mar 2022 16:34:15 +0530 Subject: [PATCH 19/53] Use two url() in test --- scripts/postcss/tests/css-url-to-variables.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js index 36a38345..645b1775 100644 --- a/scripts/postcss/tests/css-url-to-variables.test.js +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -31,14 +31,21 @@ module.exports.tests = function tests() { "url is replaced with variable": async (assert) => { const inputCSS = `div { background: no-repeat center/80% url("../img/image.png"); + } + button { + background: url("/home/foo/bar/cool.jpg"); }`; const outputCSS = `div { background: no-repeat center/80% url(var(--icon-url-0)); + } + button { + background: url(var(--icon-url-1)); }`+ ` :root { --icon-url-0: "../img/image.png"; + --icon-url-1: "/home/foo/bar/cool.jpg"; } `; await run( inputCSS, outputCSS, { }, assert); From cbff912476c19d9395bfd86f935bc9df80708fb4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 24 Mar 2022 16:36:01 +0530 Subject: [PATCH 20/53] Improve code quality --- scripts/postcss/css-url-to-variables.js | 35 ++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index a9370117..a1a47971 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -1,7 +1,23 @@ +/* +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. +*/ + const valueParser = require("postcss-value-parser"); -let counter = 0; -const variableMap = new Map(); -const format = "icon-url" +let counter; +const urlVariables = new Map(); +const idToPrepend = "icon-url"; function extractUrl(decl) { const value = decl.value; @@ -12,14 +28,14 @@ function extractUrl(decl) { return; } const urlStringNode = node.nodes[0]; - const variableName = `--${format}-${counter++}`; - variableMap.set(variableName, `"${urlStringNode.value}"`); + const variableName = `${idToPrepend}-${counter++}`; + urlVariables.set(variableName, `"${urlStringNode.value}"`); const varNode = { type: "function", value: "var", - nodes: [{ type: "word", value: variableName }], + nodes: [{ type: "word", value: `--${variableName}` }], }; - //replace the url-string node with this var-node + // replace the url-string node with this var-node node.nodes[0] = varNode; }); decl.assign({prop: decl.prop, value: parsed.toString()}) @@ -29,8 +45,8 @@ function extractUrl(decl) { function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add derived css variables to :root - variableMap.forEach((value, key) => { - const declaration = new Declaration({ prop: key, value }); + urlVariables.forEach((value, key) => { + const declaration = new Declaration({ prop: `--${key}`, value }); newRule.append(declaration); }); root.append(newRule); @@ -40,6 +56,7 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { * @type {import('postcss').PluginCreator} */ module.exports = (opts = {}) => { + counter = 0; return { postcssPlugin: "postcss-url-to-variable", From b59d6970fcdc91359097e07cc51df1f91ca02514 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 24 Mar 2022 16:52:26 +0530 Subject: [PATCH 21/53] Fix code duplication in tests --- scripts/postcss/tests/common.js | 30 +++++++++++++++++++ .../tests/css-compile-variables.test.js | 16 +++------- .../tests/css-url-to-variables.test.js | 11 +------ 3 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 scripts/postcss/tests/common.js diff --git a/scripts/postcss/tests/common.js b/scripts/postcss/tests/common.js new file mode 100644 index 00000000..78ae847e --- /dev/null +++ b/scripts/postcss/tests/common.js @@ -0,0 +1,30 @@ +/* +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. +*/ + +const postcss = require("postcss"); + +module.exports.createTestRunner = function (plugin) { + return async function run(input, output, opts = {}, assert) { + let result = await postcss([plugin(opts)]).process(input, { from: undefined, }); + assert.strictEqual( + result.css.replaceAll(/\s/g, ""), + output.replaceAll(/\s/g, "") + ); + assert.strictEqual(result.warnings().length, 0); + }; +} + + diff --git a/scripts/postcss/tests/css-compile-variables.test.js b/scripts/postcss/tests/css-compile-variables.test.js index 945d5d4d..e20e195f 100644 --- a/scripts/postcss/tests/css-compile-variables.test.js +++ b/scripts/postcss/tests/css-compile-variables.test.js @@ -18,15 +18,7 @@ const offColor = require("off-color").offColor; const postcss = require("postcss"); const plugin = require("../css-compile-variables"); const derive = require("../color").derive; - -async function run(input, output, opts = {}, assert) { - let result = await postcss([plugin({ ...opts, derive })]).process(input, { from: undefined, }); - assert.strictEqual( - result.css.replaceAll(/\s/g, ""), - output.replaceAll(/\s/g, "") - ); - assert.strictEqual(result.warnings().length, 0); -} +const run = require("./common").createTestRunner(plugin); module.exports.tests = function tests() { return { @@ -46,7 +38,7 @@ module.exports.tests = function tests() { --foo-color--lighter-50: ${transformedColor.hex()}; } `; - await run( inputCSS, outputCSS, {}, assert); + await run( inputCSS, outputCSS, {derive}, assert); }, "derived variables work with alias": async (assert) => { @@ -66,7 +58,7 @@ module.exports.tests = function tests() { --my-alias--lighter-15: ${aliasLighter}; } `; - await run(inputCSS, outputCSS, { }, assert); + await run(inputCSS, outputCSS, {derive}, assert); }, "derived variable throws if base not present in config": async (assert) => { @@ -94,7 +86,7 @@ module.exports.tests = function tests() { --foo-color--darker-20: ${transformedColor2.hex()}; } `; - await run( inputCSS, outputCSS, { }, assert); + await run( inputCSS, outputCSS, {derive}, assert); }, "multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => { diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js index 645b1775..0369995b 100644 --- a/scripts/postcss/tests/css-url-to-variables.test.js +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -14,17 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -const postcss = require("postcss"); const plugin = require("../css-url-to-variables"); - -async function run(input, output, opts = {}, assert) { - let result = await postcss([plugin(opts)]).process(input, { from: undefined, }); - assert.strictEqual( - result.css.replaceAll(/\s/g, ""), - output.replaceAll(/\s/g, "") - ); - assert.strictEqual(result.warnings().length, 0); -} +const run = require("./common").createTestRunner(plugin); module.exports.tests = function tests() { return { From 97ade0659cdcad78514c5dc42deacb11170ba3ca Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 11:40:21 +0530 Subject: [PATCH 22/53] Add explaining comment --- scripts/postcss/css-url-to-variables.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index a1a47971..07d524c9 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -15,6 +15,10 @@ limitations under the License. */ const valueParser = require("postcss-value-parser"); +/** + * This plugin extracts content inside url() into css variables. + * The extracted css variables are added to the :root section. + */ let counter; const urlVariables = new Map(); const idToPrepend = "icon-url"; From 6b4bb762aa830b19e69b6160ec57169d73484cf2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 11:40:35 +0530 Subject: [PATCH 23/53] Remove unused variable --- scripts/postcss/css-url-to-variables.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 07d524c9..79814cb2 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -26,7 +26,6 @@ const idToPrepend = "icon-url"; function extractUrl(decl) { const value = decl.value; const parsed = valueParser(value); - const variables = []; parsed.walk(node => { if (node.type !== "function" || node.value !== "url") { return; @@ -43,7 +42,6 @@ function extractUrl(decl) { node.nodes[0] = varNode; }); decl.assign({prop: decl.prop, value: parsed.toString()}) - return variables; } function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { From 2d4ec5380e9cf578e2d0f186e0e895f50b130bf3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 11:46:48 +0530 Subject: [PATCH 24/53] Initialize variables later --- scripts/postcss/css-url-to-variables.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 79814cb2..902e3e9d 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -15,12 +15,13 @@ limitations under the License. */ const valueParser = require("postcss-value-parser"); + /** * This plugin extracts content inside url() into css variables. * The extracted css variables are added to the :root section. */ let counter; -const urlVariables = new Map(); +let urlVariables; const idToPrepend = "icon-url"; function extractUrl(decl) { @@ -58,6 +59,7 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { * @type {import('postcss').PluginCreator} */ module.exports = (opts = {}) => { + urlVariables = new Map(); counter = 0; return { postcssPlugin: "postcss-url-to-variable", From f07a3ea5b5b05ca6afd9ee4df6ecbb457f60127d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 15:48:32 +0530 Subject: [PATCH 25/53] Remove css specific syntax from map --- scripts/postcss/css-url-to-variables.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 902e3e9d..dff72264 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -33,7 +33,7 @@ function extractUrl(decl) { } const urlStringNode = node.nodes[0]; const variableName = `${idToPrepend}-${counter++}`; - urlVariables.set(variableName, `"${urlStringNode.value}"`); + urlVariables.set(variableName, urlStringNode.value); const varNode = { type: "function", value: "var", @@ -49,7 +49,7 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add derived css variables to :root urlVariables.forEach((value, key) => { - const declaration = new Declaration({ prop: `--${key}`, value }); + const declaration = new Declaration({ prop: `--${key}`, value: `"${value}"`}); newRule.append(declaration); }); root.append(newRule); From 0a186dd11bc8fb7d14a76d7ad4a4efb753eb345b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 16:06:40 +0530 Subject: [PATCH 26/53] Fix css logic --- scripts/postcss/css-url-to-variables.js | 11 +++-------- scripts/postcss/tests/css-url-to-variables.test.js | 8 ++++---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index dff72264..34d21bcc 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -34,13 +34,8 @@ function extractUrl(decl) { const urlStringNode = node.nodes[0]; const variableName = `${idToPrepend}-${counter++}`; urlVariables.set(variableName, urlStringNode.value); - const varNode = { - type: "function", - value: "var", - nodes: [{ type: "word", value: `--${variableName}` }], - }; - // replace the url-string node with this var-node - node.nodes[0] = varNode; + node.value = "var"; + node.nodes = [{ type: "word", value: `--${variableName}` }]; }); decl.assign({prop: decl.prop, value: parsed.toString()}) } @@ -49,7 +44,7 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { const newRule = new Rule({ selector: ":root", source: root.source }); // Add derived css variables to :root urlVariables.forEach((value, key) => { - const declaration = new Declaration({ prop: `--${key}`, value: `"${value}"`}); + const declaration = new Declaration({ prop: `--${key}`, value: `url("${value}")`}); newRule.append(declaration); }); root.append(newRule); diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js index 0369995b..fe4d4865 100644 --- a/scripts/postcss/tests/css-url-to-variables.test.js +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -28,15 +28,15 @@ module.exports.tests = function tests() { }`; const outputCSS = `div { - background: no-repeat center/80% url(var(--icon-url-0)); + background: no-repeat center/80% var(--icon-url-0); } button { - background: url(var(--icon-url-1)); + background: var(--icon-url-1); }`+ ` :root { - --icon-url-0: "../img/image.png"; - --icon-url-1: "/home/foo/bar/cool.jpg"; + --icon-url-0: url("../img/image.png"); + --icon-url-1: url("/home/foo/bar/cool.jpg"); } `; await run( inputCSS, outputCSS, { }, assert); From b7a47ae901c148ced1f166aba0becda5bf7db596 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 16:08:15 +0530 Subject: [PATCH 27/53] Give function better name --- scripts/postcss/css-url-to-variables.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 34d21bcc..aff8eef9 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -24,7 +24,7 @@ let counter; let urlVariables; const idToPrepend = "icon-url"; -function extractUrl(decl) { +function findAndReplaceUrl(decl) { const value = decl.value; const parsed = valueParser(value); parsed.walk(node => { @@ -60,7 +60,7 @@ module.exports = (opts = {}) => { postcssPlugin: "postcss-url-to-variable", Once(root, { Rule, Declaration }) { - root.walkDecls(decl => extractUrl(decl)); + root.walkDecls(decl => findAndReplaceUrl(decl)); addResolvedVariablesToRootSelector(root, { Rule, Declaration }); }, }; From 1a50effd861fdf773578f231e3ee668a01fb1744 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 18:21:03 +0530 Subject: [PATCH 28/53] Only extract into variables if file is svg --- scripts/postcss/css-url-to-variables.js | 11 ++++++++--- .../postcss/tests/css-url-to-variables.test.js | 16 +++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index aff8eef9..2a2937c0 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -31,9 +31,12 @@ function findAndReplaceUrl(decl) { if (node.type !== "function" || node.value !== "url") { return; } - const urlStringNode = node.nodes[0]; + const url = node.nodes[0].value; + if (!url.match(/\.svg\?primary=.+/)) { + return; + } const variableName = `${idToPrepend}-${counter++}`; - urlVariables.set(variableName, urlStringNode.value); + urlVariables.set(variableName, url); node.value = "var"; node.nodes = [{ type: "word", value: `--${variableName}` }]; }); @@ -61,7 +64,9 @@ module.exports = (opts = {}) => { Once(root, { Rule, Declaration }) { root.walkDecls(decl => findAndReplaceUrl(decl)); - addResolvedVariablesToRootSelector(root, { Rule, Declaration }); + if (urlVariables.size) { + addResolvedVariablesToRootSelector(root, { Rule, Declaration }); + } }, }; }; diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js index fe4d4865..cc7d9cd2 100644 --- a/scripts/postcss/tests/css-url-to-variables.test.js +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -21,10 +21,10 @@ module.exports.tests = function tests() { return { "url is replaced with variable": async (assert) => { const inputCSS = `div { - background: no-repeat center/80% url("../img/image.png"); + background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20"); } button { - background: url("/home/foo/bar/cool.jpg"); + background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green"); }`; const outputCSS = `div { @@ -35,12 +35,18 @@ module.exports.tests = function tests() { }`+ ` :root { - --icon-url-0: url("../img/image.png"); - --icon-url-1: url("/home/foo/bar/cool.jpg"); + --icon-url-0: url("../img/image.svg?primary=main-color--darker-20"); + --icon-url-1: url("/home/foo/bar/cool.svg?primary=blue&secondary=green"); } `; - await run( inputCSS, outputCSS, { }, assert); + await run(inputCSS, outputCSS, { }, assert); }, + "non svg urls without query params are not replaced": async (assert) => { + const inputCSS = `div { + background: no-repeat url("./img/foo/bar/image.png"); + }`; + await run(inputCSS, inputCSS, {}, assert); + } }; }; From cd4fce0c6ff94b53b58d820f16c8b46597535fe3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 2 Apr 2022 23:09:20 +0530 Subject: [PATCH 29/53] Populate shared map with collected icons --- scripts/postcss/css-url-to-variables.js | 18 ++++++++++++++++++ .../postcss/tests/css-url-to-variables.test.js | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 2a2937c0..ddc65bed 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -53,6 +53,19 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { root.append(newRule); } +function populateMapWithDerivedVariables(map, cssFileLocation) { + const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; + if (map.has(location)) { + /** + * This postcss plugin is going to run on all theme variants of a single theme. + * But we only really need to populate the map once since theme variants only differ + * by the values of the base-variables and we don't care about values here. + */ + return; + } + map.set(location, { "icon": Object.fromEntries(urlVariables) }); +} + /* * * @type {import('postcss').PluginCreator} */ @@ -67,6 +80,11 @@ module.exports = (opts = {}) => { if (urlVariables.size) { addResolvedVariablesToRootSelector(root, { Rule, Declaration }); } + if (opts.compiledVariables){ + const cssFileLocation = root.source.input.from; + populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation); + } + console.log(opts.compiledVariables); }, }; }; diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js index cc7d9cd2..e298599d 100644 --- a/scripts/postcss/tests/css-url-to-variables.test.js +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -16,6 +16,7 @@ limitations under the License. const plugin = require("../css-url-to-variables"); const run = require("./common").createTestRunner(plugin); +const postcss = require("postcss"); module.exports.tests = function tests() { return { @@ -46,6 +47,21 @@ module.exports.tests = function tests() { background: no-repeat url("./img/foo/bar/image.png"); }`; await run(inputCSS, inputCSS, {}, assert); + }, + "map is populated with icons": async (assert) => { + const compiledVariables = new Map(); + const inputCSS = `div { + background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20"); + } + button { + background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green"); + }`; + const expectedObject = { + "icon-url-0": "../img/image.svg?primary=main-color--darker-20", + "icon-url-1": "/home/foo/bar/cool.svg?primary=blue&secondary=green", + }; + await postcss([plugin({compiledVariables})]).process(inputCSS, { from: "/foo/bar/test.css", }); + assert.deepEqual(expectedObject, compiledVariables.get("/foo/bar")["icon"]); } }; }; From 5e702171cebb11737a0128747bf855e3fe190b5f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 3 Apr 2022 16:44:50 +0530 Subject: [PATCH 30/53] Remove console.log --- scripts/postcss/css-url-to-variables.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index ddc65bed..6c58b093 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -84,7 +84,6 @@ module.exports = (opts = {}) => { const cssFileLocation = root.source.input.from; populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation); } - console.log(opts.compiledVariables); }, }; }; From 545ff2ec32f8cd654f7676fe1c7052700dbbd3af Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 3 Apr 2022 16:46:26 +0530 Subject: [PATCH 31/53] Add explaining comment --- scripts/postcss/css-url-to-variables.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 6c58b093..7e9f5759 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -17,8 +17,8 @@ limitations under the License. const valueParser = require("postcss-value-parser"); /** - * This plugin extracts content inside url() into css variables. - * The extracted css variables are added to the :root section. + * 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; From 9a96112146a86655a04f2b3ba9b695870ce3984d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Apr 2022 12:02:59 +0530 Subject: [PATCH 32/53] Rename function name --- scripts/postcss/css-url-to-variables.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 7e9f5759..598ac973 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -53,7 +53,7 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { root.append(newRule); } -function populateMapWithDerivedVariables(map, cssFileLocation) { +function populateMapWithIcons(map, cssFileLocation) { const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; if (map.has(location)) { /** @@ -82,7 +82,7 @@ module.exports = (opts = {}) => { } if (opts.compiledVariables){ const cssFileLocation = root.source.input.from; - populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation); + populateMapWithIcons(opts.compiledVariables, cssFileLocation); } }, }; From 2dd655cd9ab9b4aef111061eff6ca1a3270b839b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Apr 2022 12:10:54 +0530 Subject: [PATCH 33/53] Check if icon is in shared var --- scripts/postcss/css-url-to-variables.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 598ac973..4480a2b8 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -55,7 +55,8 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { function populateMapWithIcons(map, cssFileLocation) { const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; - if (map.has(location)) { + const sharedObject = map.get(location); + if (sharedObject?.["icon"]) { /** * This postcss plugin is going to run on all theme variants of a single theme. * But we only really need to populate the map once since theme variants only differ @@ -63,7 +64,7 @@ function populateMapWithIcons(map, cssFileLocation) { */ return; } - map.set(location, { "icon": Object.fromEntries(urlVariables) }); + map.set(location, { ...sharedObject, "icon": Object.fromEntries(urlVariables) }); } /* * From 6d724e27e70cd547e5d87cde06a8165d77765ad9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 5 Apr 2022 15:26:00 +0530 Subject: [PATCH 34/53] No need to check if icons are already written --- scripts/postcss/css-url-to-variables.js | 10 +--------- scripts/postcss/tests/css-url-to-variables.test.js | 5 ++++- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index 4480a2b8..1d4666f4 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -56,15 +56,7 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) { function populateMapWithIcons(map, cssFileLocation) { const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1]; const sharedObject = map.get(location); - if (sharedObject?.["icon"]) { - /** - * This postcss plugin is going to run on all theme variants of a single theme. - * But we only really need to populate the map once since theme variants only differ - * by the values of the base-variables and we don't care about values here. - */ - return; - } - map.set(location, { ...sharedObject, "icon": Object.fromEntries(urlVariables) }); + sharedObject["icon"] = Object.fromEntries(urlVariables); } /* * diff --git a/scripts/postcss/tests/css-url-to-variables.test.js b/scripts/postcss/tests/css-url-to-variables.test.js index e298599d..f406a38a 100644 --- a/scripts/postcss/tests/css-url-to-variables.test.js +++ b/scripts/postcss/tests/css-url-to-variables.test.js @@ -50,6 +50,7 @@ module.exports.tests = function tests() { }, "map is populated with icons": async (assert) => { const compiledVariables = new Map(); + compiledVariables.set("/foo/bar", { "derived-variables": ["background-color--darker-20", "accent-color--lighter-15"] }); const inputCSS = `div { background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20"); } @@ -61,7 +62,9 @@ module.exports.tests = function tests() { "icon-url-1": "/home/foo/bar/cool.svg?primary=blue&secondary=green", }; await postcss([plugin({compiledVariables})]).process(inputCSS, { from: "/foo/bar/test.css", }); - assert.deepEqual(expectedObject, compiledVariables.get("/foo/bar")["icon"]); + const sharedVariable = compiledVariables.get("/foo/bar"); + assert.deepEqual(["background-color--darker-20", "accent-color--lighter-15"], sharedVariable["derived-variables"]); + assert.deepEqual(expectedObject, sharedVariable["icon"]); } }; }; From bfd73ae52a9b6cad00d7b9b4e34343495ede798d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 7 Apr 2022 11:37:20 +0530 Subject: [PATCH 35/53] Pass derive function as argument --- scripts/postcss/tests/css-compile-variables.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/postcss/tests/css-compile-variables.test.js b/scripts/postcss/tests/css-compile-variables.test.js index e20e195f..e40751db 100644 --- a/scripts/postcss/tests/css-compile-variables.test.js +++ b/scripts/postcss/tests/css-compile-variables.test.js @@ -108,7 +108,7 @@ module.exports.tests = function tests() { --my-alias--darker-20: ${transformedColor2.hex()}; } `; - await run( inputCSS, outputCSS, { }, assert); + await run( inputCSS, outputCSS, {derive}, assert); }, "compiledVariables map is populated": async (assert) => { @@ -150,7 +150,7 @@ module.exports.tests = function tests() { --foo-color--darker-5: ${transformedColorDarker.hex()}; } `; - await run( inputCSS, outputCSS, {}, assert); + await run( inputCSS, outputCSS, {derive}, assert); } }; }; From 5d5eb93baac2d413e562f193a94304ffa6ae1c2f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 13:35:21 +0530 Subject: [PATCH 36/53] Implement plugin --- .gitignore | 3 +- scripts/postcss/css-url-processor.js | 81 ++++++++++++++++++++++++++++ scripts/postcss/svg-colorizer.js | 42 +++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 scripts/postcss/css-url-processor.js create mode 100644 scripts/postcss/svg-colorizer.js diff --git a/.gitignore b/.gitignore index 7f6220cf..089600eb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ bundle.js target lib *.tar.gz -.eslintcache \ No newline at end of file +.eslintcache +.tmp diff --git a/scripts/postcss/css-url-processor.js b/scripts/postcss/css-url-processor.js new file mode 100644 index 00000000..854b549f --- /dev/null +++ b/scripts/postcss/css-url-processor.js @@ -0,0 +1,81 @@ +/* +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. +*/ + +const valueParser = require("postcss-value-parser"); +const resolve = require("path").resolve; + +function colorsFromURL(url, colorVariables) { + const params = new URL(`file://${url}`).searchParams; + const primary = params.get("primary"); + if (!primary) { + return null; + } + const secondary = params.get("secondary"); + const primaryColor = colorVariables[primary]; + const secondaryColor = colorVariables[secondary]; + if (!primaryColor) { + throw new Error(`Variable ${primary} not found in resolved color variables!`); + } + if (!secondaryColor) { + throw new Error(`Variable ${secondary} not found in resolved color variables!`); + } + return [primaryColor, secondaryColor]; +} + +function processURL(decl, replacer, colorVariables) { + const value = decl.value; + const parsed = valueParser(value); + parsed.walk(async node => { + if (node.type !== "function" || node.value !== "url") { + return; + } + const urlStringNode = node.nodes[0]; + const oldURL = urlStringNode.value; + const cssPath = decl.source?.input.file.replace(/[^/]*$/, ""); + const oldURLAbsolute = resolve(cssPath, oldURL); + const colors = colorsFromURL(oldURLAbsolute, colorVariables); + if (!colors) { + // If no primary color is provided via url params, then this url need not be handled. + return; + } + const newURL = replacer(oldURLAbsolute.replace(/\?.+/, ""), ...colors); + if (!newURL) { + throw new Error("Replacer failed to produce a replacement URL!"); + } + urlStringNode.value = newURL; + }); + decl.assign({prop: decl.prop, value: parsed.toString()}) +} + +/* * + * @type {import('postcss').PluginCreator} + */ +module.exports = (opts = {}) => { + return { + postcssPlugin: "postcss-url-to-variable", + + Once(root) { + /* + Go through each declaration and if it contains an URL, replace the url with the result + of running replacer(url) + */ + root.walkDecls(decl => processURL(decl, opts.replacer, opts.colors)); + }, + }; +}; + +module.exports.postcss = true; + diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.js new file mode 100644 index 00000000..1a659e6a --- /dev/null +++ b/scripts/postcss/svg-colorizer.js @@ -0,0 +1,42 @@ +/* +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. +*/ + +const fs = require("fs"); +const path = require("path"); +/** + * Builds a new svg with the colors replaced and returns its location. + * @param {string} svgLocation The location of the input svg file + * @param {string} primaryColor Primary color for the new svg + * @param {string} secondaryColor Secondary color for the new svg + */ +module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondaryColor) { + const svgCode = fs.readFileSync(svgLocation, { encoding: "utf-8"}); + let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor); + coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); + const fileName = svgLocation.match(/.+\/(.+\.svg)/)[1]; + const outputPath = path.resolve(__dirname, "../../.tmp"); + try { + fs.mkdirSync(outputPath); + } + catch (e) { + if (e.code !== "EEXIST") { + throw e; + } + } + const outputFile = `${outputPath}/${fileName}`; + fs.writeFileSync(outputFile, coloredSVGCode); + return outputFile; +} From 8c6400ab2c16c232223c21e471213863a8622376 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Mar 2022 14:11:09 +0530 Subject: [PATCH 37/53] utf-8 --> utf8 --- scripts/postcss/svg-colorizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.js index 1a659e6a..1895cfa0 100644 --- a/scripts/postcss/svg-colorizer.js +++ b/scripts/postcss/svg-colorizer.js @@ -23,7 +23,7 @@ const path = require("path"); * @param {string} secondaryColor Secondary color for the new svg */ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondaryColor) { - const svgCode = fs.readFileSync(svgLocation, { encoding: "utf-8"}); + const svgCode = fs.readFileSync(svgLocation, { encoding: "utf8"}); let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor); coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); const fileName = svgLocation.match(/.+\/(.+\.svg)/)[1]; From 7046fcc7c74aef65256db3549cf4145c266cd077 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 27 Mar 2022 20:00:43 +0530 Subject: [PATCH 38/53] Find list of resolved colors from result and also throw only if secondary color was provided --- scripts/postcss/css-url-processor.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/scripts/postcss/css-url-processor.js b/scripts/postcss/css-url-processor.js index 854b549f..d663ed0a 100644 --- a/scripts/postcss/css-url-processor.js +++ b/scripts/postcss/css-url-processor.js @@ -17,25 +17,25 @@ limitations under the License. const valueParser = require("postcss-value-parser"); const resolve = require("path").resolve; -function colorsFromURL(url, colorVariables) { +function colorsFromURL(url, colorMap) { const params = new URL(`file://${url}`).searchParams; const primary = params.get("primary"); if (!primary) { return null; } const secondary = params.get("secondary"); - const primaryColor = colorVariables[primary]; - const secondaryColor = colorVariables[secondary]; + const primaryColor = colorMap.get(primary); + const secondaryColor = colorMap.get(secondary); if (!primaryColor) { throw new Error(`Variable ${primary} not found in resolved color variables!`); } - if (!secondaryColor) { + if (secondary && !secondaryColor) { throw new Error(`Variable ${secondary} not found in resolved color variables!`); } return [primaryColor, secondaryColor]; } -function processURL(decl, replacer, colorVariables) { +function processURL(decl, replacer, colorMap) { const value = decl.value; const parsed = valueParser(value); parsed.walk(async node => { @@ -46,7 +46,7 @@ function processURL(decl, replacer, colorVariables) { const oldURL = urlStringNode.value; const cssPath = decl.source?.input.file.replace(/[^/]*$/, ""); const oldURLAbsolute = resolve(cssPath, oldURL); - const colors = colorsFromURL(oldURLAbsolute, colorVariables); + const colors = colorsFromURL(oldURLAbsolute, colorMap); if (!colors) { // If no primary color is provided via url params, then this url need not be handled. return; @@ -67,15 +67,22 @@ module.exports = (opts = {}) => { return { postcssPlugin: "postcss-url-to-variable", - Once(root) { + Once(root, {result}) { + /* + postcss-compile-variables should have sent the list of resolved colours down via results + */ + const {colorMap} = result.messages.find(m => m.type === "resolved-variable-map"); + if (!colorMap) { + throw new Error("Postcss results do not contain resolved colors!"); + } /* Go through each declaration and if it contains an URL, replace the url with the result of running replacer(url) */ - root.walkDecls(decl => processURL(decl, opts.replacer, opts.colors)); + root.walkDecls(decl => processURL(decl, opts.replacer, colorMap)); + console.log("result", colorMap); }, }; }; module.exports.postcss = true; - From f2b4f2e069096a10cc487ff55eb43c4323e4d8c4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 27 Mar 2022 20:35:10 +0530 Subject: [PATCH 39/53] Remove console.log --- scripts/postcss/css-url-processor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/postcss/css-url-processor.js b/scripts/postcss/css-url-processor.js index d663ed0a..1a0e4fdf 100644 --- a/scripts/postcss/css-url-processor.js +++ b/scripts/postcss/css-url-processor.js @@ -80,7 +80,6 @@ module.exports = (opts = {}) => { of running replacer(url) */ root.walkDecls(decl => processURL(decl, opts.replacer, colorMap)); - console.log("result", colorMap); }, }; }; From c0fb8a2c77bd0d446b796c0632210730b915ffb3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 6 Apr 2022 11:02:09 +0530 Subject: [PATCH 40/53] Throw error if no replacements were made --- scripts/postcss/svg-colorizer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.js index 1895cfa0..7d527ddb 100644 --- a/scripts/postcss/svg-colorizer.js +++ b/scripts/postcss/svg-colorizer.js @@ -26,6 +26,9 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar const svgCode = fs.readFileSync(svgLocation, { encoding: "utf8"}); let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor); coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); + if (svgCode === 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)."); + } const fileName = svgLocation.match(/.+\/(.+\.svg)/)[1]; const outputPath = path.resolve(__dirname, "../../.tmp"); try { From 9755062563fb4697812ee5a55b99181615ecf3bf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 7 Apr 2022 10:33:12 +0200 Subject: [PATCH 41/53] fix error thrown during request when response code is not used --- src/matrix/net/RequestScheduler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrix/net/RequestScheduler.ts b/src/matrix/net/RequestScheduler.ts index dc5c501b..c6e546a1 100644 --- a/src/matrix/net/RequestScheduler.ts +++ b/src/matrix/net/RequestScheduler.ts @@ -27,8 +27,8 @@ class Request implements IHomeServerRequest { public readonly args: any[]; private responseResolve: (result: any) => void; public responseReject: (error: Error) => void; - private responseCodeResolve: (result: any) => void; - private responseCodeReject: (result: any) => void; + private responseCodeResolve?: (result: any) => void; + private responseCodeReject?: (result: any) => void; private _requestResult?: IHomeServerRequest; private readonly _responsePromise: Promise; private _responseCodePromise: Promise; @@ -73,7 +73,7 @@ class Request implements IHomeServerRequest { const response = await this._requestResult?.response(); this.responseResolve(response); const responseCode = await this._requestResult?.responseCode(); - this.responseCodeResolve(responseCode); + this.responseCodeResolve?.(responseCode); } get requestResult() { From 6aa79cf6e22d6c465fc2c9a1644d33c38ebed948 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 7 Apr 2022 17:19:43 +0200 Subject: [PATCH 42/53] allow to inject custom tile view creator fn into timeline view --- src/lib.ts | 2 ++ src/platform/web/ui/session/room/RoomView.js | 3 ++- .../web/ui/session/room/TimelineView.ts | 23 ++++++++++++------- src/platform/web/ui/session/room/common.ts | 12 ++++------ 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index a0ada84f..2634b0c0 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -27,6 +27,8 @@ export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js"; export {TimelineView} from "./platform/web/ui/session/room/TimelineView"; +export {viewClassForEntry} from "./platform/web/ui/session/room/common"; +export type {TileViewConstructor, ViewClassForEntryFn} from "./platform/web/ui/session/room/TimelineView"; export {Navigation} from "./domain/navigation/Navigation.js"; export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index c172766a..2190f1f1 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -23,6 +23,7 @@ import {TimelineLoadingView} from "./TimelineLoadingView.js"; import {MessageComposer} from "./MessageComposer.js"; import {RoomArchivedView} from "./RoomArchivedView.js"; import {AvatarView} from "../../AvatarView.js"; +import {viewClassForEntry} from "./common"; export class RoomView extends TemplateView { constructor(options) { @@ -54,7 +55,7 @@ export class RoomView extends TemplateView { t.div({className: "RoomView_error"}, vm => vm.error), t.mapView(vm => vm.timelineViewModel, timelineViewModel => { return timelineViewModel ? - new TimelineView(timelineViewModel) : + new TimelineView(timelineViewModel, viewClassForEntry) : new TimelineLoadingView(vm); // vm is just needed for i18n }), t.view(bottomView), diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 936b8c7c..91ff59d5 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type {TileView} from "./common"; -import {viewClassForEntry} from "./common"; import {ListView} from "../../general/ListView"; +import type {IView} from "../../general/types"; import {TemplateView, Builder} from "../../general/TemplateView"; import {IObservableValue} from "../../general/BaseUpdateView"; import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js"; @@ -25,6 +24,12 @@ import {RedactedView} from "./timeline/RedactedView.js"; import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js"; import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList"; +export interface TileView extends IView { + readonly value: SimpleTile; +} +export type TileViewConstructor = new (tile: SimpleTile) => TileView; +export type ViewClassForEntryFn = (tile: SimpleTile) => TileViewConstructor; + //import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js"; export interface TimelineViewModel extends IObservableValue { showJumpDown: boolean; @@ -55,13 +60,17 @@ export class TimelineView extends TemplateView { private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; + constructor(vm: TimelineViewModel, private readonly viewClassForEntry: ViewClassForEntryFn) { + super(vm); + } + render(t: Builder, vm: TimelineViewModel) { // assume this view will be mounted in the parent DOM straight away requestAnimationFrame(() => { // do initial scroll positioning this.restoreScrollPosition(); }); - this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition()); + this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition(), this.viewClassForEntry); const root = t.div({className: "Timeline"}, [ t.div({ className: "Timeline_scroller bottom-aligned-scroll", @@ -174,16 +183,14 @@ class TilesListView extends ListView { private onChanged: () => void; - constructor(tiles: ObservableList, onChanged: () => void) { + constructor(tiles: ObservableList, onChanged: () => void, private readonly viewClassForEntry: ViewClassForEntryFn) { const options = { list: tiles, onItemClick: (tileView, evt) => tileView.onClick(evt), }; super(options, entry => { const View = viewClassForEntry(entry); - if (View) { - return new View(entry); - } + return new View(entry); }); this.onChanged = onChanged; } @@ -195,7 +202,7 @@ class TilesListView extends ListView { onUpdate(index: number, value: SimpleTile, param: any) { if (param === "shape") { - const ExpectedClass = viewClassForEntry(value); + const ExpectedClass = this.viewClassForEntry(value); const child = this.getChildInstanceByIndex(index); if (!ExpectedClass || !(child instanceof ExpectedClass)) { // shape was updated, so we need to recreate the tile view, diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts index 5048211a..87997cc4 100644 --- a/src/platform/web/ui/session/room/common.ts +++ b/src/platform/web/ui/session/room/common.ts @@ -24,14 +24,10 @@ import {AnnouncementView} from "./timeline/AnnouncementView.js"; import {RedactedView} from "./timeline/RedactedView.js"; import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js"; import {GapView} from "./timeline/GapView.js"; +import type {TileViewConstructor, ViewClassForEntryFn} from "./TimelineView"; -export type TileView = GapView | AnnouncementView | TextMessageView | - ImageView | VideoView | FileView | LocationView | MissingAttachmentView | RedactedView; - -// TODO: this is what works for a ctor but doesn't actually check we constrain the returned ctors to the types above -type TileViewConstructor = (this: TileView, SimpleTile) => void; -export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined { - switch (entry.shape) { +export function viewClassForEntry(vm: SimpleTile): TileViewConstructor { + switch (vm.shape) { case "gap": return GapView; case "announcement": @@ -51,5 +47,7 @@ export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | unde return MissingAttachmentView; case "redacted": return RedactedView; + default: + throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tilesCreator function in the view model`); } } From 220f35ae039bc9392391c5228dfb43fb4f803a9f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 11:52:21 +0200 Subject: [PATCH 43/53] fix typescript error --- src/platform/web/ui/session/room/TimelineView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 91ff59d5..0c893847 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -26,6 +26,7 @@ import {BaseObservableList as ObservableList} from "../../../../../observable/li export interface TileView extends IView { readonly value: SimpleTile; + onClick(event: UIEvent); } export type TileViewConstructor = new (tile: SimpleTile) => TileView; export type ViewClassForEntryFn = (tile: SimpleTile) => TileViewConstructor; @@ -184,11 +185,10 @@ class TilesListView extends ListView { private onChanged: () => void; constructor(tiles: ObservableList, onChanged: () => void, private readonly viewClassForEntry: ViewClassForEntryFn) { - const options = { + super({ list: tiles, onItemClick: (tileView, evt) => tileView.onClick(evt), - }; - super(options, entry => { + }, entry => { const View = viewClassForEntry(entry); return new View(entry); }); From 5445db2a42719d809f65d49f74ecb923dbfdad0d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 12:52:30 +0200 Subject: [PATCH 44/53] allow injecting the tilesCreator from the Root/Session/RoomViewModel this changes the API slightly to be more future-proof, as we'll expose it in the SDK now. The function now returns a SimpleTile constructor, rather than an instance. This allows us to test if an entry would render in the timeline without creating a tile, which is something we might want in the matrix layer later on. The function is now called tileClassForEntry, analogue to what we do in TimelineView. --- src/domain/session/room/RoomViewModel.js | 20 ++-- .../room/timeline/ReactionsViewModel.js | 2 +- .../session/room/timeline/TilesCollection.js | 31 ++++-- .../room/timeline/TimelineViewModel.js | 4 +- .../room/timeline/tiles/BaseMediaTile.js | 4 +- .../room/timeline/tiles/BaseMessageTile.js | 22 +++-- .../room/timeline/tiles/BaseTextTile.js | 4 +- .../room/timeline/tiles/EncryptedEventTile.js | 4 +- .../session/room/timeline/tiles/FileTile.js | 4 +- .../session/room/timeline/tiles/GapTile.js | 10 +- .../session/room/timeline/tiles/ImageTile.js | 4 +- .../room/timeline/tiles/RoomMemberTile.js | 14 +-- .../session/room/timeline/tiles/SimpleTile.js | 4 +- .../session/room/timeline/tiles/index.ts | 94 +++++++++++++++++++ .../session/room/timeline/tilesCreator.js | 81 ---------------- src/platform/web/main.js | 3 + src/platform/web/ui/session/room/common.ts | 2 +- 17 files changed, 174 insertions(+), 133 deletions(-) create mode 100644 src/domain/session/room/timeline/tiles/index.ts delete mode 100644 src/domain/session/room/timeline/tilesCreator.js diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 71060728..9c3f468e 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -18,17 +18,17 @@ limitations under the License. import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {ComposerViewModel} from "./ComposerViewModel.js" import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; -import {tilesCreator} from "./timeline/tilesCreator.js"; import {ViewModel} from "../../ViewModel"; import {imageToInfo} from "../common.js"; export class RoomViewModel extends ViewModel { constructor(options) { super(options); - const {room} = options; + const {room, tileClassForEntry} = options; this._room = room; this._timelineVM = null; - this._tilesCreator = null; + this._tileClassForEntry = tileClassForEntry; + this._tileOptions = undefined; this._onRoomChange = this._onRoomChange.bind(this); this._timelineError = null; this._sendError = null; @@ -46,12 +46,13 @@ export class RoomViewModel extends ViewModel { this._room.on("change", this._onRoomChange); try { const timeline = await this._room.openTimeline(); - this._tilesCreator = tilesCreator(this.childOptions({ + this._tileOptions = this.childOptions({ roomVM: this, timeline, - })); + tileClassForEntry: this._tileClassForEntry, + }); this._timelineVM = this.track(new TimelineViewModel(this.childOptions({ - tilesCreator: this._tilesCreator, + tileOptions: this._tileOptions, timeline, }))); this.emitChange("timelineViewModel"); @@ -161,7 +162,12 @@ export class RoomViewModel extends ViewModel { } _createTile(entry) { - return this._tilesCreator(entry); + if (this._tileOptions) { + const Tile = this._tileOptions.tileClassForEntry(entry); + if (Tile) { + return new Tile(entry, this._tileOptions); + } + } } async _sendMessage(message, replyingTo) { diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index 4f366af0..1977b6f4 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -222,7 +222,7 @@ export function tests() { }; const tiles = new MappedList(timeline.entries, entry => { if (entry.eventType === "m.room.message") { - return new BaseMessageTile({entry, roomVM: {room}, timeline, platform: {logger}}); + return new BaseMessageTile(entry, {roomVM: {room}, timeline, platform: {logger}}); } return null; }, (tile, params, entry) => tile?.updateEntry(entry, params, function () {})); diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 33ae4472..75af5b09 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -18,20 +18,27 @@ import {BaseObservableList} from "../../../../observable/list/BaseObservableList import {sortedIndex} from "../../../../utils/sortedIndex"; // maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary -// for now, tileCreator should be stable in whether it returns a tile or not. +// for now, tileClassForEntry should be stable in whether it returns a tile or not. // e.g. the decision to create a tile or not should be based on properties // not updated later on (e.g. event type) // also see big comment in onUpdate export class TilesCollection extends BaseObservableList { - constructor(entries, tileCreator) { + constructor(entries, tileOptions) { super(); this._entries = entries; this._tiles = null; this._entrySubscription = null; - this._tileCreator = tileCreator; + this._tileOptions = tileOptions; this._emitSpontanousUpdate = this._emitSpontanousUpdate.bind(this); } + _createTile(entry) { + const Tile = this._tileOptions.tileClassForEntry(entry); + if (Tile) { + return new Tile(entry, this._tileOptions); + } + } + _emitSpontanousUpdate(tile, params) { const entry = tile.lowerEntry; const tileIdx = this._findTileIdx(entry); @@ -48,7 +55,7 @@ export class TilesCollection extends BaseObservableList { let currentTile = null; for (let entry of this._entries) { if (!currentTile || !currentTile.tryIncludeEntry(entry)) { - currentTile = this._tileCreator(entry); + currentTile = this._createTile(entry); if (currentTile) { this._tiles.push(currentTile); } @@ -121,7 +128,7 @@ export class TilesCollection extends BaseObservableList { return; } - const newTile = this._tileCreator(entry); + const newTile = this._createTile(entry); if (newTile) { if (prevTile) { prevTile.updateNextSibling(newTile); @@ -150,9 +157,9 @@ export class TilesCollection extends BaseObservableList { const tileIdx = this._findTileIdx(entry); const tile = this._findTileAtIdx(entry, tileIdx); if (tile) { - const action = tile.updateEntry(entry, params, this._tileCreator); + const action = tile.updateEntry(entry, params); if (action.shouldReplace) { - const newTile = this._tileCreator(entry); + const newTile = this._createTile(entry); if (newTile) { this._replaceTile(tileIdx, tile, newTile, action.updateParams); newTile.setUpdateEmit(this._emitSpontanousUpdate); @@ -303,7 +310,10 @@ export function tests() { } } const entries = new ObservableArray([{n: 5}, {n: 10}]); - const tiles = new TilesCollection(entries, entry => new UpdateOnSiblingTile(entry)); + const tileOptions = { + tileClassForEntry: entry => UpdateOnSiblingTile, + }; + const tiles = new TilesCollection(entries, tileOptions); let receivedAdd = false; tiles.subscribe({ onAdd(idx, tile) { @@ -326,7 +336,10 @@ export function tests() { } } const entries = new ObservableArray([{n: 5}, {n: 10}, {n: 15}]); - const tiles = new TilesCollection(entries, entry => new UpdateOnSiblingTile(entry)); + const tileOptions = { + tileClassForEntry: entry => UpdateOnSiblingTile, + }; + const tiles = new TilesCollection(entries, tileOptions); const events = []; tiles.subscribe({ onUpdate(idx, tile) { diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 2408146d..cf36fce4 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -37,9 +37,9 @@ import {ViewModel} from "../../../ViewModel"; export class TimelineViewModel extends ViewModel { constructor(options) { super(options); - const {timeline, tilesCreator} = options; + const {timeline, tileOptions} = options; this._timeline = this.track(timeline); - this._tiles = new TilesCollection(timeline.entries, tilesCreator); + this._tiles = new TilesCollection(timeline.entries, tileOptions); this._startTile = null; this._endTile = null; this._topLoadingPromise = null; diff --git a/src/domain/session/room/timeline/tiles/BaseMediaTile.js b/src/domain/session/room/timeline/tiles/BaseMediaTile.js index a927d766..0ba5b9a9 100644 --- a/src/domain/session/room/timeline/tiles/BaseMediaTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMediaTile.js @@ -21,8 +21,8 @@ const MAX_HEIGHT = 300; const MAX_WIDTH = 400; export class BaseMediaTile extends BaseMessageTile { - constructor(options) { - super(options); + constructor(entry, options) { + super(entry, options); this._decryptedThumbnail = null; this._decryptedFile = null; this._isVisible = false; diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 3385a587..03cc16ba 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -19,8 +19,8 @@ import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; export class BaseMessageTile extends SimpleTile { - constructor(options) { - super(options); + constructor(entry, options) { + super(entry, options); this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null; this._isContinuation = false; this._reactions = null; @@ -28,7 +28,7 @@ export class BaseMessageTile extends SimpleTile { if (this._entry.annotations || this._entry.pendingAnnotations) { this._updateReactions(); } - this._updateReplyTileIfNeeded(options.tilesCreator, undefined); + this._updateReplyTileIfNeeded(undefined); } notifyVisible() { @@ -122,23 +122,27 @@ export class BaseMessageTile extends SimpleTile { } } - updateEntry(entry, param, tilesCreator) { - const action = super.updateEntry(entry, param, tilesCreator); + updateEntry(entry, param) { + const action = super.updateEntry(entry, param); if (action.shouldUpdate) { this._updateReactions(); } - this._updateReplyTileIfNeeded(tilesCreator, param); + this._updateReplyTileIfNeeded(param); return action; } - _updateReplyTileIfNeeded(tilesCreator, param) { + _updateReplyTileIfNeeded(param) { const replyEntry = this._entry.contextEntry; if (replyEntry) { // this is an update to contextEntry used for replyPreview - const action = this._replyTile?.updateEntry(replyEntry, param, tilesCreator); + const action = this._replyTile?.updateEntry(replyEntry, param); if (action?.shouldReplace || !this._replyTile) { this.disposeTracked(this._replyTile); - this._replyTile = tilesCreator(replyEntry); + const tileClassForEntry = this._options.tileClassForEntry; + const ReplyTile = tileClassForEntry(replyEntry); + if (ReplyTile) { + this._replyTile = new ReplyTile(replyEntry, this._options); + } } if(action?.shouldUpdate) { this._replyTile?.emitChange(); diff --git a/src/domain/session/room/timeline/tiles/BaseTextTile.js b/src/domain/session/room/timeline/tiles/BaseTextTile.js index 164443e3..8e78c95f 100644 --- a/src/domain/session/room/timeline/tiles/BaseTextTile.js +++ b/src/domain/session/room/timeline/tiles/BaseTextTile.js @@ -21,8 +21,8 @@ import {createEnum} from "../../../../../utils/enum"; export const BodyFormat = createEnum("Plain", "Html"); export class BaseTextTile extends BaseMessageTile { - constructor(options) { - super(options); + constructor(entry, options) { + super(entry, options); this._messageBody = null; this._format = null } diff --git a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js index 50f507eb..b96e2d85 100644 --- a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js +++ b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js @@ -18,8 +18,8 @@ import {BaseTextTile} from "./BaseTextTile.js"; import {UpdateAction} from "../UpdateAction.js"; export class EncryptedEventTile extends BaseTextTile { - updateEntry(entry, params, tilesCreator) { - const parentResult = super.updateEntry(entry, params, tilesCreator); + updateEntry(entry, params) { + const parentResult = super.updateEntry(entry, params); // event got decrypted, recreate the tile and replace this one with it if (entry.eventType !== "m.room.encrypted") { // the "shape" parameter trigger tile recreation in TimelineView diff --git a/src/domain/session/room/timeline/tiles/FileTile.js b/src/domain/session/room/timeline/tiles/FileTile.js index 1007d28c..3f7b539b 100644 --- a/src/domain/session/room/timeline/tiles/FileTile.js +++ b/src/domain/session/room/timeline/tiles/FileTile.js @@ -20,8 +20,8 @@ import {formatSize} from "../../../../../utils/formatSize"; import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js"; export class FileTile extends BaseMessageTile { - constructor(options) { - super(options); + constructor(entry, options) { + super(entry, options); this._downloadError = null; this._downloading = false; } diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index df0cedd9..6caa4b9b 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -18,8 +18,8 @@ import {SimpleTile} from "./SimpleTile.js"; import {UpdateAction} from "../UpdateAction.js"; export class GapTile extends SimpleTile { - constructor(options) { - super(options); + constructor(entry, options) { + super(entry, options); this._loading = false; this._error = null; this._isAtTop = true; @@ -81,8 +81,8 @@ export class GapTile extends SimpleTile { this._siblingChanged = true; } - updateEntry(entry, params, tilesCreator) { - super.updateEntry(entry, params, tilesCreator); + updateEntry(entry, params) { + super.updateEntry(entry, params); if (!entry.isGap) { return UpdateAction.Remove(); } else { @@ -125,7 +125,7 @@ export function tests() { tile.updateEntry(newEntry); } }; - const tile = new GapTile({entry: new FragmentBoundaryEntry(fragment, true), roomVM: {room}}); + const tile = new GapTile(new FragmentBoundaryEntry(fragment, true), {roomVM: {room}}); await tile.fill(); await tile.fill(); await tile.fill(); diff --git a/src/domain/session/room/timeline/tiles/ImageTile.js b/src/domain/session/room/timeline/tiles/ImageTile.js index eae2b926..dd959b28 100644 --- a/src/domain/session/room/timeline/tiles/ImageTile.js +++ b/src/domain/session/room/timeline/tiles/ImageTile.js @@ -18,8 +18,8 @@ limitations under the License. import {BaseMediaTile} from "./BaseMediaTile.js"; export class ImageTile extends BaseMediaTile { - constructor(options) { - super(options); + constructor(entry, options) { + super(entry, options); this._lightboxUrl = this.urlCreator.urlForSegments([ // ensure the right room is active if in grid view this.navigation.segment("room", this._room.id), diff --git a/src/domain/session/room/timeline/tiles/RoomMemberTile.js b/src/domain/session/room/timeline/tiles/RoomMemberTile.js index ce41f031..ca9cd9b7 100644 --- a/src/domain/session/room/timeline/tiles/RoomMemberTile.js +++ b/src/domain/session/room/timeline/tiles/RoomMemberTile.js @@ -66,23 +66,25 @@ export class RoomMemberTile extends SimpleTile { export function tests() { return { "user removes display name": (assert) => { - const tile = new RoomMemberTile({ - entry: { + const tile = new RoomMemberTile( + { prevContent: {displayname: "foo", membership: "join"}, content: {membership: "join"}, stateKey: "foo@bar.com", }, - }); + {} + ); assert.strictEqual(tile.announcement, "foo@bar.com removed their name (foo)"); }, "user without display name sets a new display name": (assert) => { - const tile = new RoomMemberTile({ - entry: { + const tile = new RoomMemberTile( + { prevContent: {membership: "join"}, content: {displayname: "foo", membership: "join" }, stateKey: "foo@bar.com", }, - }); + {} + ); assert.strictEqual(tile.announcement, "foo@bar.com changed their name to foo"); }, }; diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index af2b0e12..b8a7121e 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -19,9 +19,9 @@ import {ViewModel} from "../../../../ViewModel"; import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js"; export class SimpleTile extends ViewModel { - constructor(options) { + constructor(entry, options) { super(options); - this._entry = options.entry; + this._entry = entry; } // view model props for all subclasses // hmmm, could also do instanceof ... ? diff --git a/src/domain/session/room/timeline/tiles/index.ts b/src/domain/session/room/timeline/tiles/index.ts new file mode 100644 index 00000000..242bea2f --- /dev/null +++ b/src/domain/session/room/timeline/tiles/index.ts @@ -0,0 +1,94 @@ +/* +Copyright 2020 Bruno Windels + +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 {GapTile} from "./GapTile.js"; +import {TextTile} from "./TextTile.js"; +import {RedactedTile} from "./RedactedTile.js"; +import {ImageTile} from "./ImageTile.js"; +import {VideoTile} from "./VideoTile.js"; +import {FileTile} from "./FileTile.js"; +import {LocationTile} from "./LocationTile.js"; +import {RoomNameTile} from "./RoomNameTile.js"; +import {RoomMemberTile} from "./RoomMemberTile.js"; +import {EncryptedEventTile} from "./EncryptedEventTile.js"; +import {EncryptionEnabledTile} from "./EncryptionEnabledTile.js"; +import {MissingAttachmentTile} from "./MissingAttachmentTile.js"; + +import type {SimpleTile} from "./SimpleTile.js"; +import type {Room} from "../../../../../matrix/room/Room"; +import type {Timeline} from "../../../../../matrix/room/timeline/Timeline"; +import type {FragmentBoundaryEntry} from "../../../../../matrix/room/timeline/entries/FragmentBoundaryEntry"; +import type {EventEntry} from "../../../../../matrix/room/timeline/entries/EventEntry"; +import type {PendingEventEntry} from "../../../../../matrix/room/timeline/entries/PendingEventEntry"; +import type {Options as ViewModelOptions} from "../../../../ViewModel"; + +export type TimelineEntry = FragmentBoundaryEntry | EventEntry | PendingEventEntry; +export type TileClassForEntryFn = (entry: TimelineEntry) => TileConstructor | undefined; +export type Options = ViewModelOptions & { + room: Room, + timeline: Timeline + tileClassForEntry: TileClassForEntryFn; +}; +export type TileConstructor = new (entry: TimelineEntry, options: Options) => SimpleTile; + +export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undefined { + if (entry.isGap) { + return GapTile; + } else if (entry.isPending && entry.pendingEvent.isMissingAttachments) { + return MissingAttachmentTile; + } else if (entry.eventType) { + switch (entry.eventType) { + case "m.room.message": { + if (entry.isRedacted) { + return RedactedTile; + } + const content = entry.content; + const msgtype = content && content.msgtype; + switch (msgtype) { + case "m.text": + case "m.notice": + case "m.emote": + return TextTile; + case "m.image": + return ImageTile; + case "m.video": + return VideoTile; + case "m.file": + return FileTile; + case "m.location": + return LocationTile; + default: + // unknown msgtype not rendered + return undefined; + } + } + case "m.room.name": + return RoomNameTile; + case "m.room.member": + return RoomMemberTile; + case "m.room.encrypted": + if (entry.isRedacted) { + return RedactedTile; + } + return EncryptedEventTile; + case "m.room.encryption": + return EncryptionEnabledTile; + default: + // unknown type not rendered + return undefined; + } + } +} diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js deleted file mode 100644 index dc9a850e..00000000 --- a/src/domain/session/room/timeline/tilesCreator.js +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -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 {GapTile} from "./tiles/GapTile.js"; -import {TextTile} from "./tiles/TextTile.js"; -import {RedactedTile} from "./tiles/RedactedTile.js"; -import {ImageTile} from "./tiles/ImageTile.js"; -import {VideoTile} from "./tiles/VideoTile.js"; -import {FileTile} from "./tiles/FileTile.js"; -import {LocationTile} from "./tiles/LocationTile.js"; -import {RoomNameTile} from "./tiles/RoomNameTile.js"; -import {RoomMemberTile} from "./tiles/RoomMemberTile.js"; -import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js"; -import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js"; -import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js"; - -export function tilesCreator(baseOptions) { - const tilesCreator = function tilesCreator(entry, emitUpdate) { - const options = Object.assign({entry, emitUpdate, tilesCreator}, baseOptions); - if (entry.isGap) { - return new GapTile(options); - } else if (entry.isPending && entry.pendingEvent.isMissingAttachments) { - return new MissingAttachmentTile(options); - } else if (entry.eventType) { - switch (entry.eventType) { - case "m.room.message": { - if (entry.isRedacted) { - return new RedactedTile(options); - } - const content = entry.content; - const msgtype = content && content.msgtype; - switch (msgtype) { - case "m.text": - case "m.notice": - case "m.emote": - return new TextTile(options); - case "m.image": - return new ImageTile(options); - case "m.video": - return new VideoTile(options); - case "m.file": - return new FileTile(options); - case "m.location": - return new LocationTile(options); - default: - // unknown msgtype not rendered - return null; - } - } - case "m.room.name": - return new RoomNameTile(options); - case "m.room.member": - return new RoomMemberTile(options); - case "m.room.encrypted": - if (entry.isRedacted) { - return new RedactedTile(options); - } - return new EncryptedEventTile(options); - case "m.room.encryption": - return new EncryptionEnabledTile(options); - default: - // unknown type not rendered - return null; - } - } - }; - return tilesCreator; -} diff --git a/src/platform/web/main.js b/src/platform/web/main.js index 1729c17c..9e1ca85e 100644 --- a/src/platform/web/main.js +++ b/src/platform/web/main.js @@ -18,6 +18,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 {tileClassForEntry} from "../../domain/session/room/timeline/tiles/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 @@ -42,6 +43,8 @@ export async function main(platform) { // so we call it that in the view models urlCreator: urlRouter, navigation, + // which tiles are supported by the timeline + tileClassForEntry }); await vm.load(); platform.createAndMountRootView(vm); diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts index 87997cc4..201f14d0 100644 --- a/src/platform/web/ui/session/room/common.ts +++ b/src/platform/web/ui/session/room/common.ts @@ -48,6 +48,6 @@ export function viewClassForEntry(vm: SimpleTile): TileViewConstructor { case "redacted": return RedactedView; default: - throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tilesCreator function in the view model`); + throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model`); } } From a913671f0c93a1760dee5d12669b3cc13737a9d4 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 14:19:34 +0200 Subject: [PATCH 45/53] make tileClassForEntry optional, as otherwise it is a breaking change --- src/domain/session/room/RoomViewModel.js | 5 ++++- src/platform/web/main.js | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 9c3f468e..66042ae5 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -20,6 +20,9 @@ import {ComposerViewModel} from "./ComposerViewModel.js" import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; import {ViewModel} from "../../ViewModel"; import {imageToInfo} from "../common.js"; +// TODO: remove fallback so default isn't included in bundle for SDK users that have their custom tileClassForEntry +// this is a breaking SDK change though to make this option mandatory +import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index"; export class RoomViewModel extends ViewModel { constructor(options) { @@ -27,7 +30,7 @@ export class RoomViewModel extends ViewModel { const {room, tileClassForEntry} = options; this._room = room; this._timelineVM = null; - this._tileClassForEntry = tileClassForEntry; + this._tileClassForEntry = tileClassForEntry ?? defaultTileClassForEntry; this._tileOptions = undefined; this._onRoomChange = this._onRoomChange.bind(this); this._timelineError = null; diff --git a/src/platform/web/main.js b/src/platform/web/main.js index 9e1ca85e..1729c17c 100644 --- a/src/platform/web/main.js +++ b/src/platform/web/main.js @@ -18,7 +18,6 @@ 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 {tileClassForEntry} from "../../domain/session/room/timeline/tiles/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 @@ -43,8 +42,6 @@ export async function main(platform) { // so we call it that in the view models urlCreator: urlRouter, navigation, - // which tiles are supported by the timeline - tileClassForEntry }); await vm.load(); platform.createAndMountRootView(vm); From ac4bb8ca159fb8d353b78307df28c98fa24edcb3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 14:27:08 +0200 Subject: [PATCH 46/53] export tile view & view models from SDK --- src/lib.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/lib.ts b/src/lib.ts index 2634b0c0..7f4e5316 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -26,9 +26,41 @@ export {SessionView} from "./platform/web/ui/session/SessionView.js"; export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js"; +export {tileClassForEntry} from "./domain/session/room/timeline/tiles/index"; +export type {TimelineEntry, TileClassForEntryFn, Options, TileConstructor} from "./domain/session/room/timeline/tiles/index"; +// export timeline tile view models +export {GapTile} from "./domain/session/room/timeline/tiles/GapTile.js"; +export {TextTile} from "./domain/session/room/timeline/tiles/TextTile.js"; +export {RedactedTile} from "./domain/session/room/timeline/tiles/RedactedTile.js"; +export {ImageTile} from "./domain/session/room/timeline/tiles/ImageTile.js"; +export {VideoTile} from "./domain/session/room/timeline/tiles/VideoTile.js"; +export {FileTile} from "./domain/session/room/timeline/tiles/FileTile.js"; +export {LocationTile} from "./domain/session/room/timeline/tiles/LocationTile.js"; +export {RoomNameTile} from "./domain/session/room/timeline/tiles/RoomNameTile.js"; +export {RoomMemberTile} from "./domain/session/room/timeline/tiles/RoomMemberTile.js"; +export {EncryptedEventTile} from "./domain/session/room/timeline/tiles/EncryptedEventTile.js"; +export {EncryptionEnabledTile} from "./domain/session/room/timeline/tiles/EncryptionEnabledTile.js"; +export {MissingAttachmentTile} from "./domain/session/room/timeline/tiles/MissingAttachmentTile.js"; +export {SimpleTile} from "./domain/session/room/timeline/tiles/SimpleTile.js"; + export {TimelineView} from "./platform/web/ui/session/room/TimelineView"; export {viewClassForEntry} from "./platform/web/ui/session/room/common"; export type {TileViewConstructor, ViewClassForEntryFn} from "./platform/web/ui/session/room/TimelineView"; +// export timeline tile views +export {AnnouncementView} from "./platform/web/ui/session/room/timeline/AnnouncementView.js"; +export {BaseMediaView} from "./platform/web/ui/session/room/timeline/BaseMediaView.js"; +export {BaseMessageView} from "./platform/web/ui/session/room/timeline/BaseMessageView.js"; +export {FileView} from "./platform/web/ui/session/room/timeline/FileView.js"; +export {GapView} from "./platform/web/ui/session/room/timeline/GapView.js"; +export {ImageView} from "./platform/web/ui/session/room/timeline/ImageView.js"; +export {LocationView} from "./platform/web/ui/session/room/timeline/LocationView.js"; +export {MissingAttachmentView} from "./platform/web/ui/session/room/timeline/MissingAttachmentView.js"; +export {ReactionsView} from "./platform/web/ui/session/room/timeline/ReactionsView.js"; +export {RedactedView} from "./platform/web/ui/session/room/timeline/RedactedView.js"; +export {ReplyPreviewView} from "./platform/web/ui/session/room/timeline/ReplyPreviewView.js"; +export {TextMessageView} from "./platform/web/ui/session/room/timeline/TextMessageView.js"; +export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js"; + export {Navigation} from "./domain/navigation/Navigation.js"; export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; From cda96a35eebe2158d4d9627281fd00ebedc66715 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 15:01:06 +0200 Subject: [PATCH 47/53] rename viewClassForEntry to viewClassForTile --- src/lib.ts | 2 +- src/platform/web/ui/session/room/MessageComposer.js | 4 ++-- src/platform/web/ui/session/room/RoomView.js | 4 ++-- src/platform/web/ui/session/room/TimelineView.ts | 10 +++++----- src/platform/web/ui/session/room/common.ts | 2 +- .../web/ui/session/room/timeline/ReplyPreviewView.js | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index 7f4e5316..90bf597c 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -44,7 +44,7 @@ export {MissingAttachmentTile} from "./domain/session/room/timeline/tiles/Missin export {SimpleTile} from "./domain/session/room/timeline/tiles/SimpleTile.js"; export {TimelineView} from "./platform/web/ui/session/room/TimelineView"; -export {viewClassForEntry} from "./platform/web/ui/session/room/common"; +export {viewClassForTile} from "./platform/web/ui/session/room/common"; export type {TileViewConstructor, ViewClassForEntryFn} from "./platform/web/ui/session/room/TimelineView"; // export timeline tile views export {AnnouncementView} from "./platform/web/ui/session/room/timeline/AnnouncementView.js"; diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index 9c67fa9f..7f822d37 100644 --- a/src/platform/web/ui/session/room/MessageComposer.js +++ b/src/platform/web/ui/session/room/MessageComposer.js @@ -17,7 +17,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; -import {viewClassForEntry} from "./common" +import {viewClassForTile} from "./common" export class MessageComposer extends TemplateView { constructor(viewModel) { @@ -45,7 +45,7 @@ export class MessageComposer extends TemplateView { this._focusInput = () => this._input.focus(); this.value.on("focus", this._focusInput); const replyPreview = t.map(vm => vm.replyViewModel, (rvm, t) => { - const View = rvm && viewClassForEntry(rvm); + const View = rvm && viewClassForTile(rvm); if (!View) { return null; } return t.div({ className: "MessageComposer_replyPreview" diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 2190f1f1..961be704 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -23,7 +23,7 @@ import {TimelineLoadingView} from "./TimelineLoadingView.js"; import {MessageComposer} from "./MessageComposer.js"; import {RoomArchivedView} from "./RoomArchivedView.js"; import {AvatarView} from "../../AvatarView.js"; -import {viewClassForEntry} from "./common"; +import {viewClassForTile} from "./common"; export class RoomView extends TemplateView { constructor(options) { @@ -55,7 +55,7 @@ export class RoomView extends TemplateView { t.div({className: "RoomView_error"}, vm => vm.error), t.mapView(vm => vm.timelineViewModel, timelineViewModel => { return timelineViewModel ? - new TimelineView(timelineViewModel, viewClassForEntry) : + new TimelineView(timelineViewModel, viewClassForTile) : new TimelineLoadingView(vm); // vm is just needed for i18n }), t.view(bottomView), diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 0c893847..6dd52466 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -61,7 +61,7 @@ export class TimelineView extends TemplateView { private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; - constructor(vm: TimelineViewModel, private readonly viewClassForEntry: ViewClassForEntryFn) { + constructor(vm: TimelineViewModel, private readonly viewClassForTile: ViewClassForEntryFn) { super(vm); } @@ -71,7 +71,7 @@ export class TimelineView extends TemplateView { // do initial scroll positioning this.restoreScrollPosition(); }); - this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition(), this.viewClassForEntry); + this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition(), this.viewClassForTile); const root = t.div({className: "Timeline"}, [ t.div({ className: "Timeline_scroller bottom-aligned-scroll", @@ -184,12 +184,12 @@ class TilesListView extends ListView { private onChanged: () => void; - constructor(tiles: ObservableList, onChanged: () => void, private readonly viewClassForEntry: ViewClassForEntryFn) { + constructor(tiles: ObservableList, onChanged: () => void, private readonly viewClassForTile: ViewClassForEntryFn) { super({ list: tiles, onItemClick: (tileView, evt) => tileView.onClick(evt), }, entry => { - const View = viewClassForEntry(entry); + const View = viewClassForTile(entry); return new View(entry); }); this.onChanged = onChanged; @@ -202,7 +202,7 @@ class TilesListView extends ListView { onUpdate(index: number, value: SimpleTile, param: any) { if (param === "shape") { - const ExpectedClass = this.viewClassForEntry(value); + const ExpectedClass = this.viewClassForTile(value); const child = this.getChildInstanceByIndex(index); if (!ExpectedClass || !(child instanceof ExpectedClass)) { // shape was updated, so we need to recreate the tile view, diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts index 201f14d0..7b62630f 100644 --- a/src/platform/web/ui/session/room/common.ts +++ b/src/platform/web/ui/session/room/common.ts @@ -26,7 +26,7 @@ import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/Simp import {GapView} from "./timeline/GapView.js"; import type {TileViewConstructor, ViewClassForEntryFn} from "./TimelineView"; -export function viewClassForEntry(vm: SimpleTile): TileViewConstructor { +export function viewClassForTile(vm: SimpleTile): TileViewConstructor { switch (vm.shape) { case "gap": return GapView; diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index 3c52fc71..bddcc8fe 100644 --- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -16,11 +16,11 @@ limitations under the License. import {renderStaticAvatar} from "../../../avatar"; import {TemplateView} from "../../../general/TemplateView"; -import {viewClassForEntry} from "../common"; +import {viewClassForTile} from "../common"; export class ReplyPreviewView extends TemplateView { render(t, vm) { - const viewClass = viewClassForEntry(vm); + const viewClass = viewClassForTile(vm); if (!viewClass) { throw new Error(`Shape ${vm.shape} is unrecognized.`) } From 57f50cc4160a7e35b4cb2df1c9299166fff6cf53 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 15:01:27 +0200 Subject: [PATCH 48/53] fix lint warnings --- src/domain/session/room/timeline/TilesCollection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 75af5b09..173b0cf6 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -311,7 +311,7 @@ export function tests() { } const entries = new ObservableArray([{n: 5}, {n: 10}]); const tileOptions = { - tileClassForEntry: entry => UpdateOnSiblingTile, + tileClassForEntry: () => UpdateOnSiblingTile, }; const tiles = new TilesCollection(entries, tileOptions); let receivedAdd = false; @@ -337,7 +337,7 @@ export function tests() { } const entries = new ObservableArray([{n: 5}, {n: 10}, {n: 15}]); const tileOptions = { - tileClassForEntry: entry => UpdateOnSiblingTile, + tileClassForEntry: () => UpdateOnSiblingTile, }; const tiles = new TilesCollection(entries, tileOptions); const events = []; From 1f0cb542c88e53831424155f282b01315aef88e4 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 15:02:07 +0200 Subject: [PATCH 49/53] pass viewClassForTile to tile views, so they can create reply view with correct subtile --- src/platform/web/ui/session/room/TimelineView.ts | 12 ++++++++---- .../web/ui/session/room/timeline/BaseMessageView.js | 3 ++- .../web/ui/session/room/timeline/ReplyPreviewView.js | 11 +++++++---- .../web/ui/session/room/timeline/TextMessageView.js | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 6dd52466..5a04991f 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -28,7 +28,11 @@ export interface TileView extends IView { readonly value: SimpleTile; onClick(event: UIEvent); } -export type TileViewConstructor = new (tile: SimpleTile) => TileView; +export type TileViewConstructor = new ( + tile: SimpleTile, + viewClassForTile: ViewClassForEntryFn, + renderFlags?: { reply?: boolean, interactive?: boolean } +) => TileView; export type ViewClassForEntryFn = (tile: SimpleTile) => TileViewConstructor; //import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js"; @@ -188,9 +192,9 @@ class TilesListView extends ListView { super({ list: tiles, onItemClick: (tileView, evt) => tileView.onClick(evt), - }, entry => { - const View = viewClassForTile(entry); - return new View(entry); + }, tile => { + const TileView = viewClassForTile(tile); + return new TileView(tile, viewClassForTile); }); this.onChanged = onChanged; } diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 7356cd2b..74b96ecf 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -24,10 +24,11 @@ import {Menu} from "../../../general/Menu.js"; import {ReactionsView} from "./ReactionsView.js"; export class BaseMessageView extends TemplateView { - constructor(value, renderFlags, tagName = "li") { + constructor(value, viewClassForTile, renderFlags, tagName = "li") { super(value); this._menuPopup = null; this._tagName = tagName; + this._viewClassForTile = viewClassForTile; // TODO An enum could be nice to make code easier to read at call sites. this._renderFlags = renderFlags; } diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index bddcc8fe..219e4357 100644 --- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -16,15 +16,18 @@ limitations under the License. import {renderStaticAvatar} from "../../../avatar"; import {TemplateView} from "../../../general/TemplateView"; -import {viewClassForTile} from "../common"; export class ReplyPreviewView extends TemplateView { + constructor(vm, viewClassForTile) { + super(vm); + this._viewClassForTile = viewClassForTile; + } render(t, vm) { - const viewClass = viewClassForTile(vm); - if (!viewClass) { + const TileView = this._viewClassForTile(vm); + if (!TileView) { throw new Error(`Shape ${vm.shape} is unrecognized.`) } - const view = new viewClass(vm, { reply: true, interactive: false }); + const view = new TileView(vm, this._viewClassForTile, { reply: true, interactive: false }); return t.div( { className: "ReplyPreviewView" }, t.blockquote([ diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index c0c0cfb0..8d6cb4dc 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -35,7 +35,7 @@ export class TextMessageView extends BaseMessageView { return new ReplyPreviewError(); } else if (replyTile) { - return new ReplyPreviewView(replyTile); + return new ReplyPreviewView(replyTile, this._viewClassForTile); } else { return null; From 1fea14dd10a1b830cc9be35d0077c478b2cb5a5e Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 15:04:38 +0200 Subject: [PATCH 50/53] ensure other parameters don't get passed to TemplateView parent ctors --- .../web/ui/session/room/timeline/AnnouncementView.js | 5 +++++ src/platform/web/ui/session/room/timeline/GapView.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/platform/web/ui/session/room/timeline/AnnouncementView.js b/src/platform/web/ui/session/room/timeline/AnnouncementView.js index 268bf0fa..5ae92daa 100644 --- a/src/platform/web/ui/session/room/timeline/AnnouncementView.js +++ b/src/platform/web/ui/session/room/timeline/AnnouncementView.js @@ -17,6 +17,11 @@ limitations under the License. import {TemplateView} from "../../../general/TemplateView"; export class AnnouncementView extends TemplateView { + // ignore other arguments + constructor(vm) { + super(vm); + } + render(t) { return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement)); } diff --git a/src/platform/web/ui/session/room/timeline/GapView.js b/src/platform/web/ui/session/room/timeline/GapView.js index 2d3bd6e8..db6cda59 100644 --- a/src/platform/web/ui/session/room/timeline/GapView.js +++ b/src/platform/web/ui/session/room/timeline/GapView.js @@ -18,6 +18,11 @@ import {TemplateView} from "../../../general/TemplateView"; import {spinner} from "../../../common.js"; export class GapView extends TemplateView { + // ignore other argument + constructor(vm) { + super(vm); + } + render(t) { const className = { GapView: true, From d21d10e4f25afa82c7ff41658d6fc7f79e8b42fa Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 15:15:21 +0200 Subject: [PATCH 51/53] pass in viewClassForTile from SessionView so you can also use custom tiles when using the grid view --- src/platform/web/ui/session/RoomGridView.js | 7 ++++++- src/platform/web/ui/session/SessionView.js | 5 +++-- src/platform/web/ui/session/room/RoomView.js | 8 ++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/platform/web/ui/session/RoomGridView.js b/src/platform/web/ui/session/RoomGridView.js index 79fc3d21..65289bea 100644 --- a/src/platform/web/ui/session/RoomGridView.js +++ b/src/platform/web/ui/session/RoomGridView.js @@ -21,6 +21,11 @@ import {TemplateView} from "../general/TemplateView"; import {StaticView} from "../general/StaticView.js"; export class RoomGridView extends TemplateView { + constructor(vm, viewClassForTile) { + super(vm); + this._viewClassForTile = viewClassForTile; + } + render(t, vm) { const children = []; for (let i = 0; i < (vm.height * vm.width); i+=1) { @@ -39,7 +44,7 @@ export class RoomGridView extends TemplateView { } else if (roomVM.kind === "invite") { return new InviteView(roomVM); } else { - return new RoomView(roomVM); + return new RoomView(roomVM, this._viewClassForTile); } } else { return new StaticView(t => t.div({className: "room-placeholder"}, [ diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index e7cc406a..ef63b29b 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -28,6 +28,7 @@ import {RoomGridView} from "./RoomGridView.js"; import {SettingsView} from "./settings/SettingsView.js"; import {CreateRoomView} from "./CreateRoomView.js"; import {RightPanelView} from "./rightpanel/RightPanelView.js"; +import {viewClassForTile} from "./room/common"; export class SessionView extends TemplateView { render(t, vm) { @@ -42,7 +43,7 @@ export class SessionView extends TemplateView { t.view(new LeftPanelView(vm.leftPanelViewModel)), t.mapView(vm => vm.activeMiddleViewModel, () => { if (vm.roomGridViewModel) { - return new RoomGridView(vm.roomGridViewModel); + return new RoomGridView(vm.roomGridViewModel, viewClassForTile); } else if (vm.settingsViewModel) { return new SettingsView(vm.settingsViewModel); } else if (vm.createRoomViewModel) { @@ -51,7 +52,7 @@ export class SessionView extends TemplateView { if (vm.currentRoomViewModel.kind === "invite") { return new InviteView(vm.currentRoomViewModel); } else if (vm.currentRoomViewModel.kind === "room") { - return new RoomView(vm.currentRoomViewModel); + return new RoomView(vm.currentRoomViewModel, viewClassForTile); } else if (vm.currentRoomViewModel.kind === "roomBeingCreated") { return new RoomBeingCreatedView(vm.currentRoomViewModel); } else { diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 961be704..0bc85e83 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -23,11 +23,11 @@ import {TimelineLoadingView} from "./TimelineLoadingView.js"; import {MessageComposer} from "./MessageComposer.js"; import {RoomArchivedView} from "./RoomArchivedView.js"; import {AvatarView} from "../../AvatarView.js"; -import {viewClassForTile} from "./common"; export class RoomView extends TemplateView { - constructor(options) { - super(options); + constructor(vm, viewClassForTile) { + super(vm); + this._viewClassForTile = viewClassForTile; this._optionsPopup = null; } @@ -55,7 +55,7 @@ export class RoomView extends TemplateView { t.div({className: "RoomView_error"}, vm => vm.error), t.mapView(vm => vm.timelineViewModel, timelineViewModel => { return timelineViewModel ? - new TimelineView(timelineViewModel, viewClassForTile) : + new TimelineView(timelineViewModel, this._viewClassForTile) : new TimelineLoadingView(vm); // vm is just needed for i18n }), t.view(bottomView), From cf780ce259bdcf7a6c74b759bc513a0f3f45e8c3 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 15:16:22 +0200 Subject: [PATCH 52/53] also apply custom tiles in reply preview in composer --- src/platform/web/ui/session/room/MessageComposer.js | 12 ++++++------ src/platform/web/ui/session/room/RoomView.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index 7f822d37..6ce8148f 100644 --- a/src/platform/web/ui/session/room/MessageComposer.js +++ b/src/platform/web/ui/session/room/MessageComposer.js @@ -17,11 +17,11 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; -import {viewClassForTile} from "./common" export class MessageComposer extends TemplateView { - constructor(viewModel) { + constructor(viewModel, viewClassForTile) { super(viewModel); + this._viewClassForTile = viewClassForTile; this._input = null; this._attachmentPopup = null; this._focusInput = null; @@ -45,8 +45,8 @@ export class MessageComposer extends TemplateView { this._focusInput = () => this._input.focus(); this.value.on("focus", this._focusInput); const replyPreview = t.map(vm => vm.replyViewModel, (rvm, t) => { - const View = rvm && viewClassForTile(rvm); - if (!View) { return null; } + const TileView = rvm && this._viewClassForTile(rvm); + if (!TileView) { return null; } return t.div({ className: "MessageComposer_replyPreview" }, [ @@ -55,8 +55,8 @@ export class MessageComposer extends TemplateView { className: "cancel", onClick: () => this._clearReplyingTo() }, "Close"), - t.view(new View(rvm, { interactive: false }, "div")) - ]) + t.view(new TileView(rvm, this._viewClassForTile, { interactive: false }, "div")) + ]); }); const input = t.div({className: "MessageComposer_input"}, [ this._input, diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 0bc85e83..76e26eab 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -34,7 +34,7 @@ export class RoomView extends TemplateView { render(t, vm) { let bottomView; if (vm.composerViewModel.kind === "composer") { - bottomView = new MessageComposer(vm.composerViewModel); + bottomView = new MessageComposer(vm.composerViewModel, this._viewClassForTile); } else if (vm.composerViewModel.kind === "archived") { bottomView = new RoomArchivedView(vm.composerViewModel); } From a6b6fef6d2cd73a8141556308ad496a1ee7006bf Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 8 Apr 2022 17:48:20 +0200 Subject: [PATCH 53/53] sdk release 0.0.10 --- 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 ba0e1f4f..7730bbac 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.9", + "version": "0.0.10", "main": "./hydrogen.es.js", "type": "module" }