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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..25d8e3c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,150 @@ +Contributing code to hydrogen-web +================================== + +Everyone is welcome to contribute code to hydrogen-web, provided that they are +willing to license their contributions under the same license as the project +itself. We follow a simple 'inbound=outbound' model for contributions: the act +of submitting an 'inbound' contribution means that the contributor agrees to +license the code under the same terms as the project's overall 'outbound' +license - in this case, Apache Software License v2 (see +[LICENSE](LICENSE)). + +How to contribute +----------------- + +The preferred and easiest way to contribute changes to the project is to fork +it on github, and then create a pull request to ask us to pull your changes +into our repo (https://help.github.com/articles/using-pull-requests/) + +We use GitHub's pull request workflow to review the contribution, and either +ask you to make any refinements needed or merge it and make them ourselves. + +Things that should go into your PR description: + * References to any bugs fixed by the change (in GitHub's `Fixes` notation) + * Describe the why and what is changing in the PR description so it's easy for + onlookers and reviewers to onboard and context switch. + * If your PR makes visual changes, include both **before** and **after** screenshots + to easily compare and discuss what's changing. + * Include a step-by-step testing strategy so that a reviewer can check out the + code locally and easily get to the point of testing your change. + * Add comments to the diff for the reviewer that might help them to understand + why the change is necessary or how they might better understand and review it. + +We use continuous integration, and all pull requests get automatically tested: +if your change breaks the build, then the PR will show that there are failed +checks, so please check back after a few minutes. + +Tests +----- +If your PR is a feature then we require that the PR also includes tests. +These need to test that your feature works as expected and ideally test edge cases too. + +Tests are written as unit tests by exporting a `tests` function from the file to be tested. +The function returns an object where the key is the test label, and the value is a +function that accepts an [assert](https://nodejs.org/api/assert.html) object, and return a Promise or nothing. + +Note that there is currently a limitation that files that are not indirectly included from `src/platform/web/main.js` won't be found by the runner. + +You can run the tests by running `yarn test`. +This uses the [impunity](https://github.com/bwindels/impunity) runner. + +We don't require tests for bug fixes. + +In the future we may formalise this more. + +Code style +---------- +The js-sdk aims to target TypeScript/ES6. All new files should be written in +TypeScript and existing files should use ES6 principles where possible. + +Please disable any automatic formatting tools you may have active. +If present, you'll be asked to undo any unrelated whitespace changes during code review. + +Members should not be exported as a default export in general. +In general, avoid using `export default`. + +The remaining code-style for hydrogen is [in the process of being documented](codestyle.md), but +contributors are encouraged to read the +[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md) +and follow the principles set out there. + +Please ensure your changes match the cosmetic style of the existing project, +and ***never*** mix cosmetic and functional changes in the same commit, as it +makes it horribly hard to review otherwise. + +Attribution +----------- +If you change or create a file, feel free to add yourself to the copyright holders +in the license header of that file. + +Sign off +-------- +In order to have a concrete record that your contribution is intentional +and you agree to license it under the same terms as the project's license, we've +adopted the same lightweight approach that the Linux Kernel +(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker +(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other +projects use: the DCO (Developer Certificate of Origin: +http://developercertificate.org/). This is a simple declaration that you wrote +the contribution or otherwise have the right to contribute it to Matrix: + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +If you agree to this for your contribution, then all that's needed is to +include the line in your commit or pull request comment: + +``` +Signed-off-by: Your Name +``` + +We accept contributions under a legally identifiable name, such as your name on +government documentation or common-law names (names claimed by legitimate usage +or repute). Unfortunately, we cannot accept anonymous contributions at this +time. + +Git allows you to add this signoff automatically when using the `-s` flag to +`git commit`, which uses the name and email set in your `user.name` and +`user.email` git configs. + +If you forgot to sign off your commits before making your pull request and are +on Git 2.17+ you can mass signoff using rebase: + +``` +git rebase --signoff origin/develop +``` diff --git a/README.md b/README.md index 6ee25ae6..6c447024 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,34 @@ Hydrogen's goals are: - It is a standalone webapp, but can also be easily embedded into an existing website/webapp to add chat capabilities. - Loading (unused) parts of the application after initial page load should be supported +For embedded usage, see the [SDK instructions](doc/SDK.md). + If you find this interesting, come and discuss on [`#hydrogen:matrix.org`](https://matrix.to/#/#hydrogen:matrix.org). # How to use -Hydrogen is deployed to [hydrogen.element.io](https://hydrogen.element.io). You can run it locally `yarn install` (only the first time) and `yarn start` in the terminal, and point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md). +Hydrogen is deployed to [hydrogen.element.io](https://hydrogen.element.io). You can also deploy Hydrogen on your own web server: -Hydrogen uses symbolic links in the codebase, so if you are on Windows, have a look at [making git & symlinks work](https://github.com/git-for-windows/git/wiki/Symbolic-Links) there. + 1. Download the [latest release package](https://github.com/vector-im/hydrogen-web/releases). + 1. Extract the package to the public directory of your web server. + 1. If this is your first deploy: + 1. copy `config.sample.json` to `config.json` and if needed, make any modifications (unless you've set up your own [sygnal](https://github.com/matrix-org/sygnal) instance, you don't need to change anything in the `push` section). + 1. Disable caching entirely on the server for: + - `index.html` + - `sw.js` + - `config.json` + - All theme manifests referenced in the `themeManifests` of `config.json`, these files are typically called `theme-{name}.json`. + + These resources will still be cached client-side by the service worker. Because of this; you'll still need to refresh the app twice before config.json changes are applied. + +## Set up a dev environment + +You can run Hydrogen locally by the following commands in the terminal: + + - `yarn install` (only the first time) + - `yarn start` in the terminal + +Now point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md). # FAQ diff --git a/doc/SDK.md b/doc/SDK.md index 8ce0b304..3f5bdb09 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -31,7 +31,8 @@ import { createNavigation, createRouter, RoomViewModel, - TimelineView + TimelineView, + viewClassForTile } from "hydrogen-view-sdk"; import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url'; import workerPath from 'hydrogen-view-sdk/main.js?url'; @@ -47,12 +48,13 @@ const assetPaths = { wasmBundle: olmJsPath } }; -import "hydrogen-view-sdk/style.css"; +import "hydrogen-view-sdk/theme-element-light.css"; +// OR import "hydrogen-view-sdk/theme-element-dark.css"; async function main() { const app = document.querySelector('#app')! const config = {}; - const platform = new Platform(app, assetPaths, config, { development: import.meta.env.DEV }); + const platform = new Platform({container: app, assetPaths, config, options: { development: import.meta.env.DEV }}); const navigation = createNavigation(); platform.setNavigation(navigation); const urlRouter = createRouter({ @@ -87,7 +89,7 @@ async function main() { navigation, }); await vm.load(); - const view = new TimelineView(vm.timelineViewModel); + const view = new TimelineView(vm.timelineViewModel, viewClassForTile); app.appendChild(view.mount()); } } diff --git a/package.json b/package.json index 8fa27f47..953cf678 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.26", + "version": "0.2.29", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" @@ -10,9 +10,12 @@ "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/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js", + "test:sdk": "yarn build:sdk && cd ./scripts/sdk/test/ && yarn --no-lockfile && node test-sdk-in-esm-vite-build-env.js && node test-sdk-in-commonjs-env.js", "start": "vite --port 3000", - "build": "vite build", - "build:sdk": "./scripts/sdk/build.sh" + "build": "vite build && ./scripts/cleanup.sh", + "build:sdk": "./scripts/sdk/build.sh", + "watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch" }, "repository": { "type": "git", @@ -30,6 +33,7 @@ "acorn": "^8.6.0", "acorn-walk": "^8.2.0", "aes-js": "^3.1.2", + "bs58": "^4.0.1", "core-js": "^3.6.5", "es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush", "escodegen": "^2.0.0", @@ -41,17 +45,18 @@ "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", - "vite": "^2.6.14", - "xxhashjs": "^0.2.2", - "bs58": "^4.0.1" + "vite": "^2.9.8", + "xxhashjs": "^0.2.2" }, "dependencies": { "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", - "dompurify": "^2.3.0" + "dompurify": "^2.3.0", + "off-color": "^2.0.0" } } diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 00000000..1cdfca84 --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + "env": { + "node": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "no-console": "off", + "no-empty": "off", + "no-prototype-builtins": "off", + "no-unused-vars": "warn" + }, +}; + 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..da2db73b --- /dev/null +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -0,0 +1,282 @@ +/* +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 path = require('path'); + +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; +} + +function getRootSectionWithVariables(variables) { + return `:root{\n${Object.entries(variables).reduce((acc, [key, value]) => acc + `--${key}: ${value};\n`, "")} }\n\n`; +} + +function appendVariablesToCSS(variables, cssSource) { + return cssSource + getRootSectionWithVariables(variables); +} + +function addThemesToConfig(bundle, manifestLocations, defaultThemes) { + for (const [fileName, info] of Object.entries(bundle)) { + if (fileName === "config.json") { + const source = new TextDecoder().decode(info.source); + const config = JSON.parse(source); + config["themeManifests"] = manifestLocations; + config["defaultTheme"] = defaultThemes; + info.source = new TextEncoder().encode(JSON.stringify(config, undefined, 2)); + } + } +} + +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, defaultThemes = {}; + let isDevelopment = false; + const virtualModuleId = '@theme/' + const resolvedVirtualModuleId = '\0' + virtualModuleId; + + return { + name: "build-themes", + enforce: "pre", + + configResolved(config) { + if (config.command === "serve") { + isDevelopment = true; + } + }, + + async buildStart() { + if (isDevelopment) { return; } + const { themeConfig } = options; + for (const [name, location] of Object.entries(themeConfig.themes)) { + manifest = require(`${location}/manifest.json`); + variants = manifest.values.variants; + for (const [variant, details] of Object.entries(variants)) { + const fileName = `theme-${name}-${variant}.css`; + if (name === themeConfig.default && details.default) { + // This is the default theme, stash the file name for later + if (details.dark) { + defaultDark = fileName; + defaultThemes["dark"] = `${name}-${variant}`; + } + else { + defaultLight = fileName; + defaultThemes["light"] = `${name}-${variant}`; + } + } + // emit the css as built theme bundle + this.emitFile({ + type: "chunk", + id: `${location}/theme.css?variant=${variant}${details.dark? "&dark=true": ""}`, + fileName, + }); + } + // emit the css as runtime theme bundle + this.emitFile({ + type: "chunk", + id: `${location}/theme.css?type=runtime`, + fileName: `theme-${name}-runtime.css`, + }); + } + }, + + resolveId(id) { + if (id.startsWith(virtualModuleId)) { + return '\0' + id; + } + }, + + async load(id) { + if (isDevelopment) { + /** + * To load the theme during dev, we need to take a different approach because emitFile is not supported in dev. + * We solve this by resolving virtual file "@theme/name/variant" into the necessary css import. + * This virtual file import is removed when hydrogen is built (see transform hook). + */ + if (id.startsWith(resolvedVirtualModuleId)) { + let [theme, variant, file] = id.substr(resolvedVirtualModuleId.length).split("/"); + if (theme === "default") { + theme = options.themeConfig.default; + } + const location = options.themeConfig.themes[theme]; + const manifest = require(`${location}/manifest.json`); + const variants = manifest.values.variants; + if (!variant || variant === "default") { + // choose the first default variant for now + // this will need to support light/dark variants as well + variant = Object.keys(variants).find(variantName => variants[variantName].default); + } + if (!file) { + file = "index.js"; + } + switch (file) { + case "index.js": { + const isDark = variants[variant].dark; + return `import "${path.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` + + `import "@theme/${theme}/${variant}/variables.css"`; + } + case "variables.css": { + const variables = variants[variant].variables; + const css = getRootSectionWithVariables(variables); + return css; + } + } + } + } + else { + const result = id.match(/(.+)\/theme.css\?variant=([^&]+)/); + if (result) { + const [, location, variant] = result; + const cssSource = await readCSSSource(location); + const config = variants[variant]; + return appendVariablesToCSS(config.variables, cssSource); + } + return null; + } + }, + + transform(code, id) { + if (isDevelopment) { + return; + } + /** + * Removes develop-only script tag; this cannot be done in transformIndexHtml hook because + * by the time that hook runs, the import is added to the bundled js file which would + * result in a runtime error. + */ + + const devScriptTag = + / + + diff --git a/scripts/sdk/test/package.json b/scripts/sdk/test/package.json new file mode 100644 index 00000000..a81da82c --- /dev/null +++ b/scripts/sdk/test/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-sdk", + "version": "0.0.0", + "description": "", + "dependencies": { + "hydrogen-view-sdk": "link:../../../target" + } +} diff --git a/scripts/sdk/test/test-sdk-in-commonjs-env.js b/scripts/sdk/test/test-sdk-in-commonjs-env.js new file mode 100644 index 00000000..3fd19d46 --- /dev/null +++ b/scripts/sdk/test/test-sdk-in-commonjs-env.js @@ -0,0 +1,13 @@ +// Make sure the SDK can be used in a CommonJS environment. +// Usage: node scripts/sdk/test/test-sdk-in-commonjs-env.js +const hydrogenViewSdk = require('hydrogen-view-sdk'); + +// Test that the "exports" are available: +// Worker +require.resolve('hydrogen-view-sdk/main.js'); +// Styles +require.resolve('hydrogen-view-sdk/theme-element-light.css'); +// Can access files in the assets/* directory +require.resolve('hydrogen-view-sdk/assets/main.js'); + +console.log('SDK works in CommonJS ✅'); diff --git a/scripts/sdk/test/test-sdk-in-esm-vite-build-env.js b/scripts/sdk/test/test-sdk-in-esm-vite-build-env.js new file mode 100644 index 00000000..6fc87da7 --- /dev/null +++ b/scripts/sdk/test/test-sdk-in-esm-vite-build-env.js @@ -0,0 +1,19 @@ +const { resolve } = require('path'); +const { build } = require('vite'); + +async function main() { + await build({ + outDir: './dist', + build: { + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html') + } + } + } + }); + + console.log('SDK works in Vite build ✅'); +} + +main(); diff --git a/src/domain/AccountSetupViewModel.js b/src/domain/AccountSetupViewModel.js index 4ad0d8d5..e7c1301f 100644 --- a/src/domain/AccountSetupViewModel.js +++ b/src/domain/AccountSetupViewModel.js @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; import {KeyType} from "../matrix/ssss/index"; import {Status} from "./session/settings/KeyBackupViewModel.js"; export class AccountSetupViewModel extends ViewModel { - constructor(accountSetup) { - super(); - this._accountSetup = accountSetup; + constructor(options) { + super(options); + this._accountSetup = options.accountSetup; this._dehydratedDevice = undefined; this._decryptDehydratedDeviceViewModel = undefined; if (this._accountSetup.encryptedDehydratedDevice) { @@ -53,7 +53,7 @@ export class AccountSetupViewModel extends ViewModel { // this vm adopts the same shape as KeyBackupViewModel so the same view can be reused. class DecryptDehydratedDeviceViewModel extends ViewModel { constructor(accountSetupViewModel, decryptedCallback) { - super(); + super(accountSetupViewModel.options); this._accountSetupViewModel = accountSetupViewModel; this._isBusy = false; this._status = Status.SetupKey; diff --git a/src/domain/LogoutViewModel.js b/src/domain/LogoutViewModel.ts similarity index 76% rename from src/domain/LogoutViewModel.js rename to src/domain/LogoutViewModel.ts index f22637de..3edfcad5 100644 --- a/src/domain/LogoutViewModel.js +++ b/src/domain/LogoutViewModel.ts @@ -14,11 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "./ViewModel.js"; +import {Options, ViewModel} from "./ViewModel"; import {Client} from "../matrix/Client.js"; -export class LogoutViewModel extends ViewModel { - constructor(options) { +type LogoutOptions = { sessionId: string; } & Options; + +export class LogoutViewModel extends ViewModel { + private _sessionId: string; + private _busy: boolean; + private _showConfirm: boolean; + private _error?: Error; + + constructor(options: LogoutOptions) { super(options); this._sessionId = options.sessionId; this._busy = false; @@ -26,19 +33,19 @@ export class LogoutViewModel extends ViewModel { this._error = undefined; } - get showConfirm() { + get showConfirm(): boolean { return this._showConfirm; } - get busy() { + get busy(): boolean { return this._busy; } - get cancelUrl() { + get cancelUrl(): string { return this.urlCreator.urlForSegment("session", true); } - async logout() { + async logout(): Promise { this._busy = true; this._showConfirm = false; this.emitChange("busy"); @@ -53,7 +60,7 @@ export class LogoutViewModel extends ViewModel { } } - get status() { + get status(): string { if (this._error) { return this.i18n`Could not log out of device: ${this._error.message}`; } else { diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 70f5b554..2711cd2f 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -18,9 +18,9 @@ import {Client} from "../matrix/Client.js"; import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {LoginViewModel} from "./login/LoginViewModel.js"; -import {LogoutViewModel} from "./LogoutViewModel.js"; +import {LogoutViewModel} from "./LogoutViewModel"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; export class RootViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 24df2546..abc16299 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -17,7 +17,7 @@ limitations under the License. import {AccountSetupViewModel} from "./AccountSetupViewModel.js"; import {LoadStatus} from "../matrix/Client.js"; import {SyncStatus} from "../matrix/Sync.js"; -import {ViewModel} from "./ViewModel.js"; +import {ViewModel} from "./ViewModel"; export class SessionLoadViewModel extends ViewModel { constructor(options) { @@ -43,7 +43,7 @@ export class SessionLoadViewModel extends ViewModel { this.emitChange("loading"); this._waitHandle = this._client.loadStatus.waitFor(s => { if (s === LoadStatus.AccountSetup) { - this._accountSetupViewModel = new AccountSetupViewModel(this._client.accountSetup); + this._accountSetupViewModel = new AccountSetupViewModel(this.childOptions({accountSetup: this._client.accountSetup})); } else { this._accountSetupViewModel = undefined; } diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index e4bbc7ec..e486c64f 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -15,8 +15,8 @@ limitations under the License. */ import {SortedArray} from "../observable/index.js"; -import {ViewModel} from "./ViewModel.js"; -import {avatarInitials, getIdentifierColorNumber} from "./avatar.js"; +import {ViewModel} from "./ViewModel"; +import {avatarInitials, getIdentifierColorNumber} from "./avatar"; class SessionItemViewModel extends ViewModel { constructor(options, pickerVM) { diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.ts similarity index 59% rename from src/domain/ViewModel.js rename to src/domain/ViewModel.ts index 0c665194..0bc52f6e 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.ts @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,54 +22,80 @@ limitations under the License. import {EventEmitter} from "../utils/EventEmitter"; import {Disposables} from "../utils/Disposables"; -export class ViewModel extends EventEmitter { - constructor(options = {}) { +import type {Disposable} from "../utils/Disposables"; +import type {Platform} from "../platform/web/Platform"; +import type {Clock} from "../platform/web/dom/Clock"; +import type {ILogger} from "../logging/types"; +import type {Navigation} from "./navigation/Navigation"; +import type {URLRouter} from "./navigation/URLRouter"; + +export type Options = { + platform: Platform + logger: ILogger + urlCreator: URLRouter + navigation: Navigation + emitChange?: (params: any) => void +} + +export class ViewModel extends EventEmitter<{change: never}> { + private disposables?: Disposables; + private _isDisposed = false; + private _options: Readonly; + + constructor(options: Readonly) { super(); - this.disposables = null; - this._isDisposed = false; this._options = options; } - childOptions(explicitOptions) { - const {navigation, urlCreator, platform} = this._options; - return Object.assign({navigation, urlCreator, platform}, explicitOptions); + childOptions(explicitOptions: T): T & Options { + return Object.assign({}, this._options, explicitOptions); } + get options(): Readonly { return this._options; } + // makes it easier to pass through dependencies of a sub-view model - getOption(name) { + getOption(name: N): O[N] { return this._options[name]; } - track(disposable) { + observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void) { + const segmentObservable = this.navigation.observe(type); + const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => { + onChange(value, type); + }) + this.track(unsubscribe); + } + + track(disposable: D): D { if (!this.disposables) { this.disposables = new Disposables(); } return this.disposables.track(disposable); } - untrack(disposable) { + untrack(disposable: Disposable): undefined { if (this.disposables) { return this.disposables.untrack(disposable); } - return null; + return undefined; } - dispose() { + dispose(): void { if (this.disposables) { this.disposables.dispose(); } this._isDisposed = true; } - get isDisposed() { + get isDisposed(): boolean { return this._isDisposed; } - disposeTracked(disposable) { + disposeTracked(disposable: Disposable | undefined): undefined { if (this.disposables) { return this.disposables.disposeTracked(disposable); } - return null; + return undefined; } // TODO: this will need to support binding @@ -76,7 +103,7 @@ export class ViewModel extends EventEmitter { // // translated string should probably always be bindings, unless we're fine with a refresh when changing the language? // we probably are, if we're using routing with a url, we could just refresh. - i18n(parts, ...expr) { + i18n(parts: TemplateStringsArray, ...expr: any[]) { // just concat for now let result = ""; for (let i = 0; i < parts.length; ++i) { @@ -88,11 +115,7 @@ export class ViewModel extends EventEmitter { return result; } - updateOptions(options) { - this._options = Object.assign(this._options, options); - } - - emitChange(changedProps) { + emitChange(changedProps: any): void { if (this._options.emitChange) { this._options.emitChange(changedProps); } else { @@ -100,27 +123,23 @@ export class ViewModel extends EventEmitter { } } - get platform() { + get platform(): Platform { return this._options.platform; } - get clock() { + get clock(): Clock { return this._options.platform.clock; } - get logger() { + get logger(): ILogger { return this.platform.logger; } - /** - * The url router, only meant to be used to create urls with from view models. - * @return {URLRouter} - */ - get urlCreator() { + get urlCreator(): URLRouter { return this._options.urlCreator; } - get navigation() { + get navigation(): Navigation { return this._options.navigation; } } diff --git a/src/domain/avatar.js b/src/domain/avatar.ts similarity index 74% rename from src/domain/avatar.js rename to src/domain/avatar.ts index 5b32020b..6f1ef8b0 100644 --- a/src/domain/avatar.js +++ b/src/domain/avatar.ts @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function avatarInitials(name) { +import { Platform } from "../platform/web/Platform"; +import { MediaRepository } from "../matrix/net/MediaRepository"; + +export function avatarInitials(name: string): string { let firstChar = name.charAt(0); if (firstChar === "!" || firstChar === "@" || firstChar === "#") { firstChar = name.charAt(1); @@ -29,10 +32,10 @@ export function avatarInitials(name) { * * @return {number} */ -function hashCode(str) { +function hashCode(str: string): number { let hash = 0; - let i; - let chr; + let i: number; + let chr: number; if (str.length === 0) { return hash; } @@ -44,11 +47,11 @@ function hashCode(str) { return Math.abs(hash); } -export function getIdentifierColorNumber(id) { +export function getIdentifierColorNumber(id: string): number { return (hashCode(id) % 8) + 1; } -export function getAvatarHttpUrl(avatarUrl, cssSize, platform, mediaRepository) { +export function getAvatarHttpUrl(avatarUrl: string, cssSize: number, platform: Platform, mediaRepository: MediaRepository): string | null { if (avatarUrl) { const imageSize = cssSize * platform.devicePixelRatio; return mediaRepository.mxcUrlThumbnail(avatarUrl, imageSize, imageSize, "crop"); diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index daa2aa9f..d41d53ec 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {LoginFailure} from "../../matrix/Client.js"; export class CompleteSSOLoginViewModel extends ViewModel { diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index b91df4cc..bf77e624 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Client} from "../../matrix/Client.js"; -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index 5fd8271f..7c4ff78a 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {LoginFailure} from "../../matrix/Client.js"; export class PasswordLoginViewModel extends ViewModel { diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js index 54218d22..dba0bcb5 100644 --- a/src/domain/login/StartSSOLoginViewModel.js +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; export class StartSSOLoginViewModel extends ViewModel{ constructor(options) { diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js index 51a9b7a4..12b4fbd5 100644 --- a/src/domain/session/CreateRoomViewModel.js +++ b/src/domain/session/CreateRoomViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {imageToInfo} from "./common.js"; import {RoomType} from "../../matrix/room/common"; diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index d89d821a..a7d19054 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {addPanelIfNeeded} from "../navigation/index.js"; function dedupeSparse(roomIds) { diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index 3f2263ac..8f1d0748 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {createEnum} from "../../utils/enum"; import {ConnectionStatus} from "../../matrix/net/Reconnector"; import {SyncStatus} from "../../matrix/Sync.js"; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 24276f42..a67df3a7 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -25,7 +25,7 @@ import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {CreateRoomViewModel} from "./CreateRoomViewModel.js"; -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../ViewModel"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; diff --git a/src/domain/session/leftpanel/BaseTileViewModel.js b/src/domain/session/leftpanel/BaseTileViewModel.js index b360b1d4..8f5106bf 100644 --- a/src/domain/session/leftpanel/BaseTileViewModel.js +++ b/src/domain/session/leftpanel/BaseTileViewModel.js @@ -15,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; -import {ViewModel} from "../../ViewModel.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; +import {ViewModel} from "../../ViewModel"; const KIND_ORDER = ["roomBeingCreated", "invite", "room"]; diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 843ed1ca..2fd3ca7e 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index f6cbd747..b3c8278c 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {RoomType} from "../../../matrix/room/common"; -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; export class MemberDetailsViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 5b8bb83e..b75a3d1c 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {MemberTileViewModel} from "./MemberTileViewModel.js"; import {createMemberComparator} from "./members/comparator.js"; import {Disambiguator} from "./members/disambiguator.js"; diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index eac6a6d4..153c70c8 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {ViewModel} from "../../ViewModel"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; export class MemberTileViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 3cfe378b..b4b6b4eb 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js"; import {MemberListViewModel} from "./MemberListViewModel.js"; import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js"; diff --git a/src/domain/session/rightpanel/RoomDetailsViewModel.js b/src/domain/session/rightpanel/RoomDetailsViewModel.js index 5e509fd5..4e2735b1 100644 --- a/src/domain/session/rightpanel/RoomDetailsViewModel.js +++ b/src/domain/session/rightpanel/RoomDetailsViewModel.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {ViewModel} from "../../ViewModel"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; export class RoomDetailsViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/ComposerViewModel.js b/src/domain/session/room/ComposerViewModel.js index 730e1b20..c20f6e86 100644 --- a/src/domain/session/room/ComposerViewModel.js +++ b/src/domain/session/room/ComposerViewModel.js @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class ComposerViewModel extends ViewModel { constructor(roomVM) { - super(); + super(roomVM.options); this._roomVM = roomVM; this._isEmpty = true; this._replyVM = null; diff --git a/src/domain/session/room/InviteViewModel.js b/src/domain/session/room/InviteViewModel.js index 81a08e44..00697642 100644 --- a/src/domain/session/room/InviteViewModel.js +++ b/src/domain/session/room/InviteViewModel.js @@ -15,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; -import {ViewModel} from "../../ViewModel.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; +import {ViewModel} from "../../ViewModel"; export class InviteViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index f6da39b0..8ce8757a 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class LightboxViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index f98c86f9..b503af73 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -15,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; -import {ViewModel} from "../../ViewModel.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar"; +import {ViewModel} from "../../ViewModel"; export class RoomBeingCreatedViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 90464870..7657eb13 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -17,18 +17,21 @@ limitations under the License. import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {ComposerViewModel} from "./ComposerViewModel.js" -import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; -import {tilesCreator} from "./timeline/tilesCreator.js"; -import {ViewModel} from "../../ViewModel.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) { super(options); - const {room} = options; + const {room, tileClassForEntry} = options; this._room = room; this._timelineVM = null; - this._tilesCreator = null; + this._tileClassForEntry = tileClassForEntry ?? defaultTileClassForEntry; + this._tileOptions = undefined; this._onRoomChange = this._onRoomChange.bind(this); this._timelineError = null; this._sendError = null; @@ -47,12 +50,13 @@ export class RoomViewModel extends ViewModel { try { const timeline = await this._room.openTimeline(); console.log('timeline', timeline.entries); - 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"); @@ -162,7 +166,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/UnknownRoomViewModel.js b/src/domain/session/room/UnknownRoomViewModel.js index e7969298..8bb5fb0a 100644 --- a/src/domain/session/room/UnknownRoomViewModel.js +++ b/src/domain/session/room/UnknownRoomViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; export class UnknownRoomViewModel extends ViewModel { constructor(options) { @@ -55,4 +55,4 @@ export class UnknownRoomViewModel extends ViewModel { get kind() { return "unknown"; } -} \ No newline at end of file +} diff --git a/src/domain/session/room/timeline/MessageBody.js b/src/domain/session/room/timeline/MessageBody.js index a8bf2497..65b487a9 100644 --- a/src/domain/session/room/timeline/MessageBody.js +++ b/src/domain/session/room/timeline/MessageBody.js @@ -1,5 +1,5 @@ import { linkify } from "./linkify/linkify.js"; -import { getIdentifierColorNumber, avatarInitials } from "../../../avatar.js"; +import { getIdentifierColorNumber, avatarInitials } from "../../../avatar"; /** * Parse text into parts such as newline, links and text. diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index fa48bec0..1977b6f4 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -13,7 +13,7 @@ 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 {ObservableMap} from "../../../../observable/map/ObservableMap.js"; +import {ObservableMap} from "../../../../observable/map/ObservableMap"; export class ReactionsViewModel { constructor(parentTile) { @@ -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..173b0cf6 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: () => 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: () => 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 c259168f..fc16774f 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -32,15 +32,15 @@ to the room timeline, which unload entries from memory. when loading, it just reads events from a sortkey backwards or forwards... */ import {TilesCollection} from "./TilesCollection.js"; -import {ViewModel} from "../../../ViewModel.js"; +import {ViewModel} from "../../../ViewModel"; export class TimelineViewModel extends ViewModel { constructor(options) { console.log('TimelineViewModel asdf', 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 10ad7b7c..560cdf2f 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -16,11 +16,11 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; -import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar.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() { @@ -126,23 +126,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 4c1c1de0..04141576 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -15,13 +15,14 @@ limitations under the License. */ import {UpdateAction} from "../UpdateAction.js"; -import {ViewModel} from "../../../../ViewModel.js"; +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; + this._emitUpdate = undefined; } // view model props for all subclasses // hmmm, could also do instanceof ... ? @@ -44,6 +45,10 @@ export class SimpleTile extends ViewModel { return this._entry.asEventKey(); } + get eventId() { + return this._entry.id; + } + get isPending() { return this._entry.isPending; } @@ -63,16 +68,20 @@ export class SimpleTile extends ViewModel { // TilesCollection contract below setUpdateEmit(emitUpdate) { - this.updateOptions({emitChange: paramName => { + this._emitUpdate = emitUpdate; + } + + /** overrides the emitChange in ViewModel to also emit the update over the tiles collection */ + emitChange(changedProps) { + if (this._emitUpdate) { // it can happen that after some network call // we switched away from the room and the response // comes in, triggering an emitChange in a tile that // has been disposed already (and hence the change // callback has been cleared by dispose) We should just ignore this. - if (emitUpdate) { - emitUpdate(this, paramName); - } - }}); + this._emitUpdate(this, changedProps); + } + super.emitChange(changedProps); } get upperEntry() { 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/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index b44de7e5..243b0d7c 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 0b68f168..5c89236f 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../../ViewModel.js"; +import {ViewModel} from "../../ViewModel"; import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; class PushNotificationStatus { @@ -50,6 +50,7 @@ export class SettingsViewModel extends ViewModel { this.minSentImageSizeLimit = 400; this.maxSentImageSizeLimit = 4000; this.pushNotifications = new PushNotificationStatus(); + this._activeTheme = undefined; } get _session() { @@ -76,6 +77,9 @@ export class SettingsViewModel extends ViewModel { this.sentImageSizeLimit = await this.platform.settingsStorage.getInt("sentImageSizeLimit"); this.pushNotifications.supported = await this.platform.notificationService.supportsPush(); this.pushNotifications.enabled = await this._session.arePushNotificationsEnabled(); + if (!import.meta.env.DEV) { + this._activeTheme = await this.platform.themeLoader.getActiveTheme(); + } this.emitChange(""); } @@ -127,6 +131,18 @@ export class SettingsViewModel extends ViewModel { return this._formatBytes(this._estimate?.usage); } + get themes() { + return this.platform.themeLoader.themes; + } + + get activeTheme() { + return this._activeTheme; + } + + setTheme(name) { + this.platform.themeLoader.setTheme(name); + } + _formatBytes(n) { if (typeof n === "number") { return Math.round(n / (1024 * 1024)).toFixed(1) + " MB"; diff --git a/src/index.html b/src/index.html index 456f095a..428da222 100644 --- a/src/index.html +++ b/src/index.html @@ -2,8 +2,6 @@ - - diff --git a/src/platform/web/main.js b/src/platform/web/main.js index 1729c17c..edc2cf14 100644 --- a/src/platform/web/main.js +++ b/src/platform/web/main.js @@ -32,6 +32,7 @@ export async function main(platform) { // const recorder = new RecordRequester(createFetchRequest(clock.createTimeout)); // const request = recorder.request; // window.getBrawlFetchLog = () => recorder.log(); + await platform.init(); const navigation = createNavigation(); platform.setNavigation(navigation); const urlRouter = createRouter({navigation, history: platform.history}); diff --git a/src/platform/web/sw.js b/src/platform/web/sw.js index c5f69438..088bc059 100644 --- a/src/platform/web/sw.js +++ b/src/platform/web/sw.js @@ -92,8 +92,12 @@ function isCacheableThumbnail(url) { const baseURL = new URL(self.registration.scope); let pendingFetchAbortController = new AbortController(); + async function handleRequest(request) { try { + if (request.url.includes("config.json") || /theme-.+\.json/.test(request.url)) { + return handleStaleWhileRevalidateRequest(request); + } const url = new URL(request.url); // rewrite / to /index.html so it hits the cache if (url.origin === baseURL.origin && url.pathname === baseURL.pathname) { @@ -119,6 +123,31 @@ async function handleRequest(request) { } } +/** + * Stale-while-revalidate caching for certain files + * see https://developer.chrome.com/docs/workbox/caching-strategies-overview/#stale-while-revalidate + */ +async function handleStaleWhileRevalidateRequest(request) { + let response = await readCache(request); + const networkResponsePromise = fetchAndUpdateCache(request); + if (response) { + return response; + } else { + return await networkResponsePromise; + } +} + +async function fetchAndUpdateCache(request) { + const response = await fetch(request, { + signal: pendingFetchAbortController.signal, + headers: { + "Cache-Control": "no-cache", + }, + }); + updateCache(request, response.clone()); + return response; +} + async function updateCache(request, response) { // don't write error responses to the cache if (response.status >= 400) { @@ -131,8 +160,14 @@ async function updateCache(request, response) { cache.put(request, response.clone()); } else if (request.url.startsWith(baseURL)) { let assetName = request.url.substr(baseURL.length); + let cacheName; if (HASHED_CACHED_ON_REQUEST_ASSETS.includes(assetName)) { - const cache = await caches.open(hashedCacheName); + cacheName = hashedCacheName; + } else if (UNHASHED_PRECACHED_ASSETS.includes(assetName)) { + cacheName = unhashedCacheName; + } + if (cacheName) { + const cache = await caches.open(cacheName); await cache.put(request, response.clone()); } } diff --git a/src/platform/web/ui/AvatarView.js b/src/platform/web/ui/AvatarView.js index f2d94e3b..551f7307 100644 --- a/src/platform/web/ui/AvatarView.js +++ b/src/platform/web/ui/AvatarView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseUpdateView} from "./general/BaseUpdateView"; -import {renderStaticAvatar, renderImg} from "./avatar.js"; +import {renderStaticAvatar, renderImg} from "./avatar"; /* optimization to not use a sub view when changing between img and text diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index 95fd9cae..92a89c0a 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -22,6 +22,7 @@ .RoomDetailsView_label, .RoomDetailsView_row, .RoomDetailsView, .MemberDetailsView, .EncryptionIconView { display: flex; align-items: center; + color: var(--text-color); } .RoomDetailsView_value { diff --git a/src/platform/web/ui/css/themes/element/element-logo.svg b/src/platform/web/ui/css/themes/element/element-logo.svg index 7e6c50fb..c0a94f34 100644 --- a/src/platform/web/ui/css/themes/element/element-logo.svg +++ b/src/platform/web/ui/css/themes/element/element-logo.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/src/platform/web/ui/css/themes/element/icons/chevron-down.svg b/src/platform/web/ui/css/themes/element/icons/chevron-down.svg index 6db33a25..d515ccd8 100644 --- a/src/platform/web/ui/css/themes/element/icons/chevron-down.svg +++ b/src/platform/web/ui/css/themes/element/icons/chevron-down.svg @@ -16,7 +16,7 @@ fill-rule="evenodd" clip-rule="evenodd" d="M 8.20723,2.70711 C 8.59775,3.09763 8.59878,3.73182 8.20952,4.1236 L 3.27581,9.08934 8.22556,14.0391 c 0.39052,0.3905 0.39155,1.0247 0.00229,1.4165 -0.38926,0.3918 -1.0214,0.3928 -1.41192,0.0023 L 1.15907,9.80101 C 0.768549,9.41049 0.767523,8.7763 1.15678,8.38452 L 6.79531,2.70939 C 7.18457,2.31761 7.8167,2.31658 8.20723,2.70711 Z" - fill="#8d99a5" + fill="#ff00ff" id="path830" /> - + diff --git a/src/platform/web/ui/css/themes/element/icons/chevron-right.svg b/src/platform/web/ui/css/themes/element/icons/chevron-right.svg index a7b862aa..64cd32e5 100644 --- a/src/platform/web/ui/css/themes/element/icons/chevron-right.svg +++ b/src/platform/web/ui/css/themes/element/icons/chevron-right.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/chevron-small.svg b/src/platform/web/ui/css/themes/element/icons/chevron-small.svg index 741e6be0..2f738557 100644 --- a/src/platform/web/ui/css/themes/element/icons/chevron-small.svg +++ b/src/platform/web/ui/css/themes/element/icons/chevron-small.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg b/src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg index 092bf4fb..a32dbdec 100644 --- a/src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg +++ b/src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/clear.svg b/src/platform/web/ui/css/themes/element/icons/clear.svg index 9227cf4d..2f4cea3e 100644 --- a/src/platform/web/ui/css/themes/element/icons/clear.svg +++ b/src/platform/web/ui/css/themes/element/icons/clear.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/platform/web/ui/css/themes/element/icons/disable-grid.svg b/src/platform/web/ui/css/themes/element/icons/disable-grid.svg index db870fcd..0870f4dc 100644 --- a/src/platform/web/ui/css/themes/element/icons/disable-grid.svg +++ b/src/platform/web/ui/css/themes/element/icons/disable-grid.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/dismiss.svg b/src/platform/web/ui/css/themes/element/icons/dismiss.svg index 1b9cea04..2f4cea3e 100644 --- a/src/platform/web/ui/css/themes/element/icons/dismiss.svg +++ b/src/platform/web/ui/css/themes/element/icons/dismiss.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/platform/web/ui/css/themes/element/icons/e2ee-disabled.svg b/src/platform/web/ui/css/themes/element/icons/e2ee-disabled.svg index 26e669fc..9dc7e09e 100644 --- a/src/platform/web/ui/css/themes/element/icons/e2ee-disabled.svg +++ b/src/platform/web/ui/css/themes/element/icons/e2ee-disabled.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/src/platform/web/ui/css/themes/element/icons/e2ee-normal.svg b/src/platform/web/ui/css/themes/element/icons/e2ee-normal.svg index 9d981ee7..b2ef4226 100644 --- a/src/platform/web/ui/css/themes/element/icons/e2ee-normal.svg +++ b/src/platform/web/ui/css/themes/element/icons/e2ee-normal.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/enable-grid.svg b/src/platform/web/ui/css/themes/element/icons/enable-grid.svg index 1e06f9b7..1a28f655 100644 --- a/src/platform/web/ui/css/themes/element/icons/enable-grid.svg +++ b/src/platform/web/ui/css/themes/element/icons/enable-grid.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/encryption-status.svg b/src/platform/web/ui/css/themes/element/icons/encryption-status.svg index 8c81d4cd..b18fd782 100644 --- a/src/platform/web/ui/css/themes/element/icons/encryption-status.svg +++ b/src/platform/web/ui/css/themes/element/icons/encryption-status.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/info.svg b/src/platform/web/ui/css/themes/element/icons/info.svg index d55e9356..e2f8472c 100644 --- a/src/platform/web/ui/css/themes/element/icons/info.svg +++ b/src/platform/web/ui/css/themes/element/icons/info.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/paperclip.svg b/src/platform/web/ui/css/themes/element/icons/paperclip.svg index 81a8bf06..314dda27 100644 --- a/src/platform/web/ui/css/themes/element/icons/paperclip.svg +++ b/src/platform/web/ui/css/themes/element/icons/paperclip.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/plus.svg b/src/platform/web/ui/css/themes/element/icons/plus.svg index ea197223..3800b982 100644 --- a/src/platform/web/ui/css/themes/element/icons/plus.svg +++ b/src/platform/web/ui/css/themes/element/icons/plus.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/room-members.svg b/src/platform/web/ui/css/themes/element/icons/room-members.svg index bc03be13..6d797052 100644 --- a/src/platform/web/ui/css/themes/element/icons/room-members.svg +++ b/src/platform/web/ui/css/themes/element/icons/room-members.svg @@ -2,6 +2,6 @@ - - + + diff --git a/src/platform/web/ui/css/themes/element/icons/search.svg b/src/platform/web/ui/css/themes/element/icons/search.svg index 1531c7ac..14f227be 100644 --- a/src/platform/web/ui/css/themes/element/icons/search.svg +++ b/src/platform/web/ui/css/themes/element/icons/search.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/send.svg b/src/platform/web/ui/css/themes/element/icons/send.svg index b47ab8ea..566a09ea 100644 --- a/src/platform/web/ui/css/themes/element/icons/send.svg +++ b/src/platform/web/ui/css/themes/element/icons/send.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/settings.svg b/src/platform/web/ui/css/themes/element/icons/settings.svg index 9d1fbc78..8d05e39c 100644 --- a/src/platform/web/ui/css/themes/element/icons/settings.svg +++ b/src/platform/web/ui/css/themes/element/icons/settings.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/icons/vertical-ellipsis.svg b/src/platform/web/ui/css/themes/element/icons/vertical-ellipsis.svg index 48a531e0..f3b1f3d0 100644 --- a/src/platform/web/ui/css/themes/element/icons/vertical-ellipsis.svg +++ b/src/platform/web/ui/css/themes/element/icons/vertical-ellipsis.svg @@ -1,3 +1,3 @@ - + diff --git a/src/platform/web/ui/css/themes/element/manifest.json b/src/platform/web/ui/css/themes/element/manifest.json new file mode 100644 index 00000000..ec1852cb --- /dev/null +++ b/src/platform/web/ui/css/themes/element/manifest.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "name": "element", + "values": { + "font-faces": [ + { + "font-family": "Inter", + "src": [{"asset": "/fonts/Inter.ttf", "format": "ttf"}] + } + ], + "variants": { + "light": { + "base": true, + "default": true, + "name": "Light", + "variables": { + "background-color-primary": "#fff", + "background-color-secondary": "#f6f6f6", + "text-color": "#2E2F32", + "accent-color": "#03b381", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#61708b", + "link-color": "#238cf5" + } + }, + "dark": { + "dark": true, + "default": true, + "name": "Dark", + "variables": { + "background-color-primary": "#21262b", + "background-color-secondary": "#2D3239", + "text-color": "#fff", + "accent-color": "#03B381", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#61708b", + "link-color": "#238cf5" + } + } + } + } +} + diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 383feeae..113ea254 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -15,17 +15,25 @@ See the License for the specific language governing permissions and limitations under the License. */ +@import url('../../main.css'); @import url('inter.css'); @import url('timeline.css'); :root { font-size: 10px; + /* Theme aliases */ + --icon-color: var(--background-color-secondary--darker-40); + --light-border: var(--background-color-secondary--darker-5); + --light-text-color: var(--background-color-secondary--darker-55); + --timeline-time-text-color: var(--background-color-secondary--darker-35); + --icon-background: var(--background-color-secondary--darker-7); + --right-panel-text-color: var(--background-color-secondary--darker-35); } .hydrogen { font-family: 'Inter', sans-serif, 'emoji'; - background-color: white; - color: #2e2f32; + background-color: var(--background-color-primary); + color: var(--text-color); font-size: 1.4rem; --usercolor1: #368BD6; --usercolor2: #AC3BA8; @@ -43,8 +51,8 @@ limitations under the License. .avatar { border-radius: 100%; - background: #fff; - color: white; + background: var(--background-color-primary); + color: var(--fixed-white); } .hydrogen .avatar.usercolor1 { background-color: var(--usercolor1); } @@ -59,7 +67,7 @@ limitations under the License. .logo { height: 48px; min-width: 48px; - background-image: url('element-logo.svg'); + background-image: url('element-logo.svg?primary=accent-color'); background-repeat: no-repeat; background-position: center; } @@ -82,6 +90,8 @@ limitations under the License. .form-row.text textarea { font-family: "Inter", sans-serif; + background-color: var(--background-color-secondary); + color: inherit; } .form-group { @@ -94,11 +104,13 @@ limitations under the License. .form-row.text > input, .form-row.text > textarea { padding: 12px; - border: 1px solid rgba(141, 151, 165, 0.15); + border: 1px solid var(--light-border); border-radius: 8px; margin-top: 5px; font-size: 1em; resize: vertical; + background-color: var(--background-color-secondary); + color: inherit; } .form-row.check { @@ -113,7 +125,7 @@ limitations under the License. .form-row .form-row-description { font-size: 1rem; - color: #777; + color: var(--light-text-color); margin: 8px 0 0 0; } @@ -129,26 +141,26 @@ a.button-action { .button-action.secondary { - color: #03B381; + color: var(--accent-color); } .button-action.primary { - background-color: #03B381; + background-color: var(--accent-color); border-radius: 8px; - color: white; + color: var(--fixed-white); font-weight: bold; } .button-action.primary:disabled { - color: #fffa; + color: var(--fixed-white--darker-10); } .button-action.primary.destructive { - background-color: #FF4B55; + background-color: var(--error-color); } .button-action.secondary.destructive { - color: #FF4B55; + color: var(--error-color); } .button-action { @@ -163,7 +175,7 @@ a.button-action { width: 32px; height: 32px; background-position: center; - background-color: #e1e3e6; + background-color: var(--icon-background); background-repeat: no-repeat; border: none; border-radius: 100%; @@ -171,26 +183,26 @@ a.button-action { } .button-utility.grid { - background-image: url('icons/enable-grid.svg'); + background-image: url('icons/enable-grid.svg?primary=icon-color'); } .button-utility.settings { - background-image: url('icons/settings.svg'); + background-image: url('icons/settings.svg?primary=icon-color'); } .button-utility.create { - background-image: url('icons/plus.svg'); + background-image: url('icons/plus.svg?primary=icon-color'); } .button-utility.grid.on { - background-image: url('icons/disable-grid.svg'); + background-image: url('icons/disable-grid.svg?primary=icon-color'); } .FilterField { - background-image: url('icons/search.svg'); + background-image: url('icons/search.svg?primary=icon-color'); background-repeat: no-repeat; background-position: 8px center; - background-color: #e1e3e6; + background-color: var(--icon-background); /* to prevent jumps when adding a border on focus */ border: 1px solid transparent; border-radius: 16px; @@ -201,11 +213,12 @@ a.button-action { } .FilterField:focus-within { - border: 1px #e1e3e6 solid; - background-color: white; + border: 1px var(--icon-background) solid; + background-color: var(--background-color-primary); } + .FilterField:focus-within button { - border-color: white; + border-color: var(--background-color-primary); } /*.FilterField:not(:focus-within) button { @@ -221,15 +234,16 @@ a.button-action { border: none; background-color: transparent; height: 100%; + color: var(--text-color); } .FilterField button { width: 30px; /* 32 - 1 (top) - 1 (bottom) */ height: 30px; /* 32 - 1 (top) - 1 (bottom) */ background-position: center; - background-color: #e1e3e6; + background-color: var(--icon-background); background-repeat: no-repeat; - background-image: url('icons/clear.svg'); + background-image: url('icons/clear.svg?primary=icon-color'); border: 7px solid transparent; /* 8 - 1 */ border-radius: 100%; box-sizing: border-box; @@ -249,12 +263,12 @@ a.button-action { } .StartSSOLoginView_button { - border: 1px solid #03B381; + border: 1px solid var(--accent-color); border-radius: 8px; } .LoginView_back { - background-image: url("./icons/chevron-left.svg"); + background-image: url("./icons/chevron-left.svg?primary=icon-color"); background-color: transparent; } @@ -266,7 +280,7 @@ a.button-action { .LoginView_forwardInfo { font-size: 0.9em; margin-left: 1em; - color: #777; + color: var(--light-text-color); } .CompleteSSOView_title { @@ -275,6 +289,7 @@ a.button-action { @media screen and (min-width: 600px) { .PreSessionScreen { + /* needs transparency support */ box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1); border-radius: 8px; } @@ -286,7 +301,7 @@ a.button-action { } .LeftPanel { - background: rgba(245, 245, 245, 0.90); + background: var(--background-color-secondary); font-size: 1.5rem; padding: 12px 8px 0 8px; } @@ -300,7 +315,7 @@ a.button-action { } .LeftPanel .filter { - border-bottom: 1px solid rgba(245, 245, 245, 0.90); + border-bottom: 1px solid var(--background-color-secondary); } .LeftPanel .filter input { @@ -335,7 +350,7 @@ a.button-action { } .RoomList li.active { - background: rgba(141, 151, 165, 0.1); + background: var(--background-color-secondary--darker-7); border-radius: 5px; } @@ -357,8 +372,8 @@ a.button-action { border-radius: 1.6rem; box-sizing: border-box; padding: 0.1rem 0.3rem; - background-color: #61708b; - color: white; + background-color: var(--room-badge); + color: var(--fixed-white); font-weight: bold; font-size: 1rem; line-height: 1.4rem; @@ -366,7 +381,7 @@ a.button-action { } .RoomList .badge.highlighted { - background-color: #ff4b55; + background-color: var(--error-color); } a { @@ -376,8 +391,8 @@ a { .SessionStatusView { padding: 4px; min-height: 22px; - background-color: #03B381; - color: white; + background-color: var(--accent-color); + color: var(--fixed-white); align-items: center; } @@ -386,7 +401,7 @@ a { text-align: left; } -.SessionStatusView > .end { +.SessionStatusView>.end { flex: 1; display: flex; justify-content: flex-end; @@ -397,7 +412,7 @@ a { .SessionStatusView .dismiss { border: none; background: none; - background-image: url('icons/dismiss.svg'); + background-image: url('icons/dismiss.svg?primary=background-color-primary'); background-position: center; background-repeat: no-repeat; width: 32px; @@ -418,9 +433,9 @@ a { .SessionPickerView .session-info { text-decoration: none; padding: 12px; - border: 1px solid rgba(141, 151, 165, 0.15); + border: 1px solid var(--light-border); border-radius: 8px; - background-image: url('icons/chevron-right.svg'); + background-image: url('./icons/chevron-right.svg?primary=icon-color'); background-position: center right 30px; background-repeat: no-repeat; font-weight: 500; @@ -445,12 +460,12 @@ a { } .SessionPickerView button.destructive { - color: #FF4B55; + color: var(--error-color); } .RoomGridView > div.container { - border-right: 1px solid rgba(245, 245, 245, 0.90); - border-bottom: 1px solid rgba(245, 245, 245, 0.90); + border-right: 1px solid var(--background-color-secondary); + border-bottom: 1px solid var(--background-color-secondary); } .RoomGridView > .focused > .room-placeholder .unfocused { @@ -462,21 +477,21 @@ a { } .room-placeholder .unfocused { - color: #8D99A5; + color: var(--right-panel-text-color); } .RoomGridView > div.focus-ring { - border: 2px solid rgba(134, 193, 165, 1); + border: 2px solid var(--accent-color--darker-5); border-radius: 12px; } .middle-header { box-sizing: border-box; flex: 0 0 56px; /* 12 + 32 + 12 to align with filter field + margin */ - background: white; + background: var(--background-color-primary); padding: 0 16px; - border-bottom: 1px solid rgba(245, 245, 245, 0.90); + border-bottom: 1px solid var(--background-color-secondary); } .middle-header h2 { @@ -492,7 +507,7 @@ a { } .close-middle, .close-session { - background-image: url('icons/chevron-left.svg'); + background-image: url('icons/chevron-left.svg?primary=icon-color'); background-position-x: 10px; } @@ -501,15 +516,11 @@ a { } .RoomHeader .room-options { - background-image: url("./icons/vertical-ellipsis.svg"); -} - -.RoomHeader .room-info { - background-image: url("./icons/info.svg"); + background-image: url("./icons/vertical-ellipsis.svg?primary=icon-color"); } .RoomView_error { - color: red; + color: var(--error-color); } .MessageComposer_replyPreview .Timeline_message { @@ -520,9 +531,9 @@ a { } .MessageComposer_replyPreview { - background: rgba(245, 245, 245, 0.90); + background: var(--background-color-secondary); margin: 0px 10px 10px 10px; - box-shadow: 0px 0px 5px #91919169; + box-shadow: 0px 0px 5px var(--background-color-secondary--darker-15); border-radius: 5px; } @@ -546,7 +557,7 @@ a { white-space: nowrap; overflow: hidden; background-color: transparent; - background-image: url('icons/clear.svg'); + background-image: url('icons/clear.svg?primary=icon-color'); background-repeat: no-repeat; background-position: center; background-size: 18px; @@ -554,7 +565,7 @@ a { } .MessageComposer_input:first-child { - border-top: 1px solid rgba(245, 245, 245, 0.90); + border-top: 1px solid var(--background-color-secondary); } .MessageComposer_input > :not(:first-child) { @@ -564,7 +575,8 @@ a { .MessageComposer_input > textarea { border: none; border-radius: 24px; - background: #F6F6F6; + background: var(--background-color-secondary); + color: inherit; font-size: 14px; font-family: "Inter", sans-serif; resize: none; @@ -586,8 +598,8 @@ a { text-indent: 200%; overflow: hidden; - background-color: #03B381; - background-image: url('icons/send.svg'); + background-color: var(--accent-color); + background-image: url('icons/send.svg?primary=background-color-primary'); background-repeat: no-repeat; background-position: center; align-self: end; @@ -603,7 +615,7 @@ a { white-space: nowrap; overflow: hidden; background-color: transparent; - background-image: url('icons/paperclip.svg'); + background-image: url('icons/paperclip.svg?primary=icon-color'); background-repeat: no-repeat; background-position: center; } @@ -682,7 +694,7 @@ a { } .error { - color: red; + color: var(--error-color); font-weight: 600; } @@ -694,21 +706,23 @@ button.link { cursor: pointer; margin: -12px; padding: 12px; + color: inherit; } .Settings a, .Settings .link { - color: #03B381; + color: var(--accent-color); font-weight: 600; } .lightbox { - background-color: rgba(0,0,0,0.75); + /* needs transparency support */ + background-color: rgba(0, 0, 0, 0.75); display: grid; grid-template: "content close" auto "content details" 1fr / 1fr auto; - color: white; + color: var(--background-color-primary); padding: 4px; } @@ -752,7 +766,7 @@ button.link { display: block; grid-area: close; justify-self: end; - background-image: url('icons/dismiss.svg'); + background-image: url('icons/dismiss.svg?primary=fixed-white'); background-position: center; background-size: 16px; background-repeat: no-repeat; @@ -770,9 +784,10 @@ button.link { .menu { border-radius: 8px; + /* needs transparency support */ box-shadow: 2px 2px 10px rgba(0,0,0,0.5); padding: 4px; - background-color: white; + background-color: var(--background-color-primary); list-style: none; margin: 0; } @@ -781,6 +796,10 @@ button.link { margin-bottom: 10px; } +.menu .menu-item { + color: var(--text-color); +} + .menu button { border-radius: 4px; border: none; @@ -793,7 +812,7 @@ button.link { } .menu .destructive button { - color: #FF4B55; + color: var(--error-color); } .menu .quick-reactions { @@ -842,7 +861,7 @@ button.link { grid-area: description; font-size: 1.2rem; margin: 0; - color: #777; + color: var(--light-text-color); } .InviteView_roomAvatar { @@ -877,7 +896,7 @@ button.link { .RoomArchivedView { padding: 12px; - background-color: rgba(245, 245, 245, 0.90); + background-color: var(--background-color-secondary); } .RoomArchivedView h3 { @@ -914,7 +933,7 @@ button.link { /* Right Panel */ .RightPanelView { - background: rgba(245, 245, 245, 0.90); + background: var(--background-color-secondary); } .RoomDetailsView { @@ -923,7 +942,7 @@ button.link { } .RoomDetailsView_id, .MemberDetailsView_id { - color: #737D8C; + color: var(--right-panel-text-color); font-size: 12px; } @@ -952,7 +971,7 @@ button.RoomDetailsView_row { } button.RoomDetailsView_row::after { - content: url("./icons/chevron-small.svg"); + content: url("./icons/chevron-small.svg?primary=icon-color"); margin-left: 12px; } @@ -972,16 +991,16 @@ button.RoomDetailsView_row::after { } .RoomDetailsView_value { - color: #737D8C; + color: var(--right-panel-text-color); flex: 1; } .MemberCount::before { - content: url("./icons/room-members.svg"); + content: url("./icons/room-members.svg?primary=icon-color"); } .EncryptionStatus::before { - content: url("./icons/encryption-status.svg"); + content: url("./icons/encryption-status.svg?primary=icon-color"); } /* Encryption icon next to avatar */ @@ -990,8 +1009,8 @@ button.RoomDetailsView_row::after { width: 52px; height: 52px; border-radius: 100%; - background: #737D8C; - border: 3px solid #F2F5F8; + background: var(--right-panel-text-color); + border: 3px solid var(--background-color-secondary); margin-left: -16px; } @@ -1001,11 +1020,11 @@ button.RoomDetailsView_row::after { } .EncryptionIconView_encrypted { - content: url("./icons/e2ee-normal.svg"); + content: url("./icons/e2ee-normal.svg?primary=fixed-white"); } .EncryptionIconView_unencrypted { - content: url("./icons/e2ee-disabled.svg"); + content: url("./icons/e2ee-disabled.svg?primary=fixed-white"); } .RightPanelView_buttons .button-utility { @@ -1014,11 +1033,11 @@ button.RoomDetailsView_row::after { } .RightPanelView_buttons .close { - background-image: url("./icons/clear.svg"); + background-image: url("./icons/clear.svg?primary=icon-color"); } .RightPanelView_buttons .back { - background-image: url("./icons/chevron-thin-left.svg"); + background-image: url("./icons/chevron-thin-left.svg?primary=icon-color"); } /* Memberlist Panel */ @@ -1059,7 +1078,7 @@ button.RoomDetailsView_row::after { .MemberDetailsView_label { font-size: 12px; font-weight: 600; - color: #8d99a5; + color: var(--right-panel-text-color); text-transform: uppercase; } @@ -1075,7 +1094,7 @@ button.RoomDetailsView_row::after { } .MemberDetailsView_options a, .MemberDetailsView_options button { - color: #0dbd8b; + color: var(--accent-color); text-decoration: none; margin: 0 0 3px 0; padding: 0; @@ -1130,8 +1149,8 @@ button.RoomDetailsView_row::after { width: 64px; height: 64px; border-radius: 100%; - background-color: #e1e3e6; - background-image: url('icons/plus.svg'); + background-color: var(--icon-background); + background-image: url('icons/plus.svg?primary=icon-color'); background-repeat: no-repeat; background-position: center; background-size: 36px; diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 90fee243..bac4b4a5 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -21,10 +21,10 @@ limitations under the License. bottom: 16px; right: 32px; border-radius: 100%; - border: 1px solid #8d99a5; - background-image: url("./icons/chevron-down.svg"); + border: 1px solid var(--background-color-secondary--darker-7); + background-image: url("./icons/chevron-down.svg?primary=icon-color"); background-position: center; - background-color: white; + background-color: var(--background-color-primary--darker-10); background-repeat: no-repeat; cursor: pointer; } @@ -77,6 +77,7 @@ limitations under the License. } .Timeline_message:hover:not(.disabled), .Timeline_message.selected, .Timeline_message.menuOpen { + /* needs transparency support */ background-color: rgba(141, 151, 165, 0.1); border-radius: 4px; } @@ -119,10 +120,11 @@ limitations under the License. margin-top: -12px; margin-right: 4px; /* button visuals */ - border: #ccc 1px solid; + border: var(--background-color-primary--darker-10) 1px solid; height: 24px; width: 24px; - background-color: #fff; + background-color: var(--background-color-primary); + color: inherit; border-radius: 4px; padding: 0; text-align: center; @@ -142,12 +144,12 @@ limitations under the License. .Timeline_messageBody time, .Timeline_messageTime { font-size: 0.8em; line-height: normal; - color: #aaa; + color: var(--timeline-time-text-color); } .Timeline_messageBody.statusMessage { font-style: italic; - color: #777; + color: var(--light-text-color); } .Timeline_messageBody { @@ -190,7 +192,7 @@ limitations under the License. } .Timeline_messageBody a.link { - color: #238cf5; + color: var(--link-color); text-decoration: none; } @@ -221,6 +223,7 @@ so the timeline doesn't jump when the image loads */ border-radius: 4px; display: block; } + /* stretch the image (to the spacer) on platforms where we can trust the spacer to always have the correct height, otherwise the image starts with height 0 and with loading=lazy @@ -249,21 +252,24 @@ only loads when the top comes into view*/ .Timeline_messageBody .media > time, .Timeline_messageBody .media > .sendStatus { - color: #2e2f32; + color: var(--text-color); display: block; padding: 2px; margin: 4px; + /* needs transparency support */ background-color: rgba(255, 255, 255, 0.75); border-radius: 4px; } + .Timeline_messageBody .media > .spacer { /* TODO: can we implement this with a pseudo element? or perhaps they are not grid items? */ width: 100%; /* don't stretch height as it is a spacer, just in case it doesn't match with image height */ align-self: start; } + .Timeline_messageBody code, .Timeline_messageBody pre { - background-color: #f8f8f8; + background-color: var(--background-color-secondary); font-family: monospace; font-size: 0.9em; } @@ -275,13 +281,13 @@ only loads when the top comes into view*/ } .Timeline_messageBody pre { - border: 1px solid rgb(229, 229, 229); + border: 1px solid var(--light-border); padding: 0.5em; max-height: 30em; overflow: auto; } -.Timeline_messageBody pre > code { +.Timeline_messageBody pre>code { background-color: unset; border-radius: unset; display: block; @@ -291,17 +297,17 @@ only loads when the top comes into view*/ .Timeline_messageBody blockquote { margin-left: 0; padding-left: 20px; - border-left: 4px solid rgb(229, 229, 229); + border-left: 4px solid var(--light-border); } .Timeline_messageBody table { - border: 1px solid rgb(206, 206, 206); + border: 1px solid var(--background-color-secondary--darker-15); border-radius: 2px; border-spacing: 0; } .Timeline_messageBody thead th { - border-bottom: 1px solid rgb(206, 206, 206); + border-bottom: 1px solid var(--background-color-secondary--darker-15); } .Timeline_messageBody td, .Timeline_messageBody th { @@ -309,14 +315,14 @@ only loads when the top comes into view*/ } .Timeline_messageBody tbody tr:nth-child(2n) { - background-color: #f6f6f6; + background-color: var(--background-color-secondary); } .Timeline_messageBody .pill { padding: 0px 5px; border-radius: 15px; - background-color: #f6f6f6; - border: 1px solid rgb(206, 206, 206); + background-color: var(--background-color-secondary); + border: 1px solid var(--background-color-secondary--darker-10); text-decoration: none; display: inline-flex; align-items: center; @@ -331,11 +337,11 @@ only loads when the top comes into view*/ } .Timeline_message.unsent .Timeline_messageBody { - color: #ccc; + color: var(--light-text-color); } .Timeline_message.unverified .Timeline_messageBody { - color: #ff4b55; + color: var(--error-color); } .Timeline_messageReactions { @@ -348,22 +354,28 @@ only loads when the top comes into view*/ line-height: 2.0rem; margin-right: 6px; padding: 1px 6px; - border: 1px solid #e9edf1; + border: 1px solid var(--light-border); border-radius: 10px; - background-color: #f3f8fd; + background-color: var(--background-color-secondary); + color: inherit; cursor: pointer; user-select: none; vertical-align: middle; } .Timeline_messageReactions button.active { - background-color: #e9fff9; - border-color: #0DBD8B; + background-color: var(--background-color-secondary); + border-color: var(--accent-color); } @keyframes glow-reaction-border { - 0% { border-color: #e9edf1; } - 100% { border-color: #0DBD8B; } + 0% { + border-color: var(--background-color-secondary); + } + + 100% { + border-color: var(--accent-color); + } } .Timeline_messageReactions button.active.pending { @@ -377,8 +389,8 @@ only loads when the top comes into view*/ .Timeline_locationLink { padding: 0px 8px; border-radius: 16px; - border: 1px solid #e9edf1; - background-color: #f3f8fd; + border: 1px solid var(--light-border); + background-color: var(--background-color-secondary); text-decoration: none; display: inline-block; line-height: 2rem; @@ -394,7 +406,7 @@ only loads when the top comes into view*/ .AnnouncementView > div { margin: 0 auto; padding: 10px 20px; - background-color: rgba(245, 245, 245, 0.90); + background-color: var(--background-color-secondary); text-align: center; border-radius: 10px; } diff --git a/src/platform/web/ui/general/TemplateView.ts b/src/platform/web/ui/general/TemplateView.ts index ce593f75..d6e3dd3f 100644 --- a/src/platform/web/ui/general/TemplateView.ts +++ b/src/platform/web/ui/general/TemplateView.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS, ClassNames, Child} from "./html"; +import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS, ClassNames, Child as NonBoundChild} from "./html"; import {mountView} from "./utils"; import {BaseUpdateView, IObservableValue} from "./BaseUpdateView"; import {IMountArgs, ViewNode, IView} from "./types"; @@ -30,12 +30,15 @@ function objHasFns(obj: ClassNames): obj is { [className: string]: bool } export type RenderFn = (t: Builder, vm: T) => ViewNode; +type TextBinding = (T) => string | number | boolean | undefined | null; +type Child = NonBoundChild | TextBinding; +type Children = Child | Child[]; type EventHandler = ((event: Event) => void); type AttributeStaticValue = string | boolean; type AttributeBinding = (value: T) => AttributeStaticValue; export type AttrValue = AttributeStaticValue | AttributeBinding | EventHandler | ClassNames; export type Attributes = { [attribute: string]: AttrValue }; -type ElementFn = (attributes?: Attributes | Child | Child[], children?: Child | Child[]) => Element; +type ElementFn = (attributes?: Attributes | Children, children?: Children) => Element; export type Builder = TemplateBuilder & { [tagName in typeof TAG_NAMES[string][number]]: ElementFn }; /** @@ -195,15 +198,15 @@ export class TemplateBuilder { this._addAttributeBinding(node, "className", value => classNames(obj, value)); } - _addTextBinding(fn: (value: T) => string): Text { - const initialValue = fn(this._value); + _addTextBinding(fn: (value: T) => ReturnType>): Text { + const initialValue = fn(this._value)+""; const node = text(initialValue); let prevValue = initialValue; const binding = () => { - const newValue = fn(this._value); + const newValue = fn(this._value)+""; if (prevValue !== newValue) { prevValue = newValue; - node.textContent = newValue+""; + node.textContent = newValue; } }; @@ -242,7 +245,7 @@ export class TemplateBuilder { } } - _setNodeChildren(node: Element, children: Child | Child[]): void{ + _setNodeChildren(node: Element, children: Children): void{ if (!Array.isArray(children)) { children = [children]; } @@ -276,14 +279,18 @@ export class TemplateBuilder { return node; } - el(name: string, attributes?: Attributes | Child | Child[], children?: Child | Child[]): ViewNode { + el(name: string, attributes?: Attributes | Children, children?: Children): ViewNode { return this.elNS(HTML_NS, name, attributes, children); } - elNS(ns: string, name: string, attributes?: Attributes | Child | Child[], children?: Child | Child[]): ViewNode { - if (attributes !== undefined && isChildren(attributes)) { - children = attributes; - attributes = undefined; + elNS(ns: string, name: string, attributesOrChildren?: Attributes | Children, children?: Children): ViewNode { + let attributes: Attributes | undefined; + if (attributesOrChildren) { + if (isChildren(attributesOrChildren)) { + children = attributesOrChildren as Children; + } else { + attributes = attributesOrChildren as Attributes; + } } const node = document.createElementNS(ns, name); diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index b429f2af..44f7476a 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -104,8 +104,9 @@ export const TAG_NAMES = { "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", - "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", "progress", "output", "video"], - [SVG_NS]: ["svg", "circle", "path"] + "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", + "progress", "output", "video"], + [SVG_NS]: ["svg", "g", "path", "circle", "ellipse", "rect", "use"] } as const; export const tag: { [tagName in typeof TAG_NAMES[string][number]]: (attributes?: BasicAttributes | Child | Child[], children?: Child | Child[]) => Element } = {} as any; 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/InviteView.js b/src/platform/web/ui/session/room/InviteView.js index 9d808abf..99345360 100644 --- a/src/platform/web/ui/session/room/InviteView.js +++ b/src/platform/web/ui/session/room/InviteView.js @@ -16,7 +16,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; -import {renderStaticAvatar} from "../../avatar.js"; +import {renderStaticAvatar} from "../../avatar"; export class InviteView extends TemplateView { render(t, vm) { diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index 9c67fa9f..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 {viewClassForEntry} 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 && viewClassForEntry(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 55fbd4f4..5d342a95 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -26,15 +26,16 @@ import {RoomArchivedView} from "./RoomArchivedView.js"; import {AvatarView} from "../../AvatarView.js"; export class RoomView extends TemplateView { - constructor(options) { - super(options); + constructor(vm, viewClassForTile) { + super(vm); + this._viewClassForTile = viewClassForTile; this._optionsPopup = null; } 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); } @@ -55,7 +56,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, this._viewClassForTile) : new TimelineLoadingView(vm); // vm is just needed for i18n }), bottomView ? t.view(bottomView) : text(''), diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 936b8c7c..5a04991f 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,17 @@ 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; + onClick(event: UIEvent); +} +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"; export interface TimelineViewModel extends IObservableValue { showJumpDown: boolean; @@ -55,13 +65,17 @@ export class TimelineView extends TemplateView { private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; + constructor(vm: TimelineViewModel, private readonly viewClassForTile: 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.viewClassForTile); const root = t.div({className: "Timeline"}, [ t.div({ className: "Timeline_scroller bottom-aligned-scroll", @@ -174,16 +188,13 @@ class TilesListView extends ListView { private onChanged: () => void; - constructor(tiles: ObservableList, onChanged: () => void) { - const options = { + constructor(tiles: ObservableList, onChanged: () => void, private readonly viewClassForTile: ViewClassForEntryFn) { + super({ list: tiles, onItemClick: (tileView, evt) => tileView.onClick(evt), - }; - super(options, entry => { - const View = viewClassForEntry(entry); - if (View) { - return new View(entry); - } + }, tile => { + const TileView = viewClassForTile(tile); + return new TileView(tile, viewClassForTile); }); this.onChanged = onChanged; } @@ -195,7 +206,7 @@ class TilesListView extends ListView { onUpdate(index: number, value: SimpleTile, param: any) { if (param === "shape") { - const ExpectedClass = 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 5048211a..7b62630f 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 viewClassForTile(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 tileClassForEntry function in the view model`); } } 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/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 083e0e5e..74b96ecf 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {renderStaticAvatar} from "../../../avatar.js"; +import {renderStaticAvatar} from "../../../avatar"; import {tag} from "../../../general/html"; import {mountView} from "../../../general/utils"; import {TemplateView} from "../../../general/TemplateView"; @@ -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/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, diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index 3c52fc71..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 {viewClassForEntry} from "../common"; export class ReplyPreviewView extends TemplateView { + constructor(vm, viewClassForTile) { + super(vm); + this._viewClassForTile = viewClassForTile; + } render(t, vm) { - const viewClass = viewClassForEntry(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 a83307dd..c66a9d80 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -37,7 +37,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; diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index 93e44307..dd7bbc03 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -93,10 +93,13 @@ export class SettingsView extends TemplateView { ]); }) ); - + settingNodes.push( t.h3("Preferences"), row(t, vm.i18n`Scale down images when sending`, this._imageCompressionRange(t, vm)), + t.if(vm => !import.meta.env.DEV && vm.activeTheme, (t, vm) => { + return row(t, vm.i18n`Use the following theme`, this._themeOptions(t, vm)); + }), ); settingNodes.push( t.h3("Application"), @@ -135,4 +138,13 @@ export class SettingsView extends TemplateView { vm.i18n`no resizing`; })]; } + + _themeOptions(t, vm) { + const activeTheme = vm.activeTheme; + const optionTags = []; + for (const name of vm.themes) { + optionTags.push(t.option({value: name, selected: name === activeTheme}, name)); + } + return t.select({onChange: (e) => vm.setTheme(e.target.value)}, optionTags); + } } diff --git a/src/utils/Disposables.ts b/src/utils/Disposables.ts index 19a5983c..f7c7eb53 100644 --- a/src/utils/Disposables.ts +++ b/src/utils/Disposables.ts @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +19,7 @@ export interface IDisposable { dispose(): void; } -type Disposable = IDisposable | (() => void); +export type Disposable = IDisposable | (() => void); function disposeValue(value: Disposable): void { if (typeof value === "function") { @@ -33,9 +34,9 @@ function isDisposable(value: Disposable): boolean { } export class Disposables { - private _disposables: Disposable[] | null = []; + private _disposables?: Disposable[] = []; - track(disposable: Disposable): Disposable { + track(disposable: D): D { if (!isDisposable(disposable)) { throw new Error("Not a disposable"); } @@ -48,16 +49,16 @@ export class Disposables { return disposable; } - untrack(disposable: Disposable): null { + untrack(disposable: Disposable): undefined { if (this.isDisposed) { console.warn("Disposables already disposed, cannot untrack"); - return null; + return undefined; } const idx = this._disposables!.indexOf(disposable); if (idx >= 0) { this._disposables!.splice(idx, 1); } - return null; + return undefined; } dispose(): void { @@ -65,17 +66,17 @@ export class Disposables { for (const d of this._disposables) { disposeValue(d); } - this._disposables = null; + this._disposables = undefined; } } get isDisposed(): boolean { - return this._disposables === null; + return this._disposables === undefined; } - disposeTracked(value: Disposable): null { + disposeTracked(value: Disposable | undefined): undefined { if (value === undefined || value === null || this.isDisposed) { - return null; + return undefined; } const idx = this._disposables!.indexOf(value); if (idx !== -1) { @@ -84,6 +85,6 @@ export class Disposables { } else { console.warn("disposable not found, did it leak?", value); } - return null; + return undefined; } } diff --git a/src/utils/Lock.ts b/src/utils/Lock.ts index 238d88f9..ff623eba 100644 --- a/src/utils/Lock.ts +++ b/src/utils/Lock.ts @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export class Lock { +export interface ILock { + release(): void; +} + +export class Lock implements ILock { private _promise?: Promise; private _resolve?: (() => void); @@ -52,7 +56,7 @@ export class Lock { } } -export class MultiLock { +export class MultiLock implements ILock { constructor(public readonly locks: Lock[]) { } diff --git a/vite.common-config.js b/vite.common-config.js index 44c884bd..8a82a9da 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -1,9 +1,15 @@ const cssvariables = require("postcss-css-variables"); const flexbugsFixes = require("postcss-flexbugs-fixes"); +const compileVariables = require("./scripts/postcss/css-compile-variables"); +const urlVariables = require("./scripts/postcss/css-url-to-variables"); +const urlProcessor = require("./scripts/postcss/css-url-processor"); const fs = require("fs"); const path = require("path"); const manifest = require("./package.json"); const version = manifest.version; +const compiledVariables = new Map(); +const derive = require("./scripts/postcss/color").derive; +const replacer = require("./scripts/postcss/svg-colorizer").buildColorizedSVG; const commonOptions = { logLevel: "warn", @@ -25,6 +31,7 @@ const commonOptions = { assetsInlineLimit: 0, polyfillModulePreload: false, }, + assetsInclude: ['**/config.json'], define: { DEFINE_VERSION: JSON.stringify(version), DEFINE_GLOBAL_HASH: JSON.stringify(null), @@ -32,11 +39,14 @@ const commonOptions = { css: { postcss: { plugins: [ - cssvariables({ - preserve: (declaration) => { - return declaration.value.indexOf("var(--ios-") == 0; - } - }), + compileVariables({derive, compiledVariables}), + urlVariables({compileVariables}), + urlProcessor({replacer}), + // cssvariables({ + // preserve: (declaration) => { + // return declaration.value.indexOf("var(--ios-") == 0; + // } + // }), // the grid option creates some source fragment that causes the vite warning reporter to crash because // it wants to log a warning on a line that does not exist in the source fragment. // autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}), @@ -46,4 +56,4 @@ const commonOptions = { } }; -module.exports = commonOptions; +module.exports = { commonOptions, compiledVariables }; diff --git a/vite.config.js b/vite.config.js index b6ec597d..72be0184 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,8 +1,9 @@ const injectWebManifest = require("./scripts/build-plugins/manifest"); const {injectServiceWorker, createPlaceholderValues} = require("./scripts/build-plugins/service-worker"); +const themeBuilder = require("./scripts/build-plugins/rollup-plugin-build-themes"); const {defineConfig} = require('vite'); const mergeOptions = require('merge-options').bind({concatArrays: true}); -const commonOptions = require("./vite.common-config.js"); +const {commonOptions, compiledVariables} = require("./vite.common-config.js"); export default defineConfig(({mode}) => { const definePlaceholders = createPlaceholderValues(mode); @@ -13,17 +14,56 @@ export default defineConfig(({mode}) => { outDir: "../../../target", minify: true, sourcemap: true, + rollupOptions: { + output: { + assetFileNames: (asset) => { + if (asset.name.includes("config.json")) { + return "[name][extname]"; + } + else if (asset.name.match(/theme-.+\.json/)) { + return "assets/[name][extname]"; + } + else { + return "assets/[name].[hash][extname]"; + } + } + }, + }, }, plugins: [ + themeBuilder({ + themeConfig: { + themes: { + element: "./src/platform/web/ui/css/themes/element", + }, + default: "element", + }, + compiledVariables, + }), // important this comes before service worker // otherwise the manifest and the icons it refers to won't be cached injectWebManifest("assets/manifest.json"), - injectServiceWorker("./src/platform/web/sw.js", ["index.html"], { + injectServiceWorker("./src/platform/web/sw.js", findUnhashedFileNamesFromBundle, { // placeholders to replace at end of build by chunk name - "index": {DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH}, - "sw": definePlaceholders + index: { + DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH, + }, + sw: definePlaceholders, }), ], define: definePlaceholders, }); }); + +function findUnhashedFileNamesFromBundle(bundle) { + const names = ["index.html"]; + for (const fileName of Object.keys(bundle)) { + if (fileName.includes("config.json")) { + names.push(fileName); + } + if (/theme-.+\.json/.test(fileName)) { + names.push(fileName); + } + } + return names; +} diff --git a/vite.sdk-assets-config.js b/vite.sdk-assets-config.js index 90720966..beb7bb37 100644 --- a/vite.sdk-assets-config.js +++ b/vite.sdk-assets-config.js @@ -1,11 +1,43 @@ const path = require("path"); const mergeOptions = require('merge-options'); -const commonOptions = require("./vite.common-config.js"); +const themeBuilder = require("./scripts/build-plugins/rollup-plugin-build-themes"); +const {commonOptions, compiledVariables} = require("./vite.common-config.js"); + +// These paths will be saved without their hash so they havea consisent path to +// reference +const pathsToExport = [ + "main.js", + "download-sandbox.html", + "theme-element-light.css", + "theme-element-dark.css", +]; export default mergeOptions(commonOptions, { root: "src/", base: "./", build: { outDir: "../target/asset-build/", + rollupOptions: { + output: { + assetFileNames: (chunkInfo) => { + // Get rid of the hash so we can consistently reference these + // files in our `package.json` `exports` + if(pathsToExport.includes(path.basename(chunkInfo.name))) { + return "assets/[name].[ext]"; + } + + return "assets/[name]-[hash][extname]"; + } + } + } }, + plugins: [ + themeBuilder({ + themeConfig: { + themes: { element: "./src/platform/web/ui/css/themes/element" }, + default: "element", + }, + compiledVariables, + }), + ], }); diff --git a/vite.sdk-lib-config.js b/vite.sdk-lib-config.js index c1678d8b..a5f11a53 100644 --- a/vite.sdk-lib-config.js +++ b/vite.sdk-lib-config.js @@ -1,6 +1,6 @@ const path = require("path"); const mergeOptions = require('merge-options'); -const commonOptions = require("./vite.common-config.js"); +const {commonOptions} = require("./vite.common-config.js"); const manifest = require("./package.json"); const externalDependencies = Object.keys(manifest.dependencies) diff --git a/yarn.lock b/yarn.lock index 87b8ef96..9d882bd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -498,66 +498,141 @@ entities@^2.0.0: version "4.2.8" resolved "https://github.com/bwindels/es6-promise.git#112f78f5829e627055b0ff56a52fecb63f6003b1" +esbuild-android-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.31.tgz#4b7dfbbeee62b3495ba78220b85fb590eb68d5bf" + integrity sha512-MYkuJ91w07nGmr4EouejOZK2j/f5TCnsKxY8vRr2+wpKKfHD1LTJK28VbZa+y1+AL7v1V9G98ezTUwsV3CmXNw== + esbuild-android-arm64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44" integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg== +esbuild-android-arm64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.31.tgz#24c3d693924e044fb0d23206c3e627502b10b930" + integrity sha512-0rkH/35s7ZVcsw6nS0IAkR0dekSbjZGWdlOAf3jV0lGoPqqw0x6/TmaV9w7DQgUERTH1ApmPlpAMU4kVkCq9Jg== + esbuild-darwin-64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72" integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ== +esbuild-darwin-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.31.tgz#285fbdb6dc74d4410f43dee59e6a14ebff82a9d7" + integrity sha512-kP6xPZHxtJa36Hb0jC05L3VzQSZBW2f3bpnQS20czXTRGEmM2GDiYpGdI5g2QYaw6vC4PYXjnigq8usd9g9jnQ== + esbuild-darwin-arm64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a" integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ== +esbuild-darwin-arm64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.31.tgz#b39c471a8134ce2c7811eb96fab9c500b256261c" + integrity sha512-1ZMog4hkNsdBGtDDtsftUqX6S9N52gEx4vX5aVehsSptgoBFIar1XrPiBTQty7YNH+bJasTpSVaZQgElCVvPKQ== + esbuild-freebsd-64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85" integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA== +esbuild-freebsd-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.31.tgz#7ca700ef60ae12154bae63094ad41b21c6ae1a23" + integrity sha512-Zo0BYj7QpVFWoUpkv6Ng0RO2eJ4zk/WDaHMO88+jr5HuYmxsOre0imgwaZVPquTuJnCvL1G48BFucJ3tFflSeQ== + esbuild-freebsd-arm64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52" integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ== +esbuild-freebsd-arm64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.31.tgz#f793085c7184fcd08649b8d185edc5c2ce112e82" + integrity sha512-t85bS6jbRpmdjr4pdr/FY/fpx8lo1vv9S7BAs2EsXKJQhRDMIiC3QW+k2acYJoRuqirlvJcJVFQGCq/PfyC1kA== + esbuild-linux-32@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69" integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g== +esbuild-linux-32@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.31.tgz#cac97ec7da6fbde0c21dbe08babd0d2a034f317d" + integrity sha512-XYtOk/GodSkv+UOYVwryGpGPuFnszsMvRMKq6cIUfFfdssHuKDsU9IZveyCG44J106J39ABenQ5EetbYtVJHUw== + esbuild-linux-64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3" integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA== +esbuild-linux-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.31.tgz#ec94cd5228e6777d2feb3c24a1fe1cbf8817d6da" + integrity sha512-Zf9CZxAxaXWHLqCg/QZ/hs0RU0XV3IBxV+ENQzy00S4QOTnZAvSLgPciILHHrVJ0lPIlb4XzAqlLM5y6iI2LIw== + esbuild-linux-arm64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1" integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA== +esbuild-linux-arm64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.31.tgz#d119188fccd6384db5c703de24c46dacaee3e9e8" + integrity sha512-V/H0tv+xpQ9IOHM+o85oCKNNidIEc5CcnDWl0V+hPd2F03dqdbFkWPBGphx8rD4JSQn6UefUQ1iH7y1qIzO8Fw== + esbuild-linux-arm@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe" integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA== +esbuild-linux-arm@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.31.tgz#63e10846886901e5632a591d44160f95c5d12ba7" + integrity sha512-RpiaeHPRlgCCDskxoyIsI49BhcDtZ4cl8+SLffizDm0yMNWP538SUg0ezQ2TTOPj3/svaGIbkRDwYtAon0Sjkg== + esbuild-linux-mips64le@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7" integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg== +esbuild-linux-mips64le@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.31.tgz#1cd44f72cde6489a5d6deea7c54efa6f3d6590ee" + integrity sha512-9/oBfAckInRuUg6AEgdCLLn6KJ6UOJDOLmUinTsReVSg6AfV6wxYQJq9iQM2idRogP7GUpomJ+bvCdWXpotQRQ== + esbuild-linux-ppc64le@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2" integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ== +esbuild-linux-ppc64le@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.31.tgz#3b5ccc05e5b8ef5c494f30a61fdd27811d2bbeeb" + integrity sha512-NMcb14Pg+8q8raGkzor9/R3vQwKzgxE3694BtO2SDLBwJuL2C1dQ1ZtM1t7ZvArQBgT8RiZVxb0/3fD+qGNk7g== + +esbuild-linux-riscv64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.31.tgz#d74ca78c8ed1d9b40bc907a9e3ef6e83fc06189c" + integrity sha512-l13yvmsVfawAnoYfcpuvml+nTlrOmtdceXYufSkXl2DOb0JKcuR6ARlAzuQCDcpo49SOJy1cCxpwlOIsUQBfzA== + +esbuild-linux-s390x@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.31.tgz#1bd547b8b027e323b77a838d265cb56ece2543af" + integrity sha512-GIwV9mY3koYja9MCSkKLk1P7rj+MkPV0UsGsZ575hEcIBrXeKN9jBi6X/bxDDPEN/SUAH35cJhBNrZU4x9lEfg== + esbuild-netbsd-64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038" integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w== +esbuild-netbsd-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.31.tgz#964a45dbad4fac92aa0a15056e38a182735bd6c6" + integrity sha512-bJ+pyLvKQm+Obp5k7/Wk8e9Gdkls56F1aiI3uptoIfOIUqsZImH7pDyTrSufwqsFp62kO9LRuwXnjDwQtPyhFQ== + esbuild-node-loader@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/esbuild-node-loader/-/esbuild-node-loader-0.6.3.tgz#3b90012f8bc2fcbb2ef76a659482c2c99840c5e8" @@ -570,27 +645,52 @@ esbuild-openbsd-64@0.13.15: resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7" integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g== +esbuild-openbsd-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.31.tgz#7d2a1d46450321b0459263d3e7072e6d3924ce46" + integrity sha512-NRAAPPca05H9j9Xab0kVXK0V6/pyZGGy8d2Y8KS0BMwWEydlD4KCJDmH8/7bWCKYLRGOOCE9/GPBJyPWHFW3sg== + esbuild-sunos-64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4" integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw== +esbuild-sunos-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.31.tgz#3b3e4363951cd1dda14a14fee6d94ca426108e0c" + integrity sha512-9uA+V8w9Eehu4ldb95lPWdgCMcMO5HH6pXmfkk5usn3JsSZxKdLKsXB4hYgP80wscZvVYXJl2G+KNxsUTfPhZw== + esbuild-windows-32@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7" integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw== +esbuild-windows-32@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.31.tgz#997026a41c04535bfb7c014a0458940b49145820" + integrity sha512-VGdncQTqoxD9q3v/dk0Yugbmx2FzOkcs0OemBYc1X9KXOLQYH0uQbLJIckZdZOC3J+JKSExbYFrzYCOwWPuNyA== + esbuild-windows-64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294" integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ== +esbuild-windows-64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.31.tgz#5d4b0ea686c9b60036303b3415c472f2761bdafc" + integrity sha512-v/2ye5zBqpmCzi3bLCagStbNQlnOsY7WtMrD2Q0xZxeSIXONxji15KYtVee5o7nw4lXWbQSS1BL8G6BBMvtq4A== + esbuild-windows-arm64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3" integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA== -esbuild@^0.13.12, esbuild@^0.13.2: +esbuild-windows-arm64@0.14.31: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.31.tgz#4f3b9fc34c4a33abbd0171df6cbb657ccbdbfc67" + integrity sha512-RXeU42FJoG1sriNHg73h4S+5B7L/gw+8T7U9u8IWqSSEbY6fZvBh4uofugiU1szUDqqP00GHwZ09WgYe3lGZiw== + +esbuild@^0.13.12: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf" integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw== @@ -613,6 +713,32 @@ esbuild@^0.13.12, esbuild@^0.13.2: esbuild-windows-64 "0.13.15" esbuild-windows-arm64 "0.13.15" +esbuild@^0.14.27: + version "0.14.31" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.31.tgz#f7d0db114edc615f66d84972ee9fbd2b267f4029" + integrity sha512-QA0fUM13+JZzcvg1bdrhi7wo8Lr5IRHA9ypNn2znqxGqb66dSK6pAh01TjyBOhzZGazPQJZ1K26VrCAQJ715qA== + optionalDependencies: + esbuild-android-64 "0.14.31" + esbuild-android-arm64 "0.14.31" + esbuild-darwin-64 "0.14.31" + esbuild-darwin-arm64 "0.14.31" + esbuild-freebsd-64 "0.14.31" + esbuild-freebsd-arm64 "0.14.31" + esbuild-linux-32 "0.14.31" + esbuild-linux-64 "0.14.31" + esbuild-linux-arm "0.14.31" + esbuild-linux-arm64 "0.14.31" + esbuild-linux-mips64le "0.14.31" + esbuild-linux-ppc64le "0.14.31" + esbuild-linux-riscv64 "0.14.31" + esbuild-linux-s390x "0.14.31" + esbuild-netbsd-64 "0.14.31" + esbuild-openbsd-64 "0.14.31" + esbuild-sunos-64 "0.14.31" + esbuild-windows-32 "0.14.31" + esbuild-windows-64 "0.14.31" + esbuild-windows-arm64 "0.14.31" + escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -961,10 +1087,10 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-core-module@^2.2.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" - integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg== +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== dependencies: has "^1.0.3" @@ -1108,10 +1234,10 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nanoid@^3.1.28: - version "3.1.28" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.28.tgz#3c01bac14cb6c5680569014cc65a2f26424c6bd4" - integrity sha512-gSu9VZ2HtmoKYe/lmyPFES5nknFrHa+/DT9muUFWFMi6Jh9E1I7bkvlQ8xxf1Kos9pi9o8lBnIOkatMhKX/YUw== +nanoid@^3.3.3: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== natural-compare@^1.4.0: version "1.4.0" @@ -1133,6 +1259,13 @@ nth-check@^2.0.0: dependencies: boolbase "^1.0.0" +off-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/off-color/-/off-color-2.0.0.tgz#ecf3bda52e9a78dde535db86361e048741a56631" + integrity sha512-JJ9ObbY2CzgT7F8PpdpHGNjQa7QbU8f4DkY3cCxYUq9NezYUMmL/oSofCc5MMaiUnNNBEFCc4w1unMA+R8syvw== + dependencies: + core-js "^3.6.5" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1181,20 +1314,20 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.2.3: version "2.3.0" @@ -1215,14 +1348,19 @@ postcss-flexbugs-fixes@^5.0.2: resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== -postcss@^8.3.8: - version "8.3.9" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.9.tgz#98754caa06c4ee9eb59cc48bd073bb6bd3437c31" - integrity sha512-f/ZFyAKh9Dnqytx5X62jgjhhzttjZS7hMsohcI7HEI5tjELX/HxCy3EFhsRxyzGvrzFF+82XPvCS8T9TFleVJw== +postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.13: + version "8.4.13" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575" + integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA== dependencies: - nanoid "^3.1.28" - picocolors "^0.2.1" - source-map-js "^0.6.2" + nanoid "^3.3.3" + picocolors "^1.0.0" + source-map-js "^1.0.2" prelude-ls@^1.2.1: version "1.2.1" @@ -1279,13 +1417,14 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== +resolve@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" reusify@^1.0.4: version "1.0.4" @@ -1299,10 +1438,10 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup@^2.57.0: - version "2.58.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.58.0.tgz#a643983365e7bf7f5b7c62a8331b983b7c4c67fb" - integrity sha512-NOXpusKnaRpbS7ZVSzcEXqxcLDOagN6iFS8p45RkoiMqPHDLwJm758UF05KlMoCRbLBTZsPOIa887gZJ1AiXvw== +rollup@^2.59.0: + version "2.70.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.1.tgz#824b1f1f879ea396db30b0fc3ae8d2fead93523e" + integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== optionalDependencies: fsevents "~2.3.2" @@ -1356,10 +1495,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -source-map-js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" - integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== source-map@~0.6.1: version "0.6.1" @@ -1406,6 +1545,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + table@^6.0.9: version "6.7.1" resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" @@ -1509,15 +1653,15 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -vite@^2.6.14: - version "2.6.14" - resolved "https://registry.yarnpkg.com/vite/-/vite-2.6.14.tgz#35c09a15e4df823410819a2a239ab11efb186271" - integrity sha512-2HA9xGyi+EhY2MXo0+A2dRsqsAG3eFNEVIo12olkWhOmc8LfiM+eMdrXf+Ruje9gdXgvSqjLI9freec1RUM5EA== +vite@^2.9.8: + version "2.9.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.8.tgz#2c2cb0790beb0fbe4b8c0995b80fe691a91c2545" + integrity sha512-zsBGwn5UT3YS0NLSJ7hnR54+vUKfgzMUh/Z9CxF1YKEBVIe213+63jrFLmZphgGI5zXwQCSmqIdbPuE8NJywPw== dependencies: - esbuild "^0.13.2" - postcss "^8.3.8" - resolve "^1.20.0" - rollup "^2.57.0" + esbuild "^0.14.27" + postcss "^8.4.13" + resolve "^1.22.0" + rollup "^2.59.0" optionalDependencies: fsevents "~2.3.2"