diff --git a/.eslintrc.js b/.eslintrc.js
index 521ea791..24bbb049 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -5,10 +5,10 @@ module.exports = {
},
"extends": "eslint:recommended",
"parserOptions": {
- "ecmaVersion": 2018,
+ "ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
"no-console": "off"
}
-};
\ No newline at end of file
+};
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..f433b1a5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/README.md b/README.md
index 51b110d2..aabdf89a 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,25 @@
-# Brawl
+# Hydrogen
-A minimal [Matrix](https://matrix.org/) chat client, focused on performance, offline functionality and working on my Lumia 950 Windows Phone.
+A minimal [Matrix](https://matrix.org/) chat client, focused on performance, offline functionality, and broad browser support. This is work in progress and not yet ready for primetime. We're currently not accepting any externally reported issues (features, bug reports, ...) at this time.
+
+## Goals
+
+Hydrogen's goals are:
+ - Work well on desktop as well as mobile browsers
+ - UI components can be easily used in isolation
+ - 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
## Status
-Brawl can currently log you in, or pick an existing session, sync already joined rooms, fill gaps in the timeline, and send text messages. Everything is stored locally.
-
-Here's an (outdated) GIF of what that looks like, also see link below to try it out:
-![Showing multiple sessions, and sending messages](https://bwindels.github.io/brawl-chat/images/brawl-sending.gif)
+Hydrogen can currently log you in, or pick an existing session, sync already joined rooms, fill gaps in the timeline, and send text messages. Everything is stored locally.
## Why
-I started writing Brawl both to have a functional matrix client on my aging phone, and to play around with some ideas I had how to use indexeddb optimally in a matrix client.
-
-For every interaction or network response (syncing, filling a gap), Brawl starts a transaction in indexedb, and only commits it once everything went well. This helps to keep your storage always in a consistent state. As little data is kept in memory as well, and while scrolling in the above GIF, everything is loaded straight from the storage.
+For every interaction or network response (syncing, filling a gap), Hydrogen starts a transaction in indexedb, and only commits it once everything went well. This helps to keep your storage always in a consistent state. As little data is kept in memory as well, and while scrolling in the above GIF, everything is loaded straight from the storage.
If you find this interesting, feel free to reach me at `@bwindels:matrix.org`.
# How to use
-You can [try Brawl here](https://bwindels.github.io/brawl/), or try it locally by running `yarn install` (only the first time) and `yarn start` in the terminal, and point your browser to `http://localhost:3000`.
+Try it locally by running `npm install dev` (only the first time) and `npm start` in the terminal, and point your browser to `http://localhost:3000`.
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 00000000..1a693eb2
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,13 @@
+ - make it a copy, not a fork of brawl, so we can have issues
+ - add compilation step for ie11 compatible bundle
+ - compile to es5
+ - use bluebird for promises
+ - make xhr request impl
+ - once app is loading, go over errors
+
+
+ - project goals
+ - works on mobile
+ - works well offline
+ - components can be used in isolation
+ - lazyload components?
\ No newline at end of file
diff --git a/doc/impl-thoughts/RELATIONS.md b/doc/impl-thoughts/RELATIONS.md
index 1098183c..264b55e1 100644
--- a/doc/impl-thoughts/RELATIONS.md
+++ b/doc/impl-thoughts/RELATIONS.md
@@ -16,3 +16,7 @@ As a UI for reactions, we could show (👍 14 + 1) where the + 1 is our own loca
wrt to how to store relations in indexeddb, we could store all local ids of related events (per type?) on the related-to event, so we can fetch them in one query for *all* events that have related events that were fetched in a range, without needing another index that would slow down writes. So that would only add 1 query which we only need to do when there are relations in the TimelineReader. what do we do though if we receive the relating event before the related-to event? An index would fix this mostly ... or we need a temp store where we store unresolved relations...
+
+Replies should definitely use this relation mechanism, so we can easily show the most up to date version of the replied-to event.
+
+Redactions can de done separately
diff --git a/index.html b/index.html
index ae62da91..cdf0ad4f 100644
--- a/index.html
+++ b/index.html
@@ -3,49 +3,28 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Velit dignissim sodales ut eu sem integer vitae justo eget. Libero justo laoreet sit amet cursus sit amet dictum. Egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate. Quis eleifend quam adipiscing vitae proin sagittis nisl. Egestas maecenas pharetra convallis posuere morbi leo. Metus dictum at tempor commodo ullamcorper a lacus. Odio pellentesque diam volutpat commodo sed egestas egestas. Elementum eu facilisis sed odio morbi quis commodo odio aenean. Velit euismod in pellentesque massa placerat duis ultricies lacus sed. Feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi. Pulvinar etiam non quam lacus suspendisse. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Proin gravida hendrerit lectus a. Nibh sed pulvinar proin gravida. Massa placerat duis ultricies lacus. Enim sed faucibus turpis in eu mi bibendum neque egestas. Turpis egestas sed tempus urna et pharetra pharetra.
+
+
diff --git a/prototypes/ie11.css b/prototypes/ie11.css
new file mode 100644
index 00000000..d3649477
--- /dev/null
+++ b/prototypes/ie11.css
@@ -0,0 +1,3 @@
+p {
+ color: red;
+}
\ No newline at end of file
diff --git a/prototypes/non-ie11.css b/prototypes/non-ie11.css
new file mode 100644
index 00000000..25190f61
--- /dev/null
+++ b/prototypes/non-ie11.css
@@ -0,0 +1,3 @@
+p {
+ color: green;
+}
diff --git a/scripts/build.mjs b/scripts/build.mjs
index 528f4d7c..892e27f0 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -1,49 +1,162 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import cheerio from "cheerio";
import fsRoot from "fs";
const fs = fsRoot.promises;
import path from "path";
-import rollup from 'rollup';
+import XXHash from 'xxhash';
+import { rollup } from 'rollup';
import postcss from "postcss";
import postcssImport from "postcss-import";
import { fileURLToPath } from 'url';
import { dirname } from 'path';
+import commander from "commander";
+// needed for legacy bundle
+import babel from '@rollup/plugin-babel';
+// needed to find the polyfill modules in the main-legacy.js bundle
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+// needed because some of the polyfills are written as commonjs modules
+import commonjs from '@rollup/plugin-commonjs';
+// multi-entry plugin so we can add polyfill file to main
+import multi from '@rollup/plugin-multi-entry';
+import removeJsComments from 'rollup-plugin-cleanup';
+// replace urls of asset names with content hashed version
+import postcssUrl from "postcss-url";
+import cssvariables from "postcss-css-variables";
+import flexbugsFixes from "postcss-flexbugs-fixes";
+
+const PROJECT_ID = "hydrogen";
+const PROJECT_SHORT_NAME = "Hydrogen";
+const PROJECT_NAME = "Hydrogen Chat";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../");
-const targetDir = path.join(projectDir, "target");
+const cssSrcDir = path.join(projectDir, "src/ui/web/css/");
+const targetDir = path.join(projectDir, "target/");
-const debug = false;
-const offline = true;
+const program = new commander.Command();
+program
+ .option("--no-offline", "make a build without a service worker or appcache manifest")
+program.parse(process.argv);
+const {debug, noOffline} = program;
+const offline = !noOffline;
async function build() {
+ // only used for CSS for now, using legacy for all targets for now
+ const legacy = true;
// get version number
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
- // clear target dir
- await removeDirIfExists(targetDir);
- await fs.mkdir(targetDir);
-
- await buildHtml(version);
- await buildJs();
- await buildCss();
- if (offline) {
- await buildOffline(version);
- }
- console.log(`built brawl ${version} successfully`);
-}
-
-async function buildHtml(version) {
- // transform html file
const devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8");
const doc = cheerio.load(devHtml);
- doc("link[rel=stylesheet]").attr("href", "brawl.css");
+ const themes = [];
+ findThemes(doc, themeName => {
+ themes.push(themeName);
+ });
+ // clear target dir
+ await removeDirIfExists(targetDir);
+ await createDirs(targetDir, themes);
+ // also creates the directories where the theme css bundles are placed in,
+ // so do it first
+ const themeAssets = await copyThemeAssets(themes, legacy);
+ const jsBundlePath = await buildJs();
+ const jsLegacyBundlePath = await buildJsLegacy();
+ const cssBundlePaths = await buildCssBundles(legacy ? buildCssLegacy : buildCss, themes, themeAssets);
+ const assetPaths = createAssetPaths(jsBundlePath, jsLegacyBundlePath, cssBundlePaths, themeAssets);
+
+ let manifestPath;
+ if (offline) {
+ manifestPath = await buildOffline(version, assetPaths);
+ }
+ await buildHtml(doc, version, assetPaths, manifestPath);
+
+ console.log(`built ${PROJECT_ID} ${version} successfully`);
+}
+
+function createAssetPaths(jsBundlePath, jsLegacyBundlePath, cssBundlePaths, themeAssets) {
+ function trim(path) {
+ if (!path.startsWith(targetDir)) {
+ throw new Error("invalid target path: " + targetDir);
+ }
+ return path.substr(targetDir.length);
+ }
+ return {
+ jsBundle: () => trim(jsBundlePath),
+ jsLegacyBundle: () => trim(jsLegacyBundlePath),
+ cssMainBundle: () => trim(cssBundlePaths.main),
+ cssThemeBundle: themeName => trim(cssBundlePaths.themes[themeName]),
+ cssThemeBundles: () => Object.values(cssBundlePaths.themes).map(a => trim(a)),
+ otherAssets: () => Object.values(themeAssets).map(a => trim(a))
+ };
+}
+
+async function findThemes(doc, callback) {
+ doc("link[rel~=stylesheet][title]").each((i, el) => {
+ const theme = doc(el);
+ const href = theme.attr("href");
+ const themesPrefix = "/themes/";
+ const prefixIdx = href.indexOf(themesPrefix);
+ if (prefixIdx !== -1) {
+ const themeNameStart = prefixIdx + themesPrefix.length;
+ const themeNameEnd = href.indexOf("/", themeNameStart);
+ const themeName = href.substr(themeNameStart, themeNameEnd - themeNameStart);
+ callback(themeName, theme);
+ }
+ });
+}
+
+async function createDirs(targetDir, themes) {
+ await fs.mkdir(targetDir);
+ const themeDir = path.join(targetDir, "themes");
+ await fs.mkdir(themeDir);
+ for (const theme of themes) {
+ await fs.mkdir(path.join(themeDir, theme));
+ }
+}
+
+async function copyThemeAssets(themes, legacy) {
+ const assets = {};
+ for (const theme of themes) {
+ const themeDstFolder = path.join(targetDir, `themes/${theme}`);
+ const themeSrcFolder = path.join(cssSrcDir, `themes/${theme}`);
+ const themeAssets = await copyFolder(themeSrcFolder, themeDstFolder, file => {
+ const isUnneededFont = legacy ? file.endsWith(".woff2") : file.endsWith(".woff");
+ return !file.endsWith(".css") && !isUnneededFont;
+ });
+ Object.assign(assets, themeAssets);
+ }
+ return assets;
+}
+
+async function buildHtml(doc, version, assetPaths, manifestPath) {
+ // transform html file
+ // change path to main.css to css bundle
+ doc("link[rel=stylesheet]:not([title])").attr("href", assetPaths.cssMainBundle());
+ // change paths to all theme stylesheets
+ findThemes(doc, (themeName, theme) => {
+ theme.attr("href", assetPaths.cssThemeBundle(themeName));
+ });
doc("script#main").replaceWith(
- `` +
- ``);
- removeOrEnableScript(doc("script#phone-debug-pre"), debug);
- removeOrEnableScript(doc("script#phone-debug-post"), debug);
+ `` +
+ `` +
+ ``);
removeOrEnableScript(doc("script#service-worker"), offline);
const versionScript = doc("script#version");
@@ -54,68 +167,149 @@ async function buildHtml(version) {
if (offline) {
doc("html").attr("manifest", "manifest.appcache");
- doc("head").append(``);
+ doc("head").append(``);
}
await fs.writeFile(path.join(targetDir, "index.html"), doc.html(), "utf8");
}
async function buildJs() {
// create js bundle
- const rollupConfig = {
+ const bundle = await rollup({
input: 'src/main.js',
- output: {
- file: path.join(targetDir, "brawl.js"),
- format: 'iife',
- name: 'main'
- }
- };
- const bundle = await rollup.rollup(rollupConfig);
- await bundle.write(rollupConfig);
+ plugins: [removeJsComments({comments: "none"})]
+ });
+ const {output} = await bundle.generate({
+ format: 'es',
+ name: `${PROJECT_ID}Bundle`
+ });
+ const code = output[0].code;
+ const bundlePath = resource(`${PROJECT_ID}.js`, code);
+ await fs.writeFile(bundlePath, code, "utf8");
+ return bundlePath;
}
-async function buildOffline(version) {
+async function buildJsLegacy() {
+ // compile down to whatever IE 11 needs
+ const babelPlugin = babel.babel({
+ babelHelpers: 'bundled',
+ exclude: 'node_modules/**',
+ presets: [
+ [
+ "@babel/preset-env",
+ {
+ useBuiltIns: "entry",
+ corejs: "3",
+ targets: "IE 11"
+ }
+ ]
+ ]
+ });
+ // create js bundle
+ const rollupConfig = {
+ input: ['src/legacy-polyfill.js', 'src/main.js'],
+ plugins: [multi(), commonjs(), nodeResolve(), babelPlugin, removeJsComments({comments: "none"})]
+ };
+ const bundle = await rollup(rollupConfig);
+ const {output} = await bundle.generate({
+ format: 'iife',
+ name: `${PROJECT_ID}Bundle`
+ });
+ const code = output[0].code;
+ const bundlePath = resource(`${PROJECT_ID}-legacy.js`, code);
+ await fs.writeFile(bundlePath, code, "utf8");
+ return bundlePath;
+}
+
+async function buildOffline(version, assetPaths) {
// write offline availability
- const offlineFiles = ["brawl.js", "brawl.css", "index.html", "icon-192.png"];
+ const offlineFiles = [
+ assetPaths.cssMainBundle(),
+ "index.html",
+ "icon-192.png",
+ ].concat(assetPaths.cssThemeBundles());
// write appcache manifest
- const manifestLines = [
+ const appCacheLines = [
`CACHE MANIFEST`,
`# v${version}`,
`NETWORK`,
`"*"`,
`CACHE`,
];
- manifestLines.push(...offlineFiles);
- const manifest = manifestLines.join("\n") + "\n";
- await fs.writeFile(path.join(targetDir, "manifest.appcache"), manifest, "utf8");
+ appCacheLines.push(assetPaths.jsLegacyBundle(), ...offlineFiles);
+ const swOfflineFiles = [assetPaths.jsBundle(), ...offlineFiles];
+ const appCacheManifest = appCacheLines.join("\n") + "\n";
+ await fs.writeFile(path.join(targetDir, "manifest.appcache"), appCacheManifest, "utf8");
// write service worker
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`);
- swSource = swSource.replace(`"%%FILES%%"`, JSON.stringify(offlineFiles));
+ swSource = swSource.replace(`"%%OFFLINE_FILES%%"`, JSON.stringify(swOfflineFiles));
+ swSource = swSource.replace(`"%%CACHE_FILES%%"`, JSON.stringify(assetPaths.otherAssets()));
await fs.writeFile(path.join(targetDir, "sw.js"), swSource, "utf8");
// write web manifest
const webManifest = {
- name: "Brawl Chat",
- short_name: "Brawl",
+ name:PROJECT_NAME,
+ short_name: PROJECT_SHORT_NAME,
display: "fullscreen",
start_url: "index.html",
icons: [{"src": "icon-192.png", "sizes": "192x192", "type": "image/png"}],
};
- await fs.writeFile(path.join(targetDir, "manifest.json"), JSON.stringify(webManifest), "utf8");
+ const manifestJson = JSON.stringify(webManifest);
+ const manifestPath = resource("manifest.json", manifestJson);
+ await fs.writeFile(manifestPath, manifestJson, "utf8");
// copy icon
+ // should this icon have a content hash as well?
let icon = await fs.readFile(path.join(projectDir, "icon.png"));
await fs.writeFile(path.join(targetDir, "icon-192.png"), icon);
+ return manifestPath;
}
-async function buildCss() {
- // create css bundle
- const cssMainFile = path.join(projectDir, "src/ui/web/css/main.css");
- const preCss = await fs.readFile(cssMainFile, "utf8");
- const cssBundler = postcss([postcssImport]);
- const postCss = await cssBundler.process(preCss, {from: cssMainFile});
- await fs.writeFile(path.join(targetDir, "brawl.css"), postCss, "utf8");
+async function buildCssBundles(buildFn, themes, themeAssets) {
+ const bundleCss = await buildFn(path.join(cssSrcDir, "main.css"));
+ const mainDstPath = resource(`${PROJECT_ID}.css`, bundleCss);
+ await fs.writeFile(mainDstPath, bundleCss, "utf8");
+ const bundlePaths = {main: mainDstPath, themes: {}};
+ for (const theme of themes) {
+ const urlBase = path.join(targetDir, `themes/${theme}/`);
+ const assetUrlMapper = ({absolutePath}) => {
+ const hashedDstPath = themeAssets[absolutePath];
+ if (hashedDstPath && hashedDstPath.startsWith(urlBase)) {
+ return hashedDstPath.substr(urlBase.length);
+ }
+ };
+ const themeCss = await buildFn(path.join(cssSrcDir, `themes/${theme}/theme.css`), assetUrlMapper);
+ const themeDstPath = resource(`themes/${theme}/bundle.css`, themeCss);
+ await fs.writeFile(themeDstPath, themeCss, "utf8");
+ bundlePaths.themes[theme] = themeDstPath;
+ }
+ return bundlePaths;
}
+async function buildCss(entryPath, urlMapper = null) {
+ const preCss = await fs.readFile(entryPath, "utf8");
+ const options = [postcssImport];
+ if (urlMapper) {
+ options.push(postcssUrl({url: urlMapper}));
+ }
+ const cssBundler = postcss(options);
+ const result = await cssBundler.process(preCss, {from: entryPath});
+ return result.css;
+}
+
+async function buildCssLegacy(entryPath, urlMapper = null) {
+ const preCss = await fs.readFile(entryPath, "utf8");
+ const options = [
+ postcssImport,
+ cssvariables(),
+ flexbugsFixes()
+ ];
+ if (urlMapper) {
+ options.push(postcssUrl({url: urlMapper}));
+ }
+ const cssBundler = postcss(options);
+ const result = await cssBundler.process(preCss, {from: entryPath});
+ return result.css;
+}
function removeOrEnableScript(scriptNode, enable) {
if (enable) {
@@ -127,9 +321,7 @@ function removeOrEnableScript(scriptNode, enable) {
async function removeDirIfExists(targetDir) {
try {
- const files = await fs.readdir(targetDir);
- await Promise.all(files.map(filename => fs.unlink(path.join(targetDir, filename))));
- await fs.rmdir(targetDir);
+ await fs.rmdir(targetDir, {recursive: true});
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
@@ -137,4 +329,42 @@ async function removeDirIfExists(targetDir) {
}
}
+async function copyFolder(srcRoot, dstRoot, filter) {
+ const assetPaths = {};
+ const dirEnts = await fs.readdir(srcRoot, {withFileTypes: true});
+ for (const dirEnt of dirEnts) {
+ const dstPath = path.join(dstRoot, dirEnt.name);
+ const srcPath = path.join(srcRoot, dirEnt.name);
+ if (dirEnt.isDirectory()) {
+ await fs.mkdir(dstPath);
+ Object.assign(assetPaths, await copyFolder(srcPath, dstPath, filter));
+ } else if (dirEnt.isFile() && filter(srcPath)) {
+ const content = await fs.readFile(srcPath);
+ const hashedDstPath = resource(dstPath, content);
+ await fs.writeFile(hashedDstPath, content);
+ assetPaths[srcPath] = hashedDstPath;
+ }
+ }
+ return assetPaths;
+}
+
+function resource(relPath, content) {
+ let fullPath = relPath;
+ if (!relPath.startsWith("/")) {
+ fullPath = path.join(targetDir, relPath);
+ }
+ const hash = contentHash(Buffer.from(content));
+ const dir = path.dirname(fullPath);
+ const extname = path.extname(fullPath);
+ const basename = path.basename(fullPath, extname);
+ return path.join(dir, `${basename}-${hash}${extname}`);
+}
+
+function contentHash(str) {
+ var hasher = new XXHash(0);
+ hasher.update(str);
+ return hasher.digest();
+}
+
+
build().catch(err => console.error(err));
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
new file mode 100755
index 00000000..8027b1d8
--- /dev/null
+++ b/scripts/deploy.sh
@@ -0,0 +1,6 @@
+git checkout gh-pages
+cp -R target/* .
+git add $(find . -maxdepth 1 -type f)
+git add themes
+git commit -m "update hydrogen"
+git checkout master
diff --git a/scripts/package.sh b/scripts/package.sh
new file mode 100755
index 00000000..8146fe58
--- /dev/null
+++ b/scripts/package.sh
@@ -0,0 +1,7 @@
+VERSION=$(jq -r ".version" package.json)
+PACKAGE=hydrogen-web-$VERSION.tar.gz
+yarn build
+pushd target
+tar -czvf ../$PACKAGE ./
+popd
+echo $PACKAGE
diff --git a/scripts/serve-local.js b/scripts/serve-local.js
index 535f6885..b6ff8cf3 100644
--- a/scripts/serve-local.js
+++ b/scripts/serve-local.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
const finalhandler = require('finalhandler')
const http = require('http')
const serveStatic = require('serve-static')
@@ -23,4 +39,4 @@ const server = http.createServer(function onRequest (req, res) {
});
// Listen
-server.listen(3000);
\ No newline at end of file
+server.listen(3000);
diff --git a/src/Platform.js b/src/Platform.js
index d8b4a7a9..48a1b920 100644
--- a/src/Platform.js
+++ b/src/Platform.js
@@ -1 +1,17 @@
+/*
+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.
+*/
+
export {WebPlatform as Platform} from "./ui/web/WebPlatform.js";
diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js
index 2b41b1e7..4c3a97bb 100644
--- a/src/domain/BrawlViewModel.js
+++ b/src/domain/BrawlViewModel.js
@@ -1,3 +1,19 @@
+/*
+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 {SessionViewModel} from "./session/SessionViewModel.js";
import {LoginViewModel} from "./LoginViewModel.js";
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js
index 0393582c..ed6ed67a 100644
--- a/src/domain/LoginViewModel.js
+++ b/src/domain/LoginViewModel.js
@@ -1,3 +1,19 @@
+/*
+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 {ViewModel} from "./ViewModel.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js
index 1e51a322..7b225afb 100644
--- a/src/domain/SessionLoadViewModel.js
+++ b/src/domain/SessionLoadViewModel.js
@@ -1,3 +1,19 @@
+/*
+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 {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js";
import {SyncStatus} from "../matrix/Sync.js";
import {ViewModel} from "./ViewModel.js";
diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js
index 3c0bde03..6e33883b 100644
--- a/src/domain/SessionPickerViewModel.js
+++ b/src/domain/SessionPickerViewModel.js
@@ -1,6 +1,23 @@
+/*
+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 {SortedArray} from "../observable/index.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {ViewModel} from "./ViewModel.js";
+import {avatarInitials, getIdentifierColorNumber} from "./avatar.js";
class SessionItemViewModel extends ViewModel {
constructor(sessionInfo, pickerVM) {
@@ -96,6 +113,14 @@ class SessionItemViewModel extends ViewModel {
this.emitChange("exportDataUrl");
}
}
+
+ get avatarColorNumber() {
+ return getIdentifierColorNumber(this._sessionInfo.userId);
+ }
+
+ get avatarInitials() {
+ return avatarInitials(this._sessionInfo.userId);
+ }
}
diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js
index 7e08f8d3..bc35fabd 100644
--- a/src/domain/ViewModel.js
+++ b/src/domain/ViewModel.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
// ViewModel should just be an eventemitter, not an ObservableValue
// as in some cases it would really be more convenient to have multiple events (like telling the timeline to scroll down)
// we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter
@@ -6,10 +22,10 @@ import {EventEmitter} from "../utils/EventEmitter.js";
import {Disposables} from "../utils/Disposables.js";
export class ViewModel extends EventEmitter {
- constructor({clock} = {}) {
+ constructor({clock, emitChange} = {}) {
super();
this.disposables = null;
- this._options = {clock};
+ this._options = {clock, emitChange};
}
childOptions(explicitOptions) {
@@ -54,8 +70,16 @@ export class ViewModel extends EventEmitter {
return result;
}
+ updateOptions(options) {
+ this._options = Object.assign(this._options, options);
+ }
+
emitChange(changedProps) {
- this.emit("change", changedProps);
+ if (this._options.emitChange) {
+ this._options.emitChange(changedProps);
+ } else {
+ this.emit("change", changedProps);
+ }
}
get clock() {
diff --git a/src/domain/avatar.js b/src/domain/avatar.js
new file mode 100644
index 00000000..f94ba3b2
--- /dev/null
+++ b/src/domain/avatar.js
@@ -0,0 +1,49 @@
+/*
+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.
+*/
+
+export function avatarInitials(name) {
+ let firstChar = name.charAt(0);
+ if (firstChar === "!" || firstChar === "@" || firstChar === "#") {
+ firstChar = name.charAt(1);
+ }
+ return firstChar.toUpperCase();
+}
+
+/**
+ * calculates a numeric hash for a given string
+ *
+ * @param {string} str string to hash
+ *
+ * @return {number}
+ */
+function hashCode(str) {
+ let hash = 0;
+ let i;
+ let chr;
+ if (str.length === 0) {
+ return hash;
+ }
+ for (i = 0; i < str.length; i++) {
+ chr = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + chr;
+ hash |= 0;
+ }
+ return Math.abs(hash);
+}
+
+export function getIdentifierColorNumber(id) {
+ return (hashCode(id) % 8) + 1;
+}
diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js
index 20864255..54fad4a1 100644
--- a/src/domain/session/SessionStatusViewModel.js
+++ b/src/domain/session/SessionStatusViewModel.js
@@ -1,3 +1,19 @@
+/*
+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 {ViewModel} from "../ViewModel.js";
import {createEnum} from "../../utils/enum.js";
import {ConnectionStatus} from "../../matrix/net/Reconnector.js";
diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js
index f014f362..879c5e26 100644
--- a/src/domain/session/SessionViewModel.js
+++ b/src/domain/session/SessionViewModel.js
@@ -1,3 +1,19 @@
+/*
+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 {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js";
import {RoomViewModel} from "./room/RoomViewModel.js";
import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
@@ -12,12 +28,13 @@ export class SessionViewModel extends ViewModel {
sync: sessionContainer.sync,
reconnector: sessionContainer.reconnector
})));
+ this._currentRoomTileViewModel = null;
this._currentRoomViewModel = null;
- const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => {
+ const roomTileVMs = this._session.rooms.mapValues((room, emitChange) => {
return new RoomTileViewModel({
room,
- emitUpdate,
- emitOpen: room => this._openRoom(room)
+ emitChange,
+ emitOpen: this._openRoom.bind(this)
});
});
this._roomList = roomTileVMs.sortValues((a, b) => a.compare(b));
@@ -46,7 +63,11 @@ export class SessionViewModel extends ViewModel {
}
}
- _openRoom(room) {
+ _openRoom(room, roomTileVM) {
+ if (this._currentRoomTileViewModel) {
+ this._currentRoomTileViewModel.close();
+ }
+ this._currentRoomTileViewModel = roomTileVM;
if (this._currentRoomViewModel) {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
}
diff --git a/src/domain/session/avatar.js b/src/domain/session/avatar.js
deleted file mode 100644
index ba275d65..00000000
--- a/src/domain/session/avatar.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export function avatarInitials(name) {
- const words = name.split(" ").slice(0, 2);
- return words.reduce((i, w) => i + w.charAt(0).toUpperCase(), "");
-}
\ No newline at end of file
diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js
index 2d585a0d..5b625fe6 100644
--- a/src/domain/session/room/RoomViewModel.js
+++ b/src/domain/session/room/RoomViewModel.js
@@ -1,5 +1,22 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
-import {avatarInitials} from "../avatar.js";
+import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js";
export class RoomViewModel extends ViewModel {
@@ -74,7 +91,9 @@ export class RoomViewModel extends ViewModel {
return avatarInitials(this._room.name);
}
-
+ get avatarColorNumber() {
+ return getIdentifierColorNumber(this._room.id)
+ }
async _sendMessage(message) {
if (message) {
@@ -97,12 +116,28 @@ export class RoomViewModel extends ViewModel {
}
}
-class ComposerViewModel {
+class ComposerViewModel extends ViewModel {
constructor(roomVM) {
+ super();
this._roomVM = roomVM;
+ this._isEmpty = true;
}
sendMessage(message) {
- return this._roomVM._sendMessage(message);
+ const success = this._roomVM._sendMessage(message);
+ if (success) {
+ this._isEmpty = true;
+ this.emitChange("canSend");
+ }
+ return success;
+ }
+
+ get canSend() {
+ return !this._isEmpty;
+ }
+
+ setInput(text) {
+ this._isEmpty = text.length === 0;
+ this.emitChange("canSend");
}
}
diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js
index ce96f00e..396aba9e 100644
--- a/src/domain/session/room/timeline/TilesCollection.js
+++ b/src/domain/session/room/timeline/TilesCollection.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableList} from "../../../../observable/list/BaseObservableList.js";
import {sortedIndex} from "../../../../utils/sortedIndex.js";
@@ -185,6 +201,10 @@ export class TilesCollection extends BaseObservableList {
get length() {
return this._tiles.length;
}
+
+ getFirst() {
+ return this._tiles[0];
+ }
}
import {ObservableArray} from "../../../../observable/list/ObservableArray.js";
diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js
index 4550d7bd..1527d3ae 100644
--- a/src/domain/session/room/timeline/TimelineViewModel.js
+++ b/src/domain/session/room/timeline/TimelineViewModel.js
@@ -1,3 +1,20 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
/*
need better naming, but
entry = event or gap from matrix layer
@@ -16,19 +33,30 @@ when loading, it just reads events from a sortkey backwards or forwards...
*/
import {TilesCollection} from "./TilesCollection.js";
import {tilesCreator} from "./tilesCreator.js";
+import {ViewModel} from "../../../ViewModel.js";
-export class TimelineViewModel {
- constructor({room, timeline, ownUserId}) {
+export class TimelineViewModel extends ViewModel {
+ constructor(options) {
+ super(options);
+ const {room, timeline, ownUserId} = options;
this._timeline = timeline;
// once we support sending messages we could do
// timeline.entries.concat(timeline.pendingEvents)
// for an ObservableList that also contains local echos
- this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId}));
+ this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId, clock: this.clock}));
}
- // doesn't fill gaps, only loads stored entries/tiles
- loadAtTop() {
- return this._timeline.loadAtTop(50);
+ /**
+ * @return {bool} startReached if the start of the timeline was reached
+ */
+ async loadAtTop() {
+ const firstTile = this._tiles.getFirst();
+ if (firstTile.shape === "gap") {
+ return firstTile.fill();
+ } else {
+ await this._timeline.loadAtTop(50);
+ return false;
+ }
}
unloadAtTop(tileAmount) {
diff --git a/src/domain/session/room/timeline/UpdateAction.js b/src/domain/session/room/timeline/UpdateAction.js
index ed09560f..193d903f 100644
--- a/src/domain/session/room/timeline/UpdateAction.js
+++ b/src/domain/session/room/timeline/UpdateAction.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class UpdateAction {
constructor(remove, update, updateParams) {
this._remove = remove;
diff --git a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js
new file mode 100644
index 00000000..537bd6d9
--- /dev/null
+++ b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js
@@ -0,0 +1,23 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {MessageTile} from "./MessageTile.js";
+
+export class EncryptedEventTile extends MessageTile {
+ get text() {
+ return this.i18n`**Encrypted message**`;
+ }
+}
diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js
index b0aaca95..0cfbb491 100644
--- a/src/domain/session/room/timeline/tiles/GapTile.js
+++ b/src/domain/session/room/timeline/tiles/GapTile.js
@@ -1,3 +1,19 @@
+/*
+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 {SimpleTile} from "./SimpleTile.js";
import {UpdateAction} from "../UpdateAction.js";
@@ -13,18 +29,20 @@ export class GapTile extends SimpleTile {
// prevent doing this twice
if (!this._loading) {
this._loading = true;
- this.emitUpdate("isLoading");
+ this.emitChange("isLoading");
try {
await this._timeline.fillGap(this._entry, 10);
} catch (err) {
console.error(`timeline.fillGap(): ${err.message}:\n${err.stack}`);
this._error = err;
- this.emitUpdate("error");
+ this.emitChange("error");
} finally {
this._loading = false;
- this.emitUpdate("isLoading");
+ this.emitChange("isLoading");
}
}
+ // edgeReached will have been updated by fillGap
+ return this._entry.edgeReached;
}
updateEntry(entry, params) {
@@ -44,14 +62,6 @@ export class GapTile extends SimpleTile {
return this._loading;
}
- get isUp() {
- return this._entry.direction.isBackward;
- }
-
- get isDown() {
- return this._entry.direction.isForward;
- }
-
get error() {
if (this._error) {
const dir = this._entry.prev_batch ? "previous" : "next";
diff --git a/src/domain/session/room/timeline/tiles/ImageTile.js b/src/domain/session/room/timeline/tiles/ImageTile.js
index 98b8a791..8bdaa514 100644
--- a/src/domain/session/room/timeline/tiles/ImageTile.js
+++ b/src/domain/session/room/timeline/tiles/ImageTile.js
@@ -1,3 +1,19 @@
+/*
+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 {MessageTile} from "./MessageTile.js";
const MAX_HEIGHT = 300;
@@ -10,32 +26,38 @@ export class ImageTile extends MessageTile {
}
get thumbnailUrl() {
- const mxcUrl = this._getContent().url;
- return this._room.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
+ const mxcUrl = this._getContent()?.url;
+ if (typeof mxcUrl === "string") {
+ return this._room.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
+ }
+ return null;
}
get url() {
- const mxcUrl = this._getContent().url;
- return this._room.mxcUrl(mxcUrl);
+ const mxcUrl = this._getContent()?.url;
+ if (typeof mxcUrl === "string") {
+ return this._room.mxcUrl(mxcUrl);
+ }
+ return null;
}
_scaleFactor() {
- const {info} = this._getContent();
- const scaleHeightFactor = MAX_HEIGHT / info.h;
- const scaleWidthFactor = MAX_WIDTH / info.w;
+ const info = this._getContent()?.info;
+ const scaleHeightFactor = MAX_HEIGHT / info?.h;
+ const scaleWidthFactor = MAX_WIDTH / info?.w;
// take the smallest scale factor, to respect all constraints
// we should not upscale images, so limit scale factor to 1 upwards
return Math.min(scaleWidthFactor, scaleHeightFactor, 1);
}
get thumbnailWidth() {
- const {info} = this._getContent();
- return Math.round(info.w * this._scaleFactor());
+ const info = this._getContent()?.info;
+ return Math.round(info?.w * this._scaleFactor());
}
get thumbnailHeight() {
- const {info} = this._getContent();
- return Math.round(info.h * this._scaleFactor());
+ const info = this._getContent()?.info;
+ return Math.round(info?.h * this._scaleFactor());
}
get label() {
diff --git a/src/domain/session/room/timeline/tiles/LocationTile.js b/src/domain/session/room/timeline/tiles/LocationTile.js
index 4a176233..58870361 100644
--- a/src/domain/session/room/timeline/tiles/LocationTile.js
+++ b/src/domain/session/room/timeline/tiles/LocationTile.js
@@ -1,3 +1,19 @@
+/*
+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 {MessageTile} from "./MessageTile.js";
/*
diff --git a/src/domain/session/room/timeline/tiles/MessageTile.js b/src/domain/session/room/timeline/tiles/MessageTile.js
index e0bd64d4..8ad0b22e 100644
--- a/src/domain/session/room/timeline/tiles/MessageTile.js
+++ b/src/domain/session/room/timeline/tiles/MessageTile.js
@@ -1,10 +1,28 @@
+/*
+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 {SimpleTile} from "./SimpleTile.js";
+import {getIdentifierColorNumber} from "../../../../avatar.js";
export class MessageTile extends SimpleTile {
constructor(options) {
super(options);
+ this._clock = options.clock;
this._isOwn = this._entry.sender === options.ownUserId;
- this._date = new Date(this._entry.timestamp);
+ this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
this._isContinuation = false;
}
@@ -16,12 +34,16 @@ export class MessageTile extends SimpleTile {
return this._entry.sender;
}
+ get senderColorNumber() {
+ return getIdentifierColorNumber(this._entry.sender);
+ }
+
get date() {
- return this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
+ return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
}
get time() {
- return this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"});
+ return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"});
}
get isOwn() {
@@ -38,10 +60,17 @@ export class MessageTile extends SimpleTile {
updatePreviousSibling(prev) {
super.updatePreviousSibling(prev);
- const isContinuation = prev && prev instanceof MessageTile && prev.sender === this.sender;
+ let isContinuation = false;
+ if (prev && prev instanceof MessageTile && prev.sender === this.sender) {
+ // timestamp is null for pending events
+ const myTimestamp = this._entry.timestamp || this._clock.now();
+ const otherTimestamp = prev._entry.timestamp || this._clock.now();
+ // other message was sent less than 5min ago
+ isContinuation = (myTimestamp - otherTimestamp) < (5 * 60 * 1000);
+ }
if (isContinuation !== this._isContinuation) {
this._isContinuation = isContinuation;
- this.emitUpdate("isContinuation");
+ this.emitChange("isContinuation");
}
}
}
diff --git a/src/domain/session/room/timeline/tiles/RoomMemberTile.js b/src/domain/session/room/timeline/tiles/RoomMemberTile.js
index 536cbeb5..0abc0766 100644
--- a/src/domain/session/room/timeline/tiles/RoomMemberTile.js
+++ b/src/domain/session/room/timeline/tiles/RoomMemberTile.js
@@ -1,3 +1,19 @@
+/*
+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 {SimpleTile} from "./SimpleTile.js";
export class RoomMemberTile extends SimpleTile {
diff --git a/src/domain/session/room/timeline/tiles/RoomNameTile.js b/src/domain/session/room/timeline/tiles/RoomNameTile.js
index d37255ae..cf5705dd 100644
--- a/src/domain/session/room/timeline/tiles/RoomNameTile.js
+++ b/src/domain/session/room/timeline/tiles/RoomNameTile.js
@@ -1,3 +1,19 @@
+/*
+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 {SimpleTile} from "./SimpleTile.js";
export class RoomNameTile extends SimpleTile {
diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index da5ba575..2ccb1d17 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -1,9 +1,26 @@
-import {UpdateAction} from "../UpdateAction.js";
+/*
+Copyright 2020 Bruno Windels
-export class SimpleTile {
+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 {UpdateAction} from "../UpdateAction.js";
+import {ViewModel} from "../../../../ViewModel.js";
+
+export class SimpleTile extends ViewModel {
constructor({entry}) {
+ super();
this._entry = entry;
- this._emitUpdate = null;
}
// view model props for all subclasses
// hmmm, could also do instanceof ... ?
@@ -22,12 +39,6 @@ export class SimpleTile {
return false;
}
- emitUpdate(paramName) {
- if (this._emitUpdate) {
- this._emitUpdate(this, paramName);
- }
- }
-
get internalId() {
return this._entry.asEventKey().toString();
}
@@ -37,7 +48,7 @@ export class SimpleTile {
}
// TilesCollection contract below
setUpdateEmit(emitUpdate) {
- this._emitUpdate = emitUpdate;
+ this.updateOptions({emitChange: paramName => emitUpdate(this, paramName)});
}
get upperEntry() {
diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index a6144b1b..3009e15e 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -1,3 +1,19 @@
+/*
+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 {MessageTile} from "./MessageTile.js";
export class TextTile extends MessageTile {
diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js
index 7f4d57e3..1ae17bdc 100644
--- a/src/domain/session/room/timeline/tilesCreator.js
+++ b/src/domain/session/room/timeline/tilesCreator.js
@@ -1,13 +1,30 @@
+/*
+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 {ImageTile} from "./tiles/ImageTile.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";
-export function tilesCreator({room, ownUserId}) {
+export function tilesCreator({room, ownUserId, clock}) {
return function tilesCreator(entry, emitUpdate) {
- const options = {entry, emitUpdate, ownUserId};
+ const options = {entry, emitUpdate, ownUserId, clock};
if (entry.isGap) {
return new GapTile(options, room);
} else if (entry.eventType) {
@@ -33,6 +50,8 @@ export function tilesCreator({room, ownUserId}) {
return new RoomNameTile(options);
case "m.room.member":
return new RoomMemberTile(options);
+ case "m.room.encrypted":
+ return new EncryptedEventTile(options);
default:
// unknown type not rendered
return null;
diff --git a/src/domain/session/roomlist/RoomTileViewModel.js b/src/domain/session/roomlist/RoomTileViewModel.js
index cd12242f..9f527f65 100644
--- a/src/domain/session/roomlist/RoomTileViewModel.js
+++ b/src/domain/session/roomlist/RoomTileViewModel.js
@@ -1,23 +1,60 @@
-import {avatarInitials} from "../avatar.js";
+/*
+Copyright 2020 Bruno Windels
-export class RoomTileViewModel {
+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 {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
+import {ViewModel} from "../../ViewModel.js";
+
+export class RoomTileViewModel extends ViewModel {
// we use callbacks to parent VM instead of emit because
// it would be annoying to keep track of subscriptions in
// parent for all RoomTileViewModels
// emitUpdate is ObservableMap/ObservableList update mechanism
- constructor({room, emitUpdate, emitOpen}) {
+ constructor(options) {
+ super(options);
+ const {room, emitOpen} = options;
this._room = room;
- this._emitUpdate = emitUpdate;
this._emitOpen = emitOpen;
+ this._isOpen = false;
+ }
+
+ // called by parent for now (later should integrate with router)
+ close() {
+ if (this._isOpen) {
+ this._isOpen = false;
+ this.emitChange("isOpen");
+ }
}
open() {
- this._emitOpen(this._room);
+ this._isOpen = true;
+ this.emitChange("isOpen");
+ this._emitOpen(this._room, this);
}
compare(other) {
- // sort by name for now
- return this._room.name.localeCompare(other._room.name);
+ // sort alphabetically
+ const nameCmp = this._room.name.localeCompare(other._room.name);
+ if (nameCmp === 0) {
+ return this._room.id.localeCompare(other._room.id);
+ }
+ return nameCmp;
+ }
+
+ get isOpen() {
+ return this._isOpen;
}
get name() {
@@ -27,4 +64,8 @@ export class RoomTileViewModel {
get avatarInitials() {
return avatarInitials(this._room.name);
}
+
+ get avatarColorNumber() {
+ return getIdentifierColorNumber(this._room.id)
+ }
}
diff --git a/src/legacy-polyfill.js b/src/legacy-polyfill.js
new file mode 100644
index 00000000..5665158c
--- /dev/null
+++ b/src/legacy-polyfill.js
@@ -0,0 +1,26 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// polyfills needed for IE11
+import "core-js/stable";
+import "regenerator-runtime/runtime";
+import "mdn-polyfills/Element.prototype.closest";
+// TODO: contribute this to mdn-polyfills
+if (!Element.prototype.remove) {
+ Element.prototype.remove = function remove() {
+ this.parentNode.removeChild(this);
+ };
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 7c8d46b6..2c86b9d9 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,5 +1,23 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js";
-import {fetchRequest} from "./matrix/net/request/fetch.js";
+import {createFetchRequest} from "./matrix/net/request/fetch.js";
+import {xhrRequest} from "./matrix/net/request/xhr.js";
import {SessionContainer} from "./matrix/SessionContainer.js";
import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js";
@@ -8,7 +26,10 @@ import {BrawlView} from "./ui/web/BrawlView.js";
import {Clock} from "./ui/web/dom/Clock.js";
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
-export default async function main(container) {
+// Don't use a default export here, as we use multiple entries during legacy build,
+// which does not support default exports,
+// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
+export async function main(container) {
try {
// to replay:
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
@@ -16,13 +37,17 @@ export default async function main(container) {
// const request = replay.request;
// to record:
- // const recorder = new RecordRequester(fetchRequest);
+ // const recorder = new RecordRequester(createFetchRequest(clock.createTimeout));
// const request = recorder.request;
// window.getBrawlFetchLog = () => recorder.log();
- // normal network:
- const request = fetchRequest;
- const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1");
const clock = new Clock();
+ let request;
+ if (typeof fetch === "function") {
+ request = createFetchRequest(clock.createTimeout);
+ } else {
+ request = xhrRequest;
+ }
+ const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1");
const storageFactory = new StorageFactory();
const vm = new BrawlViewModel({
diff --git a/src/matrix/SendScheduler.js b/src/matrix/SendScheduler.js
index 1b572cc2..6e6196d3 100644
--- a/src/matrix/SendScheduler.js
+++ b/src/matrix/SendScheduler.js
@@ -1,3 +1,19 @@
+/*
+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 {Platform} from "../Platform.js";
import {HomeServerError, ConnectionError} from "./error.js";
diff --git a/src/matrix/Session.js b/src/matrix/Session.js
index 4186d5f3..80ef342b 100644
--- a/src/matrix/Session.js
+++ b/src/matrix/Session.js
@@ -1,3 +1,19 @@
+/*
+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 {Room} from "./room/Room.js";
import { ObservableMap } from "../observable/index.js";
import { SendScheduler, RateLimitingBackoff } from "./SendScheduler.js";
diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js
index e5cd684c..74184a0a 100644
--- a/src/matrix/SessionContainer.js
+++ b/src/matrix/SessionContainer.js
@@ -1,3 +1,19 @@
+/*
+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 {createEnum} from "../utils/enum.js";
import {ObservableValue} from "../observable/ObservableValue.js";
import {HomeServerApi} from "./net/HomeServerApi.js";
diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js
index aba1ae30..4b4acd9d 100644
--- a/src/matrix/Sync.js
+++ b/src/matrix/Sync.js
@@ -1,3 +1,20 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import {AbortError} from "./error.js";
import {ObservableValue} from "../observable/ObservableValue.js";
import {createEnum} from "../utils/enum.js";
@@ -27,6 +44,15 @@ function parseRooms(roomsSection, roomCallback) {
return [];
}
+function timelineIsEmpty(roomResponse) {
+ try {
+ const events = roomResponse?.timeline?.events;
+ return Array.isArray(events) && events.length === 0;
+ } catch (err) {
+ return true;
+ }
+}
+
export class Sync {
constructor({hsApi, session, storage}) {
this._hsApi = hsApi;
@@ -86,6 +112,7 @@ export class Sync {
const totalRequestTimeout = timeout + (80 * 1000); // same as riot-web, don't get stuck on wedged long requests
this._currentRequest = this._hsApi.sync(syncToken, syncFilterId, timeout, {timeout: totalRequestTimeout});
const response = await this._currentRequest.response();
+ const isInitialSync = !syncToken;
syncToken = response.next_batch;
const storeNames = this._storage.storeNames;
const syncTxn = await this._storage.readWriteTxn([
@@ -105,6 +132,11 @@ export class Sync {
// presence
if (response.rooms) {
const promises = parseRooms(response.rooms, async (roomId, roomResponse, membership) => {
+ // ignore rooms with empty timelines during initial sync,
+ // see https://github.com/vector-im/hydrogen-web/issues/15
+ if (isInitialSync && timelineIsEmpty(roomResponse)) {
+ return;
+ }
let room = this._session.rooms.get(roomId);
if (!room) {
room = this._session.createRoom(roomId);
diff --git a/src/matrix/User.js b/src/matrix/User.js
index 6db27e78..06855d9a 100644
--- a/src/matrix/User.js
+++ b/src/matrix/User.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class User {
constructor(userId) {
this._userId = userId;
diff --git a/src/matrix/error.js b/src/matrix/error.js
index f8a0c57c..07144acd 100644
--- a/src/matrix/error.js
+++ b/src/matrix/error.js
@@ -1,3 +1,30 @@
+/*
+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.
+*/
+
+export class WrappedError extends Error {
+ constructor(message, cause) {
+ super(`${message}: ${cause.message}`);
+ this.cause = cause;
+ }
+
+ get name() {
+ return "WrappedError";
+ }
+}
+
export class HomeServerError extends Error {
constructor(method, url, body, status) {
super(`${body ? body.error : status} on ${method} ${url}`);
diff --git a/src/matrix/net/ExponentialRetryDelay.js b/src/matrix/net/ExponentialRetryDelay.js
index a1bae822..eac4bec0 100644
--- a/src/matrix/net/ExponentialRetryDelay.js
+++ b/src/matrix/net/ExponentialRetryDelay.js
@@ -1,3 +1,19 @@
+/*
+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 {AbortError} from "../../utils/error.js";
export class ExponentialRetryDelay {
diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js
index c65c4dfa..c71ef0a5 100644
--- a/src/matrix/net/HomeServerApi.js
+++ b/src/matrix/net/HomeServerApi.js
@@ -1,3 +1,19 @@
+/*
+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 {
HomeServerError,
ConnectionError,
@@ -5,9 +21,9 @@ import {
} from "../error.js";
class RequestWrapper {
- constructor(method, url, requestResult, responsePromise) {
+ constructor(method, url, requestResult) {
this._requestResult = requestResult;
- this._promise = responsePromise.then(response => {
+ this._promise = requestResult.response().then(response => {
// ok?
if (response.status >= 200 && response.status < 300) {
return response.body;
@@ -44,35 +60,6 @@ export class HomeServerApi {
return `${this._homeserver}/_matrix/client/r0${csPath}`;
}
- _abortOnTimeout(timeoutAmount, requestResult, responsePromise) {
- const timeout = this._createTimeout(timeoutAmount);
- // abort request if timeout finishes first
- let timedOut = false;
- timeout.elapsed().then(
- () => {
- timedOut = true;
- requestResult.abort();
- },
- () => {} // ignore AbortError
- );
- // abort timeout if request finishes first
- return responsePromise.then(
- response => {
- timeout.abort();
- return response;
- },
- err => {
- timeout.abort();
- // map error to TimeoutError
- if (err instanceof AbortError && timedOut) {
- throw new ConnectionError(`Request timed out after ${timeoutAmount}ms`, true);
- } else {
- throw err;
- }
- }
- );
- }
-
_encodeQueryParams(queryParams) {
return Object.entries(queryParams || {})
.filter(([, value]) => value !== undefined)
@@ -102,19 +89,10 @@ export class HomeServerApi {
method,
headers,
body: bodyString,
+ timeout: options && options.timeout
});
- let responsePromise = requestResult.response();
-
- if (options && options.timeout) {
- responsePromise = this._abortOnTimeout(
- options.timeout,
- requestResult,
- responsePromise
- );
- }
-
- const wrapper = new RequestWrapper(method, url, requestResult, responsePromise);
+ const wrapper = new RequestWrapper(method, url, requestResult);
if (this._reconnector) {
wrapper.response().catch(err => {
diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js
index 298920f7..d3cf790f 100644
--- a/src/matrix/net/Reconnector.js
+++ b/src/matrix/net/Reconnector.js
@@ -1,3 +1,19 @@
+/*
+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 {createEnum} from "../../utils/enum.js";
import {ObservableValue} from "../../observable/ObservableValue.js";
diff --git a/src/matrix/net/request/fetch.js b/src/matrix/net/request/fetch.js
index 886e8195..38624682 100644
--- a/src/matrix/net/request/fetch.js
+++ b/src/matrix/net/request/fetch.js
@@ -1,7 +1,25 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import {
AbortError,
ConnectionError
} from "../../error.js";
+import {abortOnTimeout} from "../timeout.js";
class RequestResult {
constructor(promise, controller) {
@@ -15,9 +33,9 @@ class RequestResult {
}
};
});
- this._promise = Promise.race([promise, abortPromise]);
+ this.promise = Promise.race([promise, abortPromise]);
} else {
- this._promise = promise;
+ this.promise = promise;
this._controller = controller;
}
}
@@ -27,47 +45,55 @@ class RequestResult {
}
response() {
- return this._promise;
+ return this.promise;
}
}
-export function fetchRequest(url, options) {
- const controller = typeof AbortController === "function" ? new AbortController() : null;
- if (controller) {
+export function createFetchRequest(createTimeout) {
+ return function fetchRequest(url, options) {
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
+ if (controller) {
+ options = Object.assign(options, {
+ signal: controller.signal
+ });
+ }
options = Object.assign(options, {
- signal: controller.signal
+ mode: "cors",
+ credentials: "omit",
+ referrer: "no-referrer",
+ cache: "no-cache",
});
- }
- options = Object.assign(options, {
- mode: "cors",
- credentials: "omit",
- referrer: "no-referrer",
- cache: "no-cache",
- });
- if (options.headers) {
- const headers = new Headers();
- for(const [name, value] of options.headers.entries()) {
- headers.append(name, value);
+ if (options.headers) {
+ const headers = new Headers();
+ for(const [name, value] of options.headers.entries()) {
+ headers.append(name, value);
+ }
+ options.headers = headers;
}
- options.headers = headers;
- }
- const promise = fetch(url, options).then(async response => {
- const {status} = response;
- const body = await response.json();
- return {status, body};
- }, err => {
- if (err.name === "AbortError") {
- throw new AbortError();
- } else if (err instanceof TypeError) {
- // Network errors are reported as TypeErrors, see
- // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful
- // this can either mean user is offline, server is offline, or a CORS error (server misconfiguration).
- //
- // One could check navigator.onLine to rule out the first
- // but the 2 latter ones are indistinguishable from javascript.
- throw new ConnectionError(`${options.method} ${url}: ${err.message}`);
+ const promise = fetch(url, options).then(async response => {
+ const {status} = response;
+ const body = await response.json();
+ return {status, body};
+ }, err => {
+ if (err.name === "AbortError") {
+ throw new AbortError();
+ } else if (err instanceof TypeError) {
+ // Network errors are reported as TypeErrors, see
+ // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful
+ // this can either mean user is offline, server is offline, or a CORS error (server misconfiguration).
+ //
+ // One could check navigator.onLine to rule out the first
+ // but the 2 latter ones are indistinguishable from javascript.
+ throw new ConnectionError(`${options.method} ${url}: ${err.message}`);
+ }
+ throw err;
+ });
+ const result = new RequestResult(promise, controller);
+
+ if (options.timeout) {
+ result.promise = abortOnTimeout(createTimeout, options.timeout, result, result.promise);
}
- throw err;
- });
- return new RequestResult(promise, controller);
+
+ return result;
+ }
}
diff --git a/src/matrix/net/request/replay.js b/src/matrix/net/request/replay.js
index c6f12269..c85a32fd 100644
--- a/src/matrix/net/request/replay.js
+++ b/src/matrix/net/request/replay.js
@@ -1,3 +1,19 @@
+/*
+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 {
AbortError,
ConnectionError
diff --git a/src/matrix/net/request/xhr.js b/src/matrix/net/request/xhr.js
new file mode 100644
index 00000000..ed4a36c3
--- /dev/null
+++ b/src/matrix/net/request/xhr.js
@@ -0,0 +1,97 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+ AbortError,
+ ConnectionError
+} from "../../error.js";
+
+class RequestResult {
+ constructor(promise, xhr) {
+ this._promise = promise;
+ this._xhr = xhr;
+ }
+
+ abort() {
+ this._xhr.abort();
+ }
+
+ response() {
+ return this._promise;
+ }
+}
+
+function send(url, options) {
+ const xhr = new XMLHttpRequest();
+ xhr.open(options.method, url);
+ if (options.headers) {
+ for(const [name, value] of options.headers.entries()) {
+ xhr.setRequestHeader(name, value);
+ }
+ }
+ if (options.timeout) {
+ xhr.timeout = options.timeout;
+ }
+
+ xhr.send(options.body || null);
+
+ return xhr;
+}
+
+function xhrAsPromise(xhr, method, url) {
+ return new Promise((resolve, reject) => {
+ xhr.addEventListener("load", () => resolve(xhr));
+ xhr.addEventListener("abort", () => reject(new AbortError()));
+ xhr.addEventListener("error", () => reject(new ConnectionError(`Error ${method} ${url}`)));
+ xhr.addEventListener("timeout", () => reject(new ConnectionError(`Timeout ${method} ${url}`, true)));
+ });
+}
+
+function addCacheBuster(urlStr, random = Math.random) {
+ // XHR doesn't have a good way to disable cache,
+ // so add a random query param
+ // see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
+ if (urlStr.includes("?")) {
+ urlStr = urlStr + "&";
+ } else {
+ urlStr = urlStr + "?";
+ }
+ return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
+}
+
+export function xhrRequest(url, options) {
+ url = addCacheBuster(url);
+ const xhr = send(url, options);
+ const promise = xhrAsPromise(xhr, options.method, url).then(xhr => {
+ const {status} = xhr;
+ let body = xhr.responseText;
+ if (xhr.getResponseHeader("Content-Type") === "application/json") {
+ body = JSON.parse(body);
+ }
+ return {status, body};
+ });
+ return new RequestResult(promise, xhr);
+}
+
+export function tests() {
+ return {
+ "add cache buster": assert => {
+ const random = () => 0.5;
+ assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
+ assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
+ }
+ }
+}
diff --git a/src/matrix/net/timeout.js b/src/matrix/net/timeout.js
new file mode 100644
index 00000000..abb7fd9e
--- /dev/null
+++ b/src/matrix/net/timeout.js
@@ -0,0 +1,51 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+ AbortError,
+ ConnectionError
+} from "../error.js";
+
+
+export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {
+ const timeout = createTimeout(timeoutAmount);
+ // abort request if timeout finishes first
+ let timedOut = false;
+ timeout.elapsed().then(
+ () => {
+ timedOut = true;
+ requestResult.abort();
+ },
+ () => {} // ignore AbortError when timeout is aborted
+ );
+ // abort timeout if request finishes first
+ return responsePromise.then(
+ response => {
+ timeout.abort();
+ return response;
+ },
+ err => {
+ timeout.abort();
+ // map error to TimeoutError
+ if (err instanceof AbortError && timedOut) {
+ throw new ConnectionError(`Request timed out after ${timeoutAmount}ms`, true);
+ } else {
+ throw err;
+ }
+ }
+ );
+}
diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js
index f7e9b335..ca7618f2 100644
--- a/src/matrix/room/Room.js
+++ b/src/matrix/room/Room.js
@@ -1,3 +1,19 @@
+/*
+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 {EventEmitter} from "../../utils/EventEmitter.js";
import {RoomSummary} from "./RoomSummary.js";
import {SyncWriter} from "./timeline/persistence/SyncWriter.js";
@@ -5,6 +21,7 @@ import {GapWriter} from "./timeline/persistence/GapWriter.js";
import {Timeline} from "./timeline/Timeline.js";
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
import {SendQueue} from "./sending/SendQueue.js";
+import {WrappedError} from "../error.js"
export class Room extends EventEmitter {
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) {
@@ -51,8 +68,12 @@ export class Room extends EventEmitter {
}
load(summary, txn) {
- this._summary.load(summary);
- return this._syncWriter.load(txn);
+ try {
+ this._summary.load(summary);
+ return this._syncWriter.load(txn);
+ } catch (err) {
+ throw new WrappedError(`Could not load room ${this._roomId}`, err);
+ }
}
sendEvent(eventType, content) {
@@ -96,6 +117,9 @@ export class Room extends EventEmitter {
/** @public */
async fillGap(fragmentEntry, amount) {
+ if (fragmentEntry.edgeReached) {
+ return;
+ }
const response = await this._hsApi.messages(this._roomId, {
from: fragmentEntry.token,
dir: fragmentEntry.direction.asApiString(),
diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js
index b15d9998..96eba2a8 100644
--- a/src/matrix/room/RoomSummary.js
+++ b/src/matrix/room/RoomSummary.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
function applySyncResponse(data, roomResponse, membership) {
if (roomResponse.summary) {
data = updateSummary(data, roomResponse.summary);
diff --git a/src/matrix/room/sending/PendingEvent.js b/src/matrix/room/sending/PendingEvent.js
index a87efc98..2b4f7477 100644
--- a/src/matrix/room/sending/PendingEvent.js
+++ b/src/matrix/room/sending/PendingEvent.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class PendingEvent {
constructor(data) {
this._data = data;
diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js
index d4ee9828..ba215e04 100644
--- a/src/matrix/room/sending/SendQueue.js
+++ b/src/matrix/room/sending/SendQueue.js
@@ -1,3 +1,19 @@
+/*
+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 {SortedArray} from "../../../observable/list/SortedArray.js";
import {ConnectionError} from "../../error.js";
import {PendingEvent} from "./PendingEvent.js";
diff --git a/src/matrix/room/timeline/Direction.js b/src/matrix/room/timeline/Direction.js
index 05fcc0dc..c83ec61c 100644
--- a/src/matrix/room/timeline/Direction.js
+++ b/src/matrix/room/timeline/Direction.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class Direction {
constructor(isForward) {
this._isForward = isForward;
diff --git a/src/matrix/room/timeline/EventKey.js b/src/matrix/room/timeline/EventKey.js
index 885efba0..128f8805 100644
--- a/src/matrix/room/timeline/EventKey.js
+++ b/src/matrix/room/timeline/EventKey.js
@@ -1,3 +1,19 @@
+/*
+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 {Platform} from "../../../Platform.js";
// key for events in the timelineEvents store
diff --git a/src/matrix/room/timeline/FragmentIdComparer.js b/src/matrix/room/timeline/FragmentIdComparer.js
index da3b2243..2c829418 100644
--- a/src/matrix/room/timeline/FragmentIdComparer.js
+++ b/src/matrix/room/timeline/FragmentIdComparer.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
/*
lookups will be far more frequent than changing fragment order,
so data structure should be optimized for fast lookup
diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js
index bd4874c5..a64be169 100644
--- a/src/matrix/room/timeline/Timeline.js
+++ b/src/matrix/room/timeline/Timeline.js
@@ -1,3 +1,19 @@
+/*
+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 {SortedArray, MappedList, ConcatList} from "../../../observable/index.js";
import {Direction} from "./Direction.js";
import {TimelineReader} from "./persistence/TimelineReader.js";
diff --git a/src/matrix/room/timeline/common.js b/src/matrix/room/timeline/common.js
index 7565d8a4..ec1ec499 100644
--- a/src/matrix/room/timeline/common.js
+++ b/src/matrix/room/timeline/common.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export function isValidFragmentId(id) {
return typeof id === "number";
}
diff --git a/src/matrix/room/timeline/entries/BaseEntry.js b/src/matrix/room/timeline/entries/BaseEntry.js
index bd129cf2..67ba158d 100644
--- a/src/matrix/room/timeline/entries/BaseEntry.js
+++ b/src/matrix/room/timeline/entries/BaseEntry.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
//entries can be sorted, first by fragment, then by entry index.
import {EventKey} from "../EventKey.js";
export const PENDING_FRAGMENT_ID = Number.MAX_SAFE_INTEGER;
diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js
index 041c392c..ead383fa 100644
--- a/src/matrix/room/timeline/entries/EventEntry.js
+++ b/src/matrix/room/timeline/entries/EventEntry.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseEntry} from "./BaseEntry.js";
export class EventEntry extends BaseEntry {
@@ -19,8 +35,7 @@ export class EventEntry extends BaseEntry {
}
get prevContent() {
- const unsigned = this._eventEntry.event.unsigned;
- return unsigned && unsigned.prev_content;
+ return this._eventEntry.event.unsigned?.prev_content;
}
get eventType() {
diff --git a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js
index c84ddede..791c7a9f 100644
--- a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js
+++ b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseEntry} from "./BaseEntry.js";
import {Direction} from "../Direction.js";
import {isValidFragmentId} from "../common.js";
@@ -44,7 +60,7 @@ export class FragmentBoundaryEntry extends BaseEntry {
}
get isGap() {
- return !!this.token;
+ return !!this.token && !this.edgeReached;
}
get token() {
@@ -63,6 +79,25 @@ export class FragmentBoundaryEntry extends BaseEntry {
}
}
+ get edgeReached() {
+ if (this.started) {
+ return this.fragment.startReached;
+ } else {
+ return this.fragment.endReached;
+ }
+ }
+
+ set edgeReached(reached) {
+
+ if (this.started) {
+ this.fragment.startReached = reached;
+ } else {
+ this.fragment.endReached = reached;
+ }
+ }
+
+
+
get linkedFragmentId() {
if (this.started) {
return this.fragment.previousId;
diff --git a/src/matrix/room/timeline/entries/PendingEventEntry.js b/src/matrix/room/timeline/entries/PendingEventEntry.js
index 0d2c9ae1..e5fd769c 100644
--- a/src/matrix/room/timeline/entries/PendingEventEntry.js
+++ b/src/matrix/room/timeline/entries/PendingEventEntry.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseEntry, PENDING_FRAGMENT_ID} from "./BaseEntry.js";
export class PendingEventEntry extends BaseEntry {
diff --git a/src/matrix/room/timeline/persistence/GapWriter.js b/src/matrix/room/timeline/persistence/GapWriter.js
index 36bb1256..11b774d3 100644
--- a/src/matrix/room/timeline/persistence/GapWriter.js
+++ b/src/matrix/room/timeline/persistence/GapWriter.js
@@ -1,3 +1,19 @@
+/*
+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 {EventKey} from "../EventKey.js";
import {EventEntry} from "../entries/EventEntry.js";
import {createEventEntry, directionalAppend} from "./common.js";
@@ -162,6 +178,14 @@ export class GapWriter {
if (fragmentEntry.token !== start) {
throw new Error("start is not equal to prev_batch or next_batch");
}
+
+ // begin (or end) of timeline reached
+ if (chunk.length === 0) {
+ fragmentEntry.edgeReached = true;
+ await txn.timelineFragments.update(fragmentEntry.fragment);
+ return {entries: [fragmentEntry], fragments: []};
+ }
+
// find last event in fragment so we get the eventIndex to begin creating keys at
let lastKey = await this._findFragmentEdgeEventKey(fragmentEntry, txn);
// find out if any event in chunk is already present using findFirstOrLastOccurringEventId
diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js
index 6400aa9d..51858164 100644
--- a/src/matrix/room/timeline/persistence/SyncWriter.js
+++ b/src/matrix/room/timeline/persistence/SyncWriter.js
@@ -1,3 +1,19 @@
+/*
+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 {EventKey} from "../EventKey.js";
import {EventEntry} from "../entries/EventEntry.js";
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js";
diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js
index 3262550a..928d6b64 100644
--- a/src/matrix/room/timeline/persistence/TimelineReader.js
+++ b/src/matrix/room/timeline/persistence/TimelineReader.js
@@ -1,3 +1,19 @@
+/*
+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 {directionalConcat, directionalAppend} from "./common.js";
import {Direction} from "../Direction.js";
import {EventEntry} from "../entries/EventEntry.js";
@@ -44,8 +60,8 @@ export class TimelineReader {
let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, this._fragmentIdComparer);
// append or prepend fragmentEntry, reuse func from GapWriter?
directionalAppend(entries, fragmentEntry, direction);
- // don't count it in amount perhaps? or do?
- if (fragmentEntry.hasLinkedFragment) {
+ // only continue loading if the fragment boundary can't be backfilled
+ if (!fragmentEntry.token && fragmentEntry.hasLinkedFragment) {
const nextFragment = await fragmentStore.get(this._roomId, fragmentEntry.linkedFragmentId);
this._fragmentIdComparer.add(nextFragment);
const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, this._fragmentIdComparer);
diff --git a/src/matrix/room/timeline/persistence/common.js b/src/matrix/room/timeline/persistence/common.js
index 93d96f94..6d954505 100644
--- a/src/matrix/room/timeline/persistence/common.js
+++ b/src/matrix/room/timeline/persistence/common.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export function createEventEntry(key, roomId, event) {
return {
fragmentId: key.fragmentId,
diff --git a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js
index 29c4be94..ba59a6f3 100644
--- a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js
+++ b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class SessionInfoStorage {
constructor(name) {
this._name = name;
diff --git a/src/matrix/storage/common.js b/src/matrix/storage/common.js
index fe0eebba..1d980998 100644
--- a/src/matrix/storage/common.js
+++ b/src/matrix/storage/common.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export const STORE_NAMES = Object.freeze([
"session",
"roomState",
@@ -24,6 +40,11 @@ export class StorageError extends Error {
if (typeof cause.code === "number") {
fullMessage += `(code: ${cause.name}) `;
}
+ }
+ if (value) {
+ fullMessage += `(value: ${JSON.stringify(value)}) `;
+ }
+ if (cause) {
fullMessage += cause.message;
}
super(fullMessage);
@@ -33,4 +54,8 @@ export class StorageError extends Error {
this.cause = cause;
this.value = value;
}
+
+ get name() {
+ return "StorageError";
+ }
}
diff --git a/src/matrix/storage/idb/QueryTarget.js b/src/matrix/storage/idb/QueryTarget.js
index 1948b685..9b6f3036 100644
--- a/src/matrix/storage/idb/QueryTarget.js
+++ b/src/matrix/storage/idb/QueryTarget.js
@@ -1,3 +1,19 @@
+/*
+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 {iterateCursor, reqAsPromise} from "./utils.js";
export class QueryTarget {
diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js
index 812e1cc8..9c79b92f 100644
--- a/src/matrix/storage/idb/Storage.js
+++ b/src/matrix/storage/idb/Storage.js
@@ -1,3 +1,19 @@
+/*
+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 {Transaction} from "./Transaction.js";
import { STORE_NAMES, StorageError } from "../common.js";
diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js
index 00c8ead9..0b58d5dc 100644
--- a/src/matrix/storage/idb/StorageFactory.js
+++ b/src/matrix/storage/idb/StorageFactory.js
@@ -1,3 +1,19 @@
+/*
+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 {Storage} from "./Storage.js";
import { openDatabase, reqAsPromise } from "./utils.js";
import { exportSession, importSession } from "./export.js";
diff --git a/src/matrix/storage/idb/Store.js b/src/matrix/storage/idb/Store.js
index 7acf9411..8cb8fd4c 100644
--- a/src/matrix/storage/idb/Store.js
+++ b/src/matrix/storage/idb/Store.js
@@ -1,3 +1,19 @@
+/*
+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 {QueryTarget} from "./QueryTarget.js";
import { reqAsPromise } from "./utils.js";
import { StorageError } from "../common.js";
@@ -98,7 +114,7 @@ export class Store extends QueryTarget {
return await reqAsPromise(this._idbStore.put(value));
} catch(err) {
const originalErr = err.cause;
- throw new StorageError(`put on ${this._idbStore.name} failed`, originalErr, value);
+ throw new StorageError(`put on ${err.databaseName}.${err.storeName} failed`, originalErr, value);
}
}
@@ -107,11 +123,17 @@ export class Store extends QueryTarget {
return await reqAsPromise(this._idbStore.add(value));
} catch(err) {
const originalErr = err.cause;
- throw new StorageError(`add on ${this._idbStore.name} failed`, originalErr, value);
+ throw new StorageError(`add on ${err.databaseName}.${err.storeName} failed`, originalErr, value);
}
}
- delete(keyOrKeyRange) {
- return reqAsPromise(this._idbStore.delete(keyOrKeyRange));
+ async delete(keyOrKeyRange) {
+ try {
+ return await reqAsPromise(this._idbStore.delete(keyOrKeyRange));
+ } catch(err) {
+ const originalErr = err.cause;
+ throw new StorageError(`delete on ${err.databaseName}.${err.storeName} failed`, originalErr, keyOrKeyRange);
+ }
+
}
}
diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js
index 8e0dcc57..4f5e3af5 100644
--- a/src/matrix/storage/idb/Transaction.js
+++ b/src/matrix/storage/idb/Transaction.js
@@ -1,3 +1,19 @@
+/*
+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 {txnAsPromise} from "./utils.js";
import {StorageError} from "../common.js";
import {Store} from "./Store.js";
diff --git a/src/matrix/storage/idb/export.js b/src/matrix/storage/idb/export.js
index 14cabc48..8a7724a3 100644
--- a/src/matrix/storage/idb/export.js
+++ b/src/matrix/storage/idb/export.js
@@ -1,3 +1,19 @@
+/*
+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 { iterateCursor, txnAsPromise } from "./utils.js";
import { STORE_NAMES } from "../common.js";
diff --git a/src/matrix/storage/idb/stores/PendingEventStore.js b/src/matrix/storage/idb/stores/PendingEventStore.js
index 7aa5408a..6492a272 100644
--- a/src/matrix/storage/idb/stores/PendingEventStore.js
+++ b/src/matrix/storage/idb/stores/PendingEventStore.js
@@ -1,3 +1,19 @@
+/*
+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 { encodeUint32, decodeUint32 } from "../utils.js";
import {Platform} from "../../../../Platform.js";
diff --git a/src/matrix/storage/idb/stores/RoomStateStore.js b/src/matrix/storage/idb/stores/RoomStateStore.js
index 65a973b0..0cec87bc 100644
--- a/src/matrix/storage/idb/stores/RoomStateStore.js
+++ b/src/matrix/storage/idb/stores/RoomStateStore.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class RoomStateStore {
constructor(idbStore) {
this._roomStateStore = idbStore;
diff --git a/src/matrix/storage/idb/stores/RoomSummaryStore.js b/src/matrix/storage/idb/stores/RoomSummaryStore.js
index 45cf8468..1264657e 100644
--- a/src/matrix/storage/idb/stores/RoomSummaryStore.js
+++ b/src/matrix/storage/idb/stores/RoomSummaryStore.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
/**
store contains:
roomId
diff --git a/src/matrix/storage/idb/stores/SessionStore.js b/src/matrix/storage/idb/stores/SessionStore.js
index 72af611d..e26de3b4 100644
--- a/src/matrix/storage/idb/stores/SessionStore.js
+++ b/src/matrix/storage/idb/stores/SessionStore.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
/**
store contains:
loginData {
diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.js b/src/matrix/storage/idb/stores/TimelineEventStore.js
index 7ccb0883..a3a829f9 100644
--- a/src/matrix/storage/idb/stores/TimelineEventStore.js
+++ b/src/matrix/storage/idb/stores/TimelineEventStore.js
@@ -1,3 +1,19 @@
+/*
+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 {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js";
import { encodeUint32 } from "../utils.js";
diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.js b/src/matrix/storage/idb/stores/TimelineFragmentStore.js
index 4dc33c13..2a28c7bc 100644
--- a/src/matrix/storage/idb/stores/TimelineFragmentStore.js
+++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.js
@@ -1,3 +1,19 @@
+/*
+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 { StorageError } from "../../common.js";
import {Platform} from "../../../../Platform.js";
import { encodeUint32 } from "../utils.js";
diff --git a/src/matrix/storage/idb/utils.js b/src/matrix/storage/idb/utils.js
index f80fe582..75f2b08b 100644
--- a/src/matrix/storage/idb/utils.js
+++ b/src/matrix/storage/idb/utils.js
@@ -1,5 +1,31 @@
+/*
+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 { StorageError } from "../common.js";
+class WrappedDOMException extends StorageError {
+ constructor(request) {
+ const source = request?.source;
+ const storeName = source?.name || "";
+ const databaseName = source?.transaction?.db?.name || "";
+ super(`Failed IDBRequest on ${databaseName}.${storeName}`, request.error);
+ this.storeName = storeName;
+ this.databaseName = databaseName;
+ }
+}
// storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb
export function encodeUint32(n) {
@@ -22,21 +48,17 @@ export function openDatabase(name, createObjectStore, version) {
return reqAsPromise(req);
}
-function wrapError(err) {
- return new StorageError(`wrapped DOMException`, err);
-}
-
export function reqAsPromise(req) {
return new Promise((resolve, reject) => {
req.addEventListener("success", event => resolve(event.target.result));
- req.addEventListener("error", event => reject(wrapError(event.target.error)));
+ req.addEventListener("error", event => reject(new WrappedDOMException(event.target)));
});
}
export function txnAsPromise(txn) {
return new Promise((resolve, reject) => {
txn.addEventListener("complete", resolve);
- txn.addEventListener("abort", event => reject(wrapError(event.target.error)));
+ txn.addEventListener("abort", event => reject(new WrappedDOMException(event.target)));
});
}
diff --git a/src/matrix/storage/memory/Storage.js b/src/matrix/storage/memory/Storage.js
index 206c95ca..c1c0fe3c 100644
--- a/src/matrix/storage/memory/Storage.js
+++ b/src/matrix/storage/memory/Storage.js
@@ -1,3 +1,19 @@
+/*
+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 {Transaction} from "./Transaction.js";
import { STORE_MAP, STORE_NAMES } from "../common.js";
diff --git a/src/matrix/storage/memory/Transaction.js b/src/matrix/storage/memory/Transaction.js
index b37a53fc..894db805 100644
--- a/src/matrix/storage/memory/Transaction.js
+++ b/src/matrix/storage/memory/Transaction.js
@@ -1,3 +1,19 @@
+/*
+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 {RoomTimelineStore} from "./stores/RoomTimelineStore.js";
export class Transaction {
diff --git a/src/matrix/storage/memory/stores/RoomTimelineStore.js b/src/matrix/storage/memory/stores/RoomTimelineStore.js
index 6152daa7..5cc31173 100644
--- a/src/matrix/storage/memory/stores/RoomTimelineStore.js
+++ b/src/matrix/storage/memory/stores/RoomTimelineStore.js
@@ -1,3 +1,19 @@
+/*
+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 {SortKey} from "../../room/timeline/SortKey.js";
import {sortedIndex} from "../../../utils/sortedIndex.js";
import {Store} from "./Store.js";
diff --git a/src/matrix/storage/memory/stores/Store.js b/src/matrix/storage/memory/stores/Store.js
index c04ac258..c7218ab8 100644
--- a/src/matrix/storage/memory/stores/Store.js
+++ b/src/matrix/storage/memory/stores/Store.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class Store {
constructor(storeValue, writable) {
this._storeValue = storeValue;
diff --git a/src/mocks/Clock.js b/src/mocks/Clock.js
index 6bbf5a1c..97b7462a 100644
--- a/src/mocks/Clock.js
+++ b/src/mocks/Clock.js
@@ -1,3 +1,19 @@
+/*
+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 {ObservableValue} from "../observable/ObservableValue.js";
class Timeout {
diff --git a/src/observable/BaseObservable.js b/src/observable/BaseObservable.js
index 3afdd4c0..660f3200 100644
--- a/src/observable/BaseObservable.js
+++ b/src/observable/BaseObservable.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export class BaseObservable {
constructor() {
this._handlers = new Set();
diff --git a/src/observable/ObservableValue.js b/src/observable/ObservableValue.js
index b9fa4c4d..f1786dbd 100644
--- a/src/observable/ObservableValue.js
+++ b/src/observable/ObservableValue.js
@@ -1,3 +1,19 @@
+/*
+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 {AbortError} from "../utils/error.js";
import {BaseObservable} from "./BaseObservable.js";
diff --git a/src/observable/index.js b/src/observable/index.js
index 2497a7db..eb6f1579 100644
--- a/src/observable/index.js
+++ b/src/observable/index.js
@@ -1,3 +1,19 @@
+/*
+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 {SortedMapList} from "./list/SortedMapList.js";
import {FilteredMap} from "./map/FilteredMap.js";
import {MappedMap} from "./map/MappedMap.js";
diff --git a/src/observable/list/BaseObservableList.js b/src/observable/list/BaseObservableList.js
index 4f15d02a..b3757a13 100644
--- a/src/observable/list/BaseObservableList.js
+++ b/src/observable/list/BaseObservableList.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservable} from "../BaseObservable.js";
export class BaseObservableList extends BaseObservable {
diff --git a/src/observable/list/ConcatList.js b/src/observable/list/ConcatList.js
index 987dbb80..a51ddd55 100644
--- a/src/observable/list/ConcatList.js
+++ b/src/observable/list/ConcatList.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableList} from "./BaseObservableList.js";
export class ConcatList extends BaseObservableList {
diff --git a/src/observable/list/MappedList.js b/src/observable/list/MappedList.js
index 55b8bd30..139a6e73 100644
--- a/src/observable/list/MappedList.js
+++ b/src/observable/list/MappedList.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableList} from "./BaseObservableList.js";
export class MappedList extends BaseObservableList {
diff --git a/src/observable/list/ObservableArray.js b/src/observable/list/ObservableArray.js
index afbc0144..ca33fcce 100644
--- a/src/observable/list/ObservableArray.js
+++ b/src/observable/list/ObservableArray.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableList} from "./BaseObservableList.js";
export class ObservableArray extends BaseObservableList {
diff --git a/src/observable/list/SortedArray.js b/src/observable/list/SortedArray.js
index 5bb89297..2245dbd9 100644
--- a/src/observable/list/SortedArray.js
+++ b/src/observable/list/SortedArray.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableList} from "./BaseObservableList.js";
import {sortedIndex} from "../../utils/sortedIndex.js";
diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js
index 154febc3..27003cba 100644
--- a/src/observable/list/SortedMapList.js
+++ b/src/observable/list/SortedMapList.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableList} from "./BaseObservableList.js";
import {sortedIndex} from "../../utils/sortedIndex.js";
diff --git a/src/observable/map/BaseObservableMap.js b/src/observable/map/BaseObservableMap.js
index c825af8e..ba376903 100644
--- a/src/observable/map/BaseObservableMap.js
+++ b/src/observable/map/BaseObservableMap.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservable} from "../BaseObservable.js";
export class BaseObservableMap extends BaseObservable {
diff --git a/src/observable/map/FilteredMap.js b/src/observable/map/FilteredMap.js
index 17500ecc..59d8efa1 100644
--- a/src/observable/map/FilteredMap.js
+++ b/src/observable/map/FilteredMap.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableMap} from "./BaseObservableMap.js";
export class FilteredMap extends BaseObservableMap {
diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.js
index 14f46c44..51a23d41 100644
--- a/src/observable/map/MappedMap.js
+++ b/src/observable/map/MappedMap.js
@@ -1,6 +1,22 @@
+/*
+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 {BaseObservableMap} from "./BaseObservableMap.js";
/*
-so a mapped value can emit updates on it's own with this._updater that is passed in the mapping function
+so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate that is passed in the mapping function
how should the mapped value be notified of an update though? and can it then decide to not propagate the update?
*/
export class MappedMap extends BaseObservableMap {
@@ -9,14 +25,18 @@ export class MappedMap extends BaseObservableMap {
this._source = source;
this._mapper = mapper;
this._mappedValues = new Map();
- this._updater = (key, params) => { // this should really be (value, params) but can't make that work for now
- const value = this._mappedValues.get(key);
- this.onUpdate(key, value, params);
- };
+ }
+
+ _emitSpontaneousUpdate(key, params) {
+ const value = this._mappedValues.get(key);
+ if (value) {
+ this.emitUpdate(key, value, params);
+ }
}
onAdd(key, value) {
- const mappedValue = this._mapper(value, this._updater);
+ const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
+ const mappedValue = this._mapper(value, emitSpontaneousUpdate);
this._mappedValues.set(key, mappedValue);
this.emitAdd(key, mappedValue);
}
@@ -31,17 +51,16 @@ export class MappedMap extends BaseObservableMap {
onUpdate(key, value, params) {
const mappedValue = this._mappedValues.get(key);
if (mappedValue !== undefined) {
- const newParams = this._updater(value, params);
- // if (newParams !== undefined) {
- this.emitUpdate(key, mappedValue, newParams);
- // }
+ // TODO: map params somehow if needed?
+ this.emitUpdate(key, mappedValue, params);
}
}
onSubscribeFirst() {
this._subscription = this._source.subscribe(this);
for (let [key, value] of this._source) {
- const mappedValue = this._mapper(value, this._updater);
+ const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
+ const mappedValue = this._mapper(value, emitSpontaneousUpdate);
this._mappedValues.set(key, mappedValue);
}
super.onSubscribeFirst();
diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.js
index cfc366ab..68e64c89 100644
--- a/src/observable/map/ObservableMap.js
+++ b/src/observable/map/ObservableMap.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableMap} from "./BaseObservableMap.js";
export class ObservableMap extends BaseObservableMap {
diff --git a/src/service-worker.template.js b/src/service-worker.template.js
index ee9c54e0..485afba8 100644
--- a/src/service-worker.template.js
+++ b/src/service-worker.template.js
@@ -1,11 +1,31 @@
+/*
+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.
+*/
+
const VERSION = "%%VERSION%%";
-const FILES = "%%FILES%%";
-const cacheName = `brawl-${VERSION}`;
+const OFFLINE_FILES = "%%OFFLINE_FILES%%";
+// TODO: cache these files when requested
+// The difficulty is that these are relative filenames, and we don't have access to document.baseURI
+// Clients.match({type: "window"}).url and assume they are all the same? they really should be ... safari doesn't support this though
+const CACHE_FILES = "%%CACHE_FILES%%";
+const cacheName = `hydrogen-${VERSION}`;
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open(cacheName).then(function(cache) {
- return cache.addAll(FILES);
+ return cache.addAll(OFFLINE_FILES);
})
);
});
diff --git a/src/ui/web/BrawlView.js b/src/ui/web/BrawlView.js
index 27779dc6..ec84c716 100644
--- a/src/ui/web/BrawlView.js
+++ b/src/ui/web/BrawlView.js
@@ -1,3 +1,19 @@
+/*
+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 {SessionView} from "./session/SessionView.js";
import {LoginView} from "./login/LoginView.js";
import {SessionPickerView} from "./login/SessionPickerView.js";
diff --git a/src/ui/web/WebPlatform.js b/src/ui/web/WebPlatform.js
index 453e36e6..22a41323 100644
--- a/src/ui/web/WebPlatform.js
+++ b/src/ui/web/WebPlatform.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export const WebPlatform = {
get minStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
diff --git a/src/ui/web/common.js b/src/ui/web/common.js
index 28faccef..d7ae198b 100644
--- a/src/ui/web/common.js
+++ b/src/ui/web/common.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
export function spinner(t, extraClasses = undefined) {
return t.svg({className: Object.assign({"spinner": true}, extraClasses), viewBox:"0 0 100 100"},
t.circle({cx:"50%", cy:"50%", r:"45%", pathLength:"100"})
diff --git a/src/ui/web/css/avatar.css b/src/ui/web/css/avatar.css
index 70a19bdd..f64d39ae 100644
--- a/src/ui/web/css/avatar.css
+++ b/src/ui/web/css/avatar.css
@@ -1,8 +1,24 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
.avatar {
--avatar-size: 32px;
width: var(--avatar-size);
height: var(--avatar-size);
- border-radius: 100px;
overflow: hidden;
flex-shrink: 0;
-moz-user-select: none;
@@ -13,8 +29,6 @@
font-size: calc(var(--avatar-size) * 0.6);
text-align: center;
letter-spacing: calc(var(--avatar-size) * -0.05);
- background: white;
- color: black;
speak: none;
}
diff --git a/src/ui/web/css/font.css b/src/ui/web/css/font.css
new file mode 100644
index 00000000..f6ef9a29
--- /dev/null
+++ b/src/ui/web/css/font.css
@@ -0,0 +1,15 @@
+/** from https://gist.github.com/mfornos/9991865 */
+
+@font-face {
+ font-family: 'emoji';
+ src: local('Apple Color Emoji'),
+ local('Segoe UI Emoji'),
+ local('Segoe UI Symbol'),
+ local('Noto Color Emoji'),
+ local('Android Emoji'),
+ local('EmojiSymbols'),
+ local('Symbola');
+
+ /* Emoji unicode blocks */
+ unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF;
+}
diff --git a/src/ui/web/css/form.css b/src/ui/web/css/form.css
new file mode 100644
index 00000000..741d2bfd
--- /dev/null
+++ b/src/ui/web/css/form.css
@@ -0,0 +1,22 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.form input {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+}
diff --git a/src/ui/web/css/layout.css b/src/ui/web/css/layout.css
index fe03ec3e..36385a75 100644
--- a/src/ui/web/css/layout.css
+++ b/src/ui/web/css/layout.css
@@ -1,8 +1,32 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
html {
height: 100%;
}
-body {
- margin: 0;
+
+
+@media screen and (min-width: 600px) {
+ .PreSessionScreen {
+ width: 600px;
+ box-sizing: border-box;
+ margin: 0 auto;
+ margin-top: 50px;
+ }
}
.SessionView {
@@ -54,7 +78,7 @@ body {
height: 100%;
}
-.TimelinePanel ul {
+.TimelinePanel .Timeline, .TimelinePanel .TimelineLoadingView {
flex: 1 0 0;
}
diff --git a/src/ui/web/css/left-panel.css b/src/ui/web/css/left-panel.css
index cb3eeca2..9400da4d 100644
--- a/src/ui/web/css/left-panel.css
+++ b/src/ui/web/css/left-panel.css
@@ -1,7 +1,21 @@
+/*
+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.
+*/
+
.LeftPanel {
- background: #333;
- color: white;
overflow-y: auto;
overscroll-behavior: contain;
}
@@ -13,24 +27,10 @@
}
.LeftPanel li {
- margin: 5px;
- padding: 10px;
display: flex;
align-items: center;
}
-.LeftPanel li {
- border-bottom: 1px #555 solid;
-}
-
-.LeftPanel li:last-child {
- border-bottom: none;
-}
-
-.LeftPanel li > * {
- margin-right: 10px;
-}
-
.LeftPanel div.description {
margin: 0;
flex: 1 1 0;
@@ -42,7 +42,3 @@
white-space: nowrap;
text-overflow: ellipsis;
}
-
-.LeftPanel .description .last-message {
- font-size: 0.8em;
-}
diff --git a/src/ui/web/css/login.css b/src/ui/web/css/login.css
index 3a12f849..1032ead0 100644
--- a/src/ui/web/css/login.css
+++ b/src/ui/web/css/login.css
@@ -1,3 +1,54 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/** contains styles for everything before the session view, like the session picker, login, load view, ... */
+
+.SessionPickerView {
+ padding: 0.4em;
+}
+
+.SessionPickerView ul {
+ list-style: none;
+ padding: 0;
+}
+
+.SessionPickerView li {
+ margin: 0.4em 0;
+}
+
+.SessionPickerView .session-info {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.SessionPickerView li .user-id {
+ flex: 1;
+}
+
+.SessionPickerView li .error {
+ margin: 0 20px;
+}
+
+.LoginView {
+ padding: 0.4em;
+}
+
.SessionLoadView {
display: flex;
}
diff --git a/src/ui/web/css/main.css b/src/ui/web/css/main.css
index a6733557..496f76db 100644
--- a/src/ui/web/css/main.css
+++ b/src/ui/web/css/main.css
@@ -1,3 +1,20 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+@import url('font.css');
@import url('layout.css');
@import url('login.css');
@import url('left-panel.css');
@@ -5,15 +22,19 @@
@import url('timeline.css');
@import url('avatar.css');
@import url('spinner.css');
+@import url('form.css');
+@import url('status.css');
-.brawl {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- background-color: black;
- color: white;
+/* only if the body contains the whole app (e.g. we're not embedded in a page), make some changes */
+body.hydrogen {
/* make sure to disable rubber-banding and pull to refresh in a PWA if we'd end up having a scrollbar */
overscroll-behavior: none;
+ /* disable rubberband scrolling on document in IE11 */
+ overflow: hidden;
+}
+
+.hydrogen {
+ margin: 0;
}
.hiddenWithLayout {
@@ -23,78 +44,3 @@
.hidden {
display: none !important;
}
-
-.SessionStatusView {
- display: flex;
- padding: 5px;
- background-color: #555;
-}
-
-.SessionStatusView p {
- margin: 0 10px;
- word-break: break-all;
- word-break: break-word;
-}
-
-.SessionStatusView button {
- border: none;
- background: none;
- color: currentcolor;
- text-decoration: underline;
-}
-
-
-.RoomPlaceholderView {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: row;
-}
-
-.SessionPickerView {
- padding: 0.4em;
-}
-
-.SessionPickerView ul {
- list-style: none;
- padding: 0;
-}
-
-.SessionPickerView li {
- margin: 0.4em 0;
- font-size: 1.2em;
- background-color: grey;
- padding: 0.5em;
-}
-
-.SessionPickerView .sessionInfo {
- cursor: pointer;
- display: flex;
-}
-
-.SessionPickerView li span.userId {
- flex: 1;
-}
-
-.SessionPickerView li span.error {
- margin: 0 20px;
-}
-
-.LoginView {
- padding: 0.4em;
-}
-
-a {
- color: white;
-}
-
-.form > div {
- margin: 0.4em 0;
-}
-
-.form input {
- display: block;
- width: 100%;
- box-sizing: border-box;
-}
-
diff --git a/src/ui/web/css/room.css b/src/ui/web/css/room.css
index 122055e0..6bf01da7 100644
--- a/src/ui/web/css/room.css
+++ b/src/ui/web/css/room.css
@@ -1,7 +1,27 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.RoomPlaceholderView {
+ display: flex;
+ flex-direction: row;
+}
.RoomHeader {
- padding: 10px;
- background-color: #333;
+ align-items: center;
}
.RoomHeader > *:last-child {
@@ -14,16 +34,7 @@
}
.RoomHeader button {
- width: 40px;
- height: 40px;
- display: none;
- font-size: 1.5em;
- padding: 0;
display: block;
- background: white;
- border: none;
- font-weight: bolder;
- line-height: 40px;
}
.RoomHeader .back {
@@ -36,24 +47,11 @@
}
.RoomHeader .topic {
- font-size: 0.8em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
-.back::before {
- content: "☰";
-}
-
-.more::before {
- content: "⋮";
-}
-
-.RoomHeader {
- align-items: center;
-}
-
.RoomHeader .description {
flex: 1 1 auto;
min-width: 0;
@@ -66,14 +64,22 @@
margin: 0;
}
-.RoomView_error {
- color: red;
+.MessageComposer {
+ display: flex;
}
.MessageComposer > input {
display: block;
- width: 100%;
+ flex: 1;
box-sizing: border-box;
- padding: 0.8em;
- border: none;
+}
+
+.TimelineLoadingView {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.TimelineLoadingView div {
+ margin-left: 10px;
}
diff --git a/src/ui/web/css/spinner.css b/src/ui/web/css/spinner.css
index be8192a6..62974da6 100644
--- a/src/ui/web/css/spinner.css
+++ b/src/ui/web/css/spinner.css
@@ -1,3 +1,20 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
@keyframes spinner {
0% {
transform: rotate(0);
@@ -21,6 +38,12 @@
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
+ /**
+ * TODO
+ * see if with IE11 we can just set a static stroke state and make it rotate?
+ */
+ stroke-dasharray: 0 0 85 85;
+
fill: none;
stroke: currentcolor;
stroke-width: 12;
diff --git a/src/ui/web/css/status.css b/src/ui/web/css/status.css
new file mode 100644
index 00000000..de891de1
--- /dev/null
+++ b/src/ui/web/css/status.css
@@ -0,0 +1,33 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.SessionStatusView {
+ display: flex;
+}
+
+.SessionStatusView p {
+ margin: 0 10px;
+ word-break: break-all;
+ word-break: break-word;
+}
+
+.SessionStatusView button {
+ border: none;
+ background: none;
+ color: currentcolor;
+ text-decoration: underline;
+}
diff --git a/src/ui/web/css/themes/README.md b/src/ui/web/css/themes/README.md
new file mode 100644
index 00000000..9078da14
--- /dev/null
+++ b/src/ui/web/css/themes/README.md
@@ -0,0 +1,7 @@
+things that go in the theme:
+ - margin specialization
+ - padding
+ - colors (foreground, background, border, ...)
+ - border-radius
+ - font faces, weights and sizes
+ - alignment
diff --git a/src/ui/web/css/themes/bubbles/theme.css b/src/ui/web/css/themes/bubbles/theme.css
new file mode 100644
index 00000000..d06b49f1
--- /dev/null
+++ b/src/ui/web/css/themes/bubbles/theme.css
@@ -0,0 +1,186 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.hydrogen {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, sans-serif, 'emoji';
+ background-color: black;
+ color: white;
+}
+
+.avatar {
+ border-radius: 100%;
+ background: white;
+ color: black;
+}
+
+.LeftPanel {
+ background: #333;
+ color: white;
+}
+
+.LeftPanel ul {
+ padding: 0;
+ margin: 0;
+}
+
+.LeftPanel li {
+ margin: 5px;
+ padding: 10px;
+ /* vertical align */
+ align-items: center;
+}
+
+.LeftPanel li {
+ border-bottom: 1px #555 solid;
+}
+
+.LeftPanel li:last-child {
+ border-bottom: none;
+}
+
+.LeftPanel li > * {
+ margin-right: 10px;
+}
+
+.LeftPanel .description .last-message {
+ font-size: 0.8em;
+}
+
+a {
+ color: white;
+}
+
+
+.SessionStatusView {
+ padding: 5px;
+ background-color: #555;
+}
+
+.RoomPlaceholderView {
+ align-items: center;
+ justify-content: center;
+}
+
+.SessionPickerView li {
+ font-size: 1.2em;
+ background-color: grey;
+}
+
+
+.RoomHeader {
+ padding: 10px;
+ background-color: #333;
+}
+
+.RoomHeader button {
+ width: 40px;
+ height: 40px;
+ font-size: 1.5em;
+ padding: 0;
+ background: white;
+ border: none;
+ font-weight: bolder;
+ line-height: 40px;
+}
+
+.back::before {
+ content: "☰";
+}
+
+.more::before {
+ content: "⋮";
+}
+
+.RoomHeader .topic {
+ font-size: 0.8em;
+}
+
+.RoomHeader {
+ padding: 10px;
+ background-color: #333;
+}
+
+.RoomView_error {
+ color: red;
+}
+
+.MessageComposer > input {
+ padding: 0.8em;
+ border: none;
+}
+
+.message-container {
+ max-width: 80%;
+ padding: 5px 10px;
+ margin: 5px 10px;
+ background: blue;
+}
+
+.message-container .sender {
+ margin: 5px 0;
+ font-size: 0.9em;
+ font-weight: bold;
+}
+
+.TextMessageView .message-container time {
+ padding: 2px 0 0px 20px;
+ font-size: 0.9em;
+ color: lightblue;
+}
+
+.message-container time {
+ font-size: 0.9em;
+ color: lightblue;
+}
+
+.own time {
+ color: lightgreen;
+}
+
+.own .message-container {
+ background-color: darkgreen;
+}
+
+.TextMessageView.own .message-container {
+ margin-left: auto;
+}
+
+.TextMessageView.pending .message-container {
+ background-color: #333;
+}
+
+.TextMessageView .message-container time {
+ float: right;
+}
+
+.message-container p {
+ margin: 5px 0;
+}
+
+.AnnouncementView {
+ margin: 5px 0;
+ padding: 5px 10%;
+}
+
+.AnnouncementView > div {
+ margin: 0 auto;
+ padding: 10px 20px;
+ background-color: #333;
+ font-size: 0.9em;
+ color: #CCC;
+ text-align: center;
+}
diff --git a/src/ui/web/css/themes/element/element-logo.svg b/src/ui/web/css/themes/element/element-logo.svg
new file mode 100644
index 00000000..7e6c50fb
--- /dev/null
+++ b/src/ui/web/css/themes/element/element-logo.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/ui/web/css/themes/element/icons/chevron-right.svg b/src/ui/web/css/themes/element/icons/chevron-right.svg
new file mode 100644
index 00000000..a7b862aa
--- /dev/null
+++ b/src/ui/web/css/themes/element/icons/chevron-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/ui/web/css/themes/element/icons/send.svg b/src/ui/web/css/themes/element/icons/send.svg
new file mode 100644
index 00000000..b47ab8ea
--- /dev/null
+++ b/src/ui/web/css/themes/element/icons/send.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/ui/web/css/themes/element/inter.css b/src/ui/web/css/themes/element/inter.css
new file mode 100644
index 00000000..5c69d7e6
--- /dev/null
+++ b/src/ui/web/css/themes/element/inter.css
@@ -0,0 +1,152 @@
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 100;
+ font-display: swap;
+ src: url("inter/Inter-Thin.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-Thin.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 100;
+ font-display: swap;
+ src: url("inter/Inter-ThinItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-ThinItalic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 200;
+ font-display: swap;
+ src: url("inter/Inter-ExtraLight.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-ExtraLight.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 200;
+ font-display: swap;
+ src: url("inter/Inter-ExtraLightItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-ExtraLightItalic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url("inter/Inter-Light.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-Light.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 300;
+ font-display: swap;
+ src: url("inter/Inter-LightItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-LightItalic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url("inter/Inter-Regular.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-Regular.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url("inter/Inter-Italic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-Italic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url("inter/Inter-Medium.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-Medium.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url("inter/Inter-MediumItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-MediumItalic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url("inter/Inter-SemiBold.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-SemiBold.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 600;
+ font-display: swap;
+ src: url("inter/Inter-SemiBoldItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-SemiBoldItalic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url("inter/Inter-Bold.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-Bold.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 700;
+ font-display: swap;
+ src: url("inter/Inter-BoldItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-BoldItalic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 800;
+ font-display: swap;
+ src: url("inter/Inter-ExtraBold.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-ExtraBold.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 800;
+ font-display: swap;
+ src: url("inter/Inter-ExtraBoldItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-ExtraBoldItalic.woff?v=3.13") format("woff");
+}
+
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 900;
+ font-display: swap;
+ src: url("inter/Inter-Black.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-Black.woff?v=3.13") format("woff");
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-weight: 900;
+ font-display: swap;
+ src: url("inter/Inter-BlackItalic.woff2?v=3.13") format("woff2"),
+ url("inter/Inter-BlackItalic.woff?v=3.13") format("woff");
+}
diff --git a/src/ui/web/css/themes/element/inter/Inter-Black.woff b/src/ui/web/css/themes/element/inter/Inter-Black.woff
new file mode 100644
index 00000000..07800f4b
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Black.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Black.woff2 b/src/ui/web/css/themes/element/inter/Inter-Black.woff2
new file mode 100644
index 00000000..9a615e6e
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Black.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-BlackItalic.woff b/src/ui/web/css/themes/element/inter/Inter-BlackItalic.woff
new file mode 100644
index 00000000..8790638b
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-BlackItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-BlackItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-BlackItalic.woff2
new file mode 100644
index 00000000..9dfced91
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-BlackItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Bold.woff b/src/ui/web/css/themes/element/inter/Inter-Bold.woff
new file mode 100644
index 00000000..61e1c25e
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Bold.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Bold.woff2 b/src/ui/web/css/themes/element/inter/Inter-Bold.woff2
new file mode 100644
index 00000000..6c401bb0
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Bold.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-BoldItalic.woff b/src/ui/web/css/themes/element/inter/Inter-BoldItalic.woff
new file mode 100644
index 00000000..2de403ed
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-BoldItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-BoldItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-BoldItalic.woff2
new file mode 100644
index 00000000..80efd484
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-BoldItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraBold.woff b/src/ui/web/css/themes/element/inter/Inter-ExtraBold.woff
new file mode 100644
index 00000000..433fb328
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraBold.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraBold.woff2 b/src/ui/web/css/themes/element/inter/Inter-ExtraBold.woff2
new file mode 100644
index 00000000..5a08b364
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraBold.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraBoldItalic.woff b/src/ui/web/css/themes/element/inter/Inter-ExtraBoldItalic.woff
new file mode 100644
index 00000000..bd97df37
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraBoldItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraBoldItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-ExtraBoldItalic.woff2
new file mode 100644
index 00000000..97bda246
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraBoldItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraLight.woff b/src/ui/web/css/themes/element/inter/Inter-ExtraLight.woff
new file mode 100644
index 00000000..39e6f971
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraLight.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraLight.woff2 b/src/ui/web/css/themes/element/inter/Inter-ExtraLight.woff2
new file mode 100644
index 00000000..442e2255
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraLight.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraLightItalic.woff b/src/ui/web/css/themes/element/inter/Inter-ExtraLightItalic.woff
new file mode 100644
index 00000000..a6e1d14b
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraLightItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ExtraLightItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-ExtraLightItalic.woff2
new file mode 100644
index 00000000..db6880b1
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ExtraLightItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Italic.woff b/src/ui/web/css/themes/element/inter/Inter-Italic.woff
new file mode 100644
index 00000000..e7da6663
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Italic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Italic.woff2 b/src/ui/web/css/themes/element/inter/Inter-Italic.woff2
new file mode 100644
index 00000000..8559dfde
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Italic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Light.woff b/src/ui/web/css/themes/element/inter/Inter-Light.woff
new file mode 100644
index 00000000..108c1120
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Light.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Light.woff2 b/src/ui/web/css/themes/element/inter/Inter-Light.woff2
new file mode 100644
index 00000000..80378a31
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Light.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-LightItalic.woff b/src/ui/web/css/themes/element/inter/Inter-LightItalic.woff
new file mode 100644
index 00000000..b30452af
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-LightItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-LightItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-LightItalic.woff2
new file mode 100644
index 00000000..091ff4ed
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-LightItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Medium.woff b/src/ui/web/css/themes/element/inter/Inter-Medium.woff
new file mode 100644
index 00000000..8c36a634
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Medium.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Medium.woff2 b/src/ui/web/css/themes/element/inter/Inter-Medium.woff2
new file mode 100644
index 00000000..3b31d335
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Medium.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-MediumItalic.woff b/src/ui/web/css/themes/element/inter/Inter-MediumItalic.woff
new file mode 100644
index 00000000..fb79e91f
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-MediumItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-MediumItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-MediumItalic.woff2
new file mode 100644
index 00000000..d32c111f
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-MediumItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Regular.woff b/src/ui/web/css/themes/element/inter/Inter-Regular.woff
new file mode 100644
index 00000000..7d587c40
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Regular.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Regular.woff2 b/src/ui/web/css/themes/element/inter/Inter-Regular.woff2
new file mode 100644
index 00000000..d5ffd2a1
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Regular.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-SemiBold.woff b/src/ui/web/css/themes/element/inter/Inter-SemiBold.woff
new file mode 100644
index 00000000..99df06cb
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-SemiBold.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-SemiBold.woff2 b/src/ui/web/css/themes/element/inter/Inter-SemiBold.woff2
new file mode 100644
index 00000000..df746af9
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-SemiBold.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-SemiBoldItalic.woff b/src/ui/web/css/themes/element/inter/Inter-SemiBoldItalic.woff
new file mode 100644
index 00000000..91e192b9
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-SemiBoldItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-SemiBoldItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-SemiBoldItalic.woff2
new file mode 100644
index 00000000..ff8774cc
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-SemiBoldItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Thin.woff b/src/ui/web/css/themes/element/inter/Inter-Thin.woff
new file mode 100644
index 00000000..9d2e3e54
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Thin.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-Thin.woff2 b/src/ui/web/css/themes/element/inter/Inter-Thin.woff2
new file mode 100644
index 00000000..5f7bc37c
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-Thin.woff2 differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ThinItalic.woff b/src/ui/web/css/themes/element/inter/Inter-ThinItalic.woff
new file mode 100644
index 00000000..885d8d5d
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ThinItalic.woff differ
diff --git a/src/ui/web/css/themes/element/inter/Inter-ThinItalic.woff2 b/src/ui/web/css/themes/element/inter/Inter-ThinItalic.woff2
new file mode 100644
index 00000000..2ceec604
Binary files /dev/null and b/src/ui/web/css/themes/element/inter/Inter-ThinItalic.woff2 differ
diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css
new file mode 100644
index 00000000..53ccb2f8
--- /dev/null
+++ b/src/ui/web/css/themes/element/theme.css
@@ -0,0 +1,356 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+@import url('inter.css');
+
+:root {
+ font-size: 10px;
+}
+
+
+.hydrogen {
+ font-family: 'Inter', sans-serif, 'emoji';
+ background-color: white;
+ color: #2e2f32;
+ font-size: 1.4rem;
+ --usercolor1: #368BD6;
+ --usercolor2: #AC3BA8;
+ --usercolor3: #03B381;
+ --usercolor4: #E64F7A;
+ --usercolor5: #FF812D;
+ --usercolor6: #2DC2C5;
+ --usercolor7: #5C56F5;
+ --usercolor8: #74D12C;
+}
+
+.avatar {
+ border-radius: 100%;
+ background: #3D88FA;
+ color: white;
+}
+
+.hydrogen .avatar.usercolor1 { background-color: var(--usercolor1); }
+.hydrogen .avatar.usercolor2 { background-color: var(--usercolor2); }
+.hydrogen .avatar.usercolor3 { background-color: var(--usercolor3); }
+.hydrogen .avatar.usercolor4 { background-color: var(--usercolor4); }
+.hydrogen .avatar.usercolor5 { background-color: var(--usercolor5); }
+.hydrogen .avatar.usercolor6 { background-color: var(--usercolor6); }
+.hydrogen .avatar.usercolor7 { background-color: var(--usercolor7); }
+.hydrogen .avatar.usercolor8 { background-color: var(--usercolor8); }
+
+.logo {
+ height: 48px;
+ min-width: 48px;
+ background-image: url('element-logo.svg');
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+/** buttons */
+.button-row {
+ display: flex;
+}
+.button-row > * {
+ margin-right: 10px;
+}
+.button-row > *:last-child {
+ margin-right: 0px;
+}
+
+.button-row button {
+ margin: 10px 0;
+ flex: 1 0 auto;
+}
+
+.form-row {
+ margin: 12px 0;
+}
+
+.form-row input {
+ padding: 12px;
+ border: 1px solid rgba(141, 151, 165, 0.15);
+ border-radius: 8px;
+ margin-top: 5px;
+ font-size: 1em;
+}
+
+.form-row label, .form-row input {
+ display: block;
+}
+
+button.styled.secondary {
+ color: #03B381;
+}
+
+button.styled.primary {
+ background-color: #03B381;
+ border-radius: 8px;
+ color: white;
+}
+
+button.styled.primary.destructive {
+ background-color: #FF4B55;
+}
+
+button.styled.secondary.destructive {
+ color: #FF4B55;
+}
+
+button.styled {
+ border: none;
+ padding: 10px;
+ background: none;
+ font-weight: 500;
+}
+
+.PreSessionScreen {
+ padding: 30px;
+}
+
+.PreSessionScreen h1 {
+ font-size: 16px;
+ text-align: center;
+}
+
+@media screen and (min-width: 600px) {
+ .PreSessionScreen {
+ box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ }
+}
+
+.PreSessionScreen .logo {
+ height: 48px;
+ min-width: 48px;
+}
+
+.LeftPanel {
+ background: rgba(245, 245, 245, 0.90);
+ font-size: 1.5rem;
+}
+
+.LeftPanel ul {
+ padding: 0;
+ margin: 0;
+}
+
+.LeftPanel li {
+ margin: 3px 10px;
+ padding: 5px;
+ /* vertical align */
+ align-items: center;
+}
+
+.LeftPanel li.active {
+ background: rgba(141, 151, 165, 0.1);
+ border-radius: 5px;
+}
+
+.LeftPanel li > * {
+ margin-right: 10px;
+}
+
+.LeftPanel .description .last-message {
+ font-size: 0.8em;
+}
+
+a {
+ color: inherit;
+}
+
+.SessionStatusView {
+ padding: 5px;
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ background-color: #3D88FA;
+ color: white;
+ border-radius: 10px;
+}
+
+.room-shown .SessionStatusView {
+ top: 72px;
+}
+
+.RoomPlaceholderView {
+ align-items: center;
+ justify-content: center;
+}
+
+.SessionPickerView li {
+ font-size: 1.2em;
+}
+
+.SessionPickerView .session-info {
+ padding: 12px;
+ border: 1px solid rgba(141, 151, 165, 0.15);
+ border-radius: 8px;
+ background-image: url('icons/chevron-right.svg');
+ background-position: center right 30px;
+ background-repeat: no-repeat;
+ font-weight: 500;
+}
+
+.SessionPickerView .session-actions {
+ margin: 10px 0 20px 0;
+ display: flex;
+}
+
+.SessionPickerView .session-actions > * {
+ margin-right: 10px;
+}
+.SessionPickerView .session-actions > *:last-child {
+ margin-right: 0px;
+}
+
+.SessionPickerView .session-actions button {
+ border: none;
+ background: none;
+ color: inherit;
+}
+
+.SessionPickerView button.destructive {
+ color: #FF4B55;
+}
+
+.RoomHeader {
+ background: rgba(245, 245, 245, 0.90);
+ padding: 10px;
+}
+
+.RoomHeader h2 {
+ font-size: 1.8rem;
+ font-weight: 600;
+}
+
+.RoomHeader button {
+ width: 40px;
+ height: 40px;
+ font-size: 1.5em;
+ padding: 0;
+ background: white;
+ border: none;
+ font-weight: bolder;
+ line-height: 40px;
+}
+
+.back::before {
+ content: "☰";
+}
+
+.more::before {
+ content: "⋮";
+}
+
+.RoomHeader .topic {
+ font-size: 14rem;
+}
+
+.RoomView_error {
+ color: red;
+}
+
+.MessageComposer {
+ border-top: 1px solid rgba(245, 245, 245, 0.90);
+}
+
+.MessageComposer > input {
+ padding: 0.8em;
+ border: none;
+}
+
+.MessageComposer > button.send {
+ margin: 8px;
+ width: 32px;
+ height: 32px;
+ display: block;
+ border-radius: 100%;
+ border: none;
+ text-indent: 200%;
+ overflow: hidden;
+
+ background-color: #03B381;
+ background-image: url('icons/send.svg');
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.MessageComposer > button.send:disabled {
+ background-color: #E3E8F0;
+}
+
+ul.Timeline > li:not(.continuation) {
+ margin-top: 7px;
+}
+
+ul.Timeline > li.continuation .sender {
+ display: none;
+}
+
+.message-container {
+ padding: 1px 10px 0px 10px;
+ margin: 5px 10px 0 10px;
+}
+
+.TextMessageView.continuation .message-container {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.message-container .sender {
+ margin: 6px 0;
+ font-weight: bold;
+ line-height: 1.7rem;
+}
+
+.hydrogen .sender.usercolor1 { color: var(--usercolor1); }
+.hydrogen .sender.usercolor2 { color: var(--usercolor2); }
+.hydrogen .sender.usercolor3 { color: var(--usercolor3); }
+.hydrogen .sender.usercolor4 { color: var(--usercolor4); }
+.hydrogen .sender.usercolor5 { color: var(--usercolor5); }
+.hydrogen .sender.usercolor6 { color: var(--usercolor6); }
+.hydrogen .sender.usercolor7 { color: var(--usercolor7); }
+.hydrogen .sender.usercolor8 { color: var(--usercolor8); }
+
+.message-container time {
+ padding: 2px 0 0px 10px;
+ font-size: 0.8em;
+ line-height: normal;
+ color: #aaa;
+}
+
+.TextMessageView.pending .message-container {
+ color: #ccc;
+}
+
+.message-container p {
+ margin: 3px 0;
+ line-height: 2.2rem;
+}
+
+.AnnouncementView {
+ margin: 5px 0;
+ padding: 5px 10%;
+}
+
+.AnnouncementView > div {
+ margin: 0 auto;
+ padding: 10px 20px;
+ background-color: rgba(245, 245, 245, 0.90);
+ text-align: center;
+ border-radius: 10px;
+}
diff --git a/src/ui/web/css/timeline.css b/src/ui/web/css/timeline.css
index cc9c36ba..14b60b26 100644
--- a/src/ui/web/css/timeline.css
+++ b/src/ui/web/css/timeline.css
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
.TimelinePanel ul {
overflow-y: auto;
@@ -12,10 +28,6 @@
.message-container {
flex: 0 1 auto;
- max-width: 80%;
- padding: 5px 10px;
- margin: 5px 10px;
- background: blue;
/* first try break-all, then break-word, which isn't supported everywhere */
word-break: break-all;
word-break: break-word;
@@ -23,14 +35,25 @@
.message-container .sender {
margin: 5px 0;
- font-size: 0.9em;
- font-weight: bold;
+}
+
+.message-container a {
+ display: block;
+ position: relative;
+ max-width: 100%;
+ /* width and padding-top set inline to maintain aspect ratio,
+ replace with css aspect-ratio once supported */
}
.message-container img {
display: block;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
width: 100%;
- height: auto;
+ height: 100%;
}
.TextMessageView {
@@ -38,50 +61,22 @@
min-width: 0;
}
-.TextMessageView.own .message-container {
- margin-left: auto;
-}
-
-.TextMessageView .message-container time {
- float: right;
- padding: 2px 0 0px 20px;
- font-size: 0.9em;
- color: lightblue;
-}
-
-.message-container time {
- font-size: 0.9em;
- color: lightblue;
-}
-
-.own time {
- color: lightgreen;
-}
-
-.own .message-container {
- background-color: darkgreen;
-}
-
-.TextMessageView.pending .message-container {
- background-color: #333;
-}
-
-.message-container p {
- margin: 5px 0;
-}
-
.AnnouncementView {
- margin: 5px 0;
- padding: 5px 10%;
display: flex;
align-items: center;
}
-.AnnouncementView > div {
- margin: 0 auto;
+.GapView {
+ visibility: hidden;
+ display: flex;
padding: 10px 20px;
- background-color: #333;
- font-size: 0.9em;
- color: #CCC;
- text-align: center;
+}
+
+.GapView.isLoading {
+ visibility: visible;
+}
+
+.GapView > div {
+ flex: 1;
+ margin-left: 10px;
}
diff --git a/src/ui/web/dom/Clock.js b/src/ui/web/dom/Clock.js
index f5cc6ed0..7e64de47 100644
--- a/src/ui/web/dom/Clock.js
+++ b/src/ui/web/dom/Clock.js
@@ -1,3 +1,19 @@
+/*
+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 {AbortError} from "../../../utils/error.js";
class Timeout {
diff --git a/src/ui/web/dom/OnlineStatus.js b/src/ui/web/dom/OnlineStatus.js
index 1cd3a9b5..e1d7843a 100644
--- a/src/ui/web/dom/OnlineStatus.js
+++ b/src/ui/web/dom/OnlineStatus.js
@@ -1,3 +1,19 @@
+/*
+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 {BaseObservableValue} from "../../../observable/ObservableValue.js";
export class OnlineStatus extends BaseObservableValue {
diff --git a/src/ui/web/general/ListView.js b/src/ui/web/general/ListView.js
index 011f2f1c..cb5a3298 100644
--- a/src/ui/web/general/ListView.js
+++ b/src/ui/web/general/ListView.js
@@ -1,3 +1,19 @@
+/*
+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 {tag} from "./html.js";
function insertAt(parentNode, idx, childNode) {
@@ -86,12 +102,14 @@ export class ListView {
}
this._subscription = this._list.subscribe(this);
this._childInstances = [];
+ const fragment = document.createDocumentFragment();
for (let item of this._list) {
const child = this._childCreator(item);
this._childInstances.push(child);
const childDomNode = child.mount(this._mountArgs);
- this._root.appendChild(childDomNode);
+ fragment.appendChild(childDomNode);
}
+ this._root.appendChild(fragment);
}
onAdd(idx, value) {
diff --git a/src/ui/web/general/SwitchView.js b/src/ui/web/general/SwitchView.js
index 66950052..ae273265 100644
--- a/src/ui/web/general/SwitchView.js
+++ b/src/ui/web/general/SwitchView.js
@@ -1,3 +1,19 @@
+/*
+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 {errorToDOM} from "./error.js";
export class SwitchView {
@@ -31,7 +47,7 @@ export class SwitchView {
} catch (err) {
newRoot = errorToDOM(err);
}
- const parent = oldRoot.parentElement;
+ const parent = oldRoot.parentNode;
if (parent) {
parent.replaceChild(newRoot, oldRoot);
}
diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js
index 8cdccdf1..4e897cf0 100644
--- a/src/ui/web/general/TemplateView.js
+++ b/src/ui/web/general/TemplateView.js
@@ -1,3 +1,19 @@
+/*
+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 { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html.js";
import {errorToDOM} from "./error.js";
@@ -226,8 +242,8 @@ class TemplateBuilder {
if (prevValue !== newValue) {
prevValue = newValue;
const newNode = renderNode(node);
- if (node.parentElement) {
- node.parentElement.replaceChild(newNode, node);
+ if (node.parentNode) {
+ node.parentNode.replaceChild(newNode, node);
}
node = newNode;
}
diff --git a/src/ui/web/general/error.js b/src/ui/web/general/error.js
index c218504e..d72275fc 100644
--- a/src/ui/web/general/error.js
+++ b/src/ui/web/general/error.js
@@ -1,3 +1,19 @@
+/*
+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 {tag} from "./html.js";
export function errorToDOM(error) {
diff --git a/src/ui/web/general/html.js b/src/ui/web/general/html.js
index 7cb001c0..846539c7 100644
--- a/src/ui/web/general/html.js
+++ b/src/ui/web/general/html.js
@@ -1,3 +1,19 @@
+/*
+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.
+*/
+
// DOM helper functions
export function isChildren(children) {
@@ -78,7 +94,7 @@ export const TAG_NAMES = {
[HTML_NS]: [
"a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
- "pre", "button", "time", "input", "textarea"],
+ "pre", "button", "time", "input", "textarea", "label"],
[SVG_NS]: ["svg", "circle"]
};
diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js
index 18d99f65..ef2afbb6 100644
--- a/src/ui/web/login/LoginView.js
+++ b/src/ui/web/login/LoginView.js
@@ -1,26 +1,69 @@
+/*
+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 {TemplateView} from "../general/TemplateView.js";
-import {brawlGithubLink} from "./common.js";
+import {hydrogenGithubLink} from "./common.js";
import {SessionLoadView} from "./SessionLoadView.js";
export class LoginView extends TemplateView {
render(t, vm) {
const disabled = vm => !!vm.isBusy;
- const username = t.input({type: "text", placeholder: vm.i18n`Username`, disabled});
- const password = t.input({type: "password", placeholder: vm.i18n`Password`, disabled});
- const homeserver = t.input({type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, disabled});
- return t.div({className: "LoginView form"}, [
- t.h1([vm.i18n`Log in to your homeserver`]),
- t.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))),
- t.div(username),
- t.div(password),
- t.div(homeserver),
- t.div(t.button({
- onClick: () => vm.login(username.value, password.value, homeserver.value),
- disabled
- }, vm.i18n`Log In`)),
- t.div(t.button({onClick: () => vm.cancel(), disabled}, [vm.i18n`Pick an existing session`])),
- t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadView(loadViewModel) : null),
- t.p(brawlGithubLink(t))
+ const username = t.input({
+ id: "username",
+ type: "text",
+ placeholder: vm.i18n`Username`,
+ disabled
+ });
+ const password = t.input({
+ id: "password",
+ type: "password",
+ placeholder: vm.i18n`Password`,
+ disabled
+ });
+ const homeserver = t.input({
+ id: "homeserver",
+ type: "text",
+ placeholder: vm.i18n`Your matrix homeserver`,
+ value: vm.defaultHomeServer,
+ disabled
+ });
+
+ return t.div({className: "PreSessionScreen"}, [
+ t.div({className: "logo"}),
+ t.div({className: "LoginView form"}, [
+ t.h1([vm.i18n`Sign In`]),
+ t.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))),
+ t.div({className: "form-row"}, [t.label({for: "username"}, vm.i18n`Username`), username]),
+ t.div({className: "form-row"}, [t.label({for: "password"}, vm.i18n`Password`), password]),
+ t.div({className: "form-row"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]),
+ t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadView(loadViewModel) : null),
+ t.div({className: "button-row"}, [
+ t.button({
+ className: "styled secondary",
+ onClick: () => vm.cancel(), disabled
+ }, [vm.i18n`Go Back`]),
+ t.button({
+ className: "styled primary",
+ onClick: () => vm.login(username.value, password.value, homeserver.value),
+ disabled
+ }, vm.i18n`Log In`),
+ ]),
+ // use t.mapView rather than t.if to create a new view when the view model changes too
+ t.p(hydrogenGithubLink(t))
+ ])
]);
}
}
diff --git a/src/ui/web/login/SessionLoadView.js b/src/ui/web/login/SessionLoadView.js
index 5340844b..637c204c 100644
--- a/src/ui/web/login/SessionLoadView.js
+++ b/src/ui/web/login/SessionLoadView.js
@@ -1,3 +1,19 @@
+/*
+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 {TemplateView} from "../general/TemplateView.js";
import {spinner} from "../common.js";
diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js
index 057b6563..8b051dcc 100644
--- a/src/ui/web/login/SessionPickerView.js
+++ b/src/ui/web/login/SessionPickerView.js
@@ -1,6 +1,22 @@
+/*
+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 {ListView} from "../general/ListView.js";
import {TemplateView} from "../general/TemplateView.js";
-import {brawlGithubLink} from "./common.js";
+import {hydrogenGithubLink} from "./common.js";
import {SessionLoadView} from "./SessionLoadView.js";
function selectFileAsText(mimeType) {
@@ -36,9 +52,10 @@ class SessionPickerItemView extends TemplateView {
render(t, vm) {
const deleteButton = t.button({
+ className: "destructive",
disabled: vm => vm.isDeleting,
onClick: this._onDeleteClick.bind(this),
- }, "Delete");
+ }, "Sign Out");
const clearButton = t.button({
disabled: vm => vm.isClearing,
onClick: () => vm.clear(),
@@ -54,17 +71,20 @@ class SessionPickerItemView extends TemplateView {
onClick: () => setTimeout(() => vm.clearExport(), 100),
}, "Download");
}));
-
- const userName = t.span({className: "userId"}, vm => vm.label);
- const errorMessage = t.if(vm => vm.error, t.createTemplate(t => t.span({className: "error"}, vm => vm.error)));
- return t.li([t.div({className: "sessionInfo"}, [
- userName,
- errorMessage,
- downloadExport,
- exportButton,
- clearButton,
- deleteButton,
- ])]);
+ const errorMessage = t.if(vm => vm.error, t.createTemplate(t => t.p({className: "error"}, vm => vm.error)));
+ return t.li([
+ t.div({className: "session-info"}, [
+ t.div({className: `avatar usercolor${vm.avatarColorNumber}`}, vm => vm.avatarInitials),
+ t.div({className: "user-id"}, vm => vm.label),
+ ]),
+ t.div({className: "session-actions"}, [
+ deleteButton,
+ exportButton,
+ downloadExport,
+ clearButton,
+ ]),
+ errorMessage
+ ]);
}
}
@@ -73,7 +93,7 @@ export class SessionPickerView extends TemplateView {
const sessionList = new ListView({
list: vm.sessions,
onItemClick: (item, event) => {
- if (event.target.closest(".userId")) {
+ if (event.target.closest(".session-info")) {
vm.pick(item.value.id);
}
},
@@ -82,13 +102,24 @@ export class SessionPickerView extends TemplateView {
return new SessionPickerItemView(sessionInfo);
});
- return t.div({className: "SessionPickerView"}, [
- t.h1(["Pick a session"]),
- t.view(sessionList),
- t.p(t.button({onClick: () => vm.cancel()}, ["Log in to a new session instead"])),
- t.p(t.button({onClick: async () => vm.import(await selectFileAsText("application/json"))}, "Import")),
- t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)),
- t.p(brawlGithubLink(t))
+ return t.div({className: "PreSessionScreen"}, [
+ t.div({className: "logo"}),
+ t.div({className: "SessionPickerView"}, [
+ t.h1(["Continue as …"]),
+ t.view(sessionList),
+ t.div({className: "button-row"}, [
+ t.button({
+ className: "styled secondary",
+ onClick: async () => vm.import(await selectFileAsText("application/json"))
+ }, vm.i18n`Import a session`),
+ t.button({
+ className: "styled primary",
+ onClick: () => vm.cancel()
+ }, vm.i18n`Sign In`)
+ ]),
+ t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)),
+ t.p(hydrogenGithubLink(t))
+ ])
]);
}
}
diff --git a/src/ui/web/login/common.js b/src/ui/web/login/common.js
index ce728fef..f1e6496f 100644
--- a/src/ui/web/login/common.js
+++ b/src/ui/web/login/common.js
@@ -1,7 +1,26 @@
-export function brawlGithubLink(t) {
- if (window.BRAWL_VERSION) {
- return t.a({target: "_blank", href: `https://github.com/bwindels/brawl-chat/releases/tag/v${window.BRAWL_VERSION}`}, `Brawl v${window.BRAWL_VERSION} on Github`);
+/*
+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.
+*/
+
+export function hydrogenGithubLink(t) {
+ if (window.HYDROGEN_VERSION) {
+ return t.a({target: "_blank",
+ href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${window.HYDROGEN_VERSION}`},
+ `Hydrogen v${window.HYDROGEN_VERSION} on Github`);
} else {
- return t.a({target: "_blank", href: "https://github.com/bwindels/brawl-chat"}, "Brawl on Github");
+ return t.a({target: "_blank", href: "https://github.com/vector-im/hydrogen-web"},
+ "Hydrogen on Github");
}
}
diff --git a/src/ui/web/session/RoomPlaceholderView.js b/src/ui/web/session/RoomPlaceholderView.js
index 3a8f3e27..3b79b7ef 100644
--- a/src/ui/web/session/RoomPlaceholderView.js
+++ b/src/ui/web/session/RoomPlaceholderView.js
@@ -1,3 +1,19 @@
+/*
+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 {tag} from "../general/html.js";
export class RoomPlaceholderView {
diff --git a/src/ui/web/session/RoomTile.js b/src/ui/web/session/RoomTile.js
index c7f3edd9..9268bfe5 100644
--- a/src/ui/web/session/RoomTile.js
+++ b/src/ui/web/session/RoomTile.js
@@ -1,9 +1,25 @@
+/*
+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 {TemplateView} from "../general/TemplateView.js";
export class RoomTile extends TemplateView {
- render(t) {
- return t.li([
- t.div({className: "avatar medium"}, vm => vm.avatarInitials),
+ render(t, vm) {
+ return t.li({"className": {"active": vm => vm.isOpen}}, [
+ t.div({className: `avatar medium usercolor${vm.avatarColorNumber}`}, vm => vm.avatarInitials),
t.div({className: "description"}, t.div({className: "name"}, vm => vm.name))
]);
}
diff --git a/src/ui/web/session/SessionStatusView.js b/src/ui/web/session/SessionStatusView.js
index 84c44dd7..648ef232 100644
--- a/src/ui/web/session/SessionStatusView.js
+++ b/src/ui/web/session/SessionStatusView.js
@@ -1,3 +1,19 @@
+/*
+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 {TemplateView} from "../general/TemplateView.js";
import {spinner} from "../common.js";
diff --git a/src/ui/web/session/SessionView.js b/src/ui/web/session/SessionView.js
index f7925f31..73e6bf98 100644
--- a/src/ui/web/session/SessionView.js
+++ b/src/ui/web/session/SessionView.js
@@ -1,3 +1,19 @@
+/*
+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 {ListView} from "../general/ListView.js";
import {RoomTile} from "./RoomTile.js";
import {RoomView} from "./room/RoomView.js";
diff --git a/src/ui/web/session/room/MessageComposer.js b/src/ui/web/session/room/MessageComposer.js
index 13211965..e07e77e3 100644
--- a/src/ui/web/session/room/MessageComposer.js
+++ b/src/ui/web/session/room/MessageComposer.js
@@ -1,3 +1,19 @@
+/*
+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 {TemplateView} from "../../general/TemplateView.js";
export class MessageComposer extends TemplateView {
@@ -6,19 +22,32 @@ export class MessageComposer extends TemplateView {
this._input = null;
}
- render(t) {
+ render(t, vm) {
this._input = t.input({
placeholder: "Send a message ...",
- onKeydown: e => this._onKeyDown(e)
+ onKeydown: e => this._onKeyDown(e),
+ onInput: () => vm.setInput(this._input.value),
});
- return t.div({className: "MessageComposer"}, [this._input]);
+ return t.div({className: "MessageComposer"}, [
+ this._input,
+ t.button({
+ className: "send",
+ title: vm.i18n`Send`,
+ disabled: vm => !vm.canSend,
+ onClick: () => this._trySend(),
+ }, vm.i18n`Send`),
+ ]);
+ }
+
+ _trySend() {
+ if (this.value.sendMessage(this._input.value)) {
+ this._input.value = "";
+ }
}
_onKeyDown(event) {
if (event.key === "Enter") {
- if (this.value.sendMessage(this._input.value)) {
- this._input.value = "";
- }
+ this._trySend();
}
}
}
diff --git a/src/ui/web/session/room/RoomView.js b/src/ui/web/session/room/RoomView.js
index fbf6bd10..7a4abd45 100644
--- a/src/ui/web/session/room/RoomView.js
+++ b/src/ui/web/session/room/RoomView.js
@@ -1,35 +1,44 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import {TemplateView} from "../../general/TemplateView.js";
import {TimelineList} from "./TimelineList.js";
+import {TimelineLoadingView} from "./TimelineLoadingView.js";
import {MessageComposer} from "./MessageComposer.js";
export class RoomView extends TemplateView {
- constructor(viewModel) {
- super(viewModel);
- this._timelineList = null;
- }
-
render(t, vm) {
- this._timelineList = new TimelineList();
return t.div({className: "RoomView"}, [
t.div({className: "TimelinePanel"}, [
t.div({className: "RoomHeader"}, [
t.button({className: "back", onClick: () => vm.close()}),
- t.div({className: "avatar large"}, vm => vm.avatarInitials),
+ t.div({className: `avatar usercolor${vm.avatarColorNumber}`}, vm => vm.avatarInitials),
t.div({className: "room-description"}, [
t.h2(vm => vm.name),
]),
]),
t.div({className: "RoomView_error"}, vm => vm.error),
- t.view(this._timelineList),
+ t.mapView(vm => vm.timelineViewModel, timelineViewModel => {
+ return timelineViewModel ?
+ new TimelineList(timelineViewModel) :
+ new TimelineLoadingView(vm); // vm is just needed for i18n
+ }),
t.view(new MessageComposer(this.value.composerViewModel)),
])
]);
}
-
- update(value, prop) {
- super.update(value, prop);
- if (prop === "timelineViewModel") {
- this._timelineList.update({viewModel: this.value.timelineViewModel});
- }
- }
}
diff --git a/src/ui/web/session/room/TimelineList.js b/src/ui/web/session/room/TimelineList.js
index 1d8bfd47..8838963c 100644
--- a/src/ui/web/session/room/TimelineList.js
+++ b/src/ui/web/session/room/TimelineList.js
@@ -1,3 +1,19 @@
+/*
+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 {ListView} from "../../general/ListView.js";
import {GapView} from "./timeline/GapView.js";
import {TextMessageView} from "./timeline/TextMessageView.js";
@@ -5,8 +21,11 @@ import {ImageView} from "./timeline/ImageView.js";
import {AnnouncementView} from "./timeline/AnnouncementView.js";
export class TimelineList extends ListView {
- constructor(options = {}) {
- options.className = "Timeline";
+ constructor(viewModel) {
+ const options = {
+ className: "Timeline",
+ list: viewModel.tiles,
+ }
super(options, entry => {
switch (entry.shape) {
case "gap": return new GapView(entry);
@@ -18,28 +37,48 @@ export class TimelineList extends ListView {
this._atBottom = false;
this._onScroll = this._onScroll.bind(this);
this._topLoadingPromise = null;
- this._viewModel = null;
+ this._viewModel = viewModel;
}
- async _onScroll() {
- const root = this.root();
- if (root.scrollTop === 0 && !this._topLoadingPromise && this._viewModel) {
- const beforeFromBottom = this._distanceFromBottom();
- this._topLoadingPromise = this._viewModel.loadAtTop();
- await this._topLoadingPromise;
- const fromBottom = this._distanceFromBottom();
- const amountGrown = fromBottom - beforeFromBottom;
- root.scrollTop = root.scrollTop + amountGrown;
+ async _loadAtTopWhile(predicate) {
+ if (this._topLoadingPromise) {
+ return;
+ }
+ try {
+ while (predicate()) {
+ // fill, not enough content to fill timeline
+ this._topLoadingPromise = this._viewModel.loadAtTop();
+ const startReached = await this._topLoadingPromise;
+ if (startReached) {
+ break;
+ }
+ }
+ }
+ catch (err) {
+ //ignore error, as it is handled in the VM
+ }
+ finally {
this._topLoadingPromise = null;
}
}
- update(attributes) {
- if(attributes.viewModel) {
- this._viewModel = attributes.viewModel;
- attributes.list = attributes.viewModel.tiles;
+ async _onScroll() {
+ const PAGINATE_OFFSET = 100;
+ const root = this.root();
+ if (root.scrollTop < PAGINATE_OFFSET && !this._topLoadingPromise && this._viewModel) {
+ // to calculate total amountGrown to check when we stop loading
+ let beforeContentHeight = root.scrollHeight;
+ // to adjust scrollTop every time
+ let lastContentHeight = beforeContentHeight;
+ // load until pagination offset is reached again
+ this._loadAtTopWhile(() => {
+ const contentHeight = root.scrollHeight;
+ const amountGrown = contentHeight - beforeContentHeight;
+ root.scrollTop = root.scrollTop + (contentHeight - lastContentHeight);
+ lastContentHeight = contentHeight;
+ return amountGrown < PAGINATE_OFFSET;
+ });
}
- super.update(attributes);
}
mount() {
@@ -53,10 +92,21 @@ export class TimelineList extends ListView {
super.unmount();
}
- loadList() {
+ async loadList() {
super.loadList();
const root = this.root();
- root.scrollTop = root.scrollHeight;
+ // yield so the browser can render the list
+ // and we can measure the content below
+ await Promise.resolve();
+ const {scrollHeight, clientHeight} = root;
+ if (scrollHeight > clientHeight) {
+ root.scrollTop = root.scrollHeight;
+ }
+ // load while viewport is not filled
+ this._loadAtTopWhile(() => {
+ const {scrollHeight, clientHeight} = root;
+ return scrollHeight <= clientHeight;
+ });
}
onBeforeListChanged() {
@@ -70,8 +120,8 @@ export class TimelineList extends ListView {
}
onListChanged() {
+ const root = this.root();
if (this._atBottom) {
- const root = this.root();
root.scrollTop = root.scrollHeight;
}
}
diff --git a/src/ui/web/session/room/TimelineLoadingView.js b/src/ui/web/session/room/TimelineLoadingView.js
new file mode 100644
index 00000000..88d07f43
--- /dev/null
+++ b/src/ui/web/session/room/TimelineLoadingView.js
@@ -0,0 +1,27 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {TemplateView} from "../../general/TemplateView.js";
+import {spinner} from "../../common.js";
+
+export class TimelineLoadingView extends TemplateView {
+ render(t, vm) {
+ return t.div({className: "TimelineLoadingView"}, [
+ spinner(t),
+ t.div(vm.i18n`Loading messages…`)
+ ]);
+ }
+}
diff --git a/src/ui/web/session/room/timeline/AnnouncementView.js b/src/ui/web/session/room/timeline/AnnouncementView.js
index b377a1c0..43eb91ea 100644
--- a/src/ui/web/session/room/timeline/AnnouncementView.js
+++ b/src/ui/web/session/room/timeline/AnnouncementView.js
@@ -1,3 +1,19 @@
+/*
+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 {TemplateView} from "../../../general/TemplateView.js";
export class AnnouncementView extends TemplateView {
diff --git a/src/ui/web/session/room/timeline/GapView.js b/src/ui/web/session/room/timeline/GapView.js
index 5687ce58..2b23ae3c 100644
--- a/src/ui/web/session/room/timeline/GapView.js
+++ b/src/ui/web/session/room/timeline/GapView.js
@@ -1,4 +1,21 @@
+/*
+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 {TemplateView} from "../../../general/TemplateView.js";
+import {spinner} from "../../../common.js";
export class GapView extends TemplateView {
render(t, vm) {
@@ -6,12 +23,9 @@ export class GapView extends TemplateView {
GapView: true,
isLoading: vm => vm.isLoading
};
- const label = (vm.isUp ? "🠝" : "🠟") + " fill gap"; //no binding
return t.li({className}, [
- t.button({
- onClick: () => vm.fill(),
- disabled: vm => vm.isLoading
- }, label),
+ spinner(t),
+ t.div(vm.i18n`Loading more messages …`),
t.if(vm => vm.error, t.createTemplate(t => t.strong(vm => vm.error)))
]);
}
diff --git a/src/ui/web/session/room/timeline/ImageView.js b/src/ui/web/session/room/timeline/ImageView.js
index c394ab2a..4770510c 100644
--- a/src/ui/web/session/room/timeline/ImageView.js
+++ b/src/ui/web/session/room/timeline/ImageView.js
@@ -1,27 +1,41 @@
+/*
+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 {TemplateView} from "../../../general/TemplateView.js";
+import {renderMessage} from "./common.js";
export class ImageView extends TemplateView {
render(t, vm) {
+ // replace with css aspect-ratio once supported
+ const heightRatioPercent = (vm.thumbnailHeight / vm.thumbnailWidth) * 100;
const image = t.img({
src: vm.thumbnailUrl,
width: vm.thumbnailWidth,
height: vm.thumbnailHeight,
loading: "lazy",
- style: `max-width: ${vm.thumbnailWidth}px`,
alt: vm.label,
});
const linkContainer = t.a({
href: vm.url,
- target: "_blank"
+ target: "_blank",
+ style: `padding-top: ${heightRatioPercent}%; width: ${vm.thumbnailWidth}px;`
}, image);
- return t.li(
- {className: {"TextMessageView": true, own: vm.isOwn, pending: vm.isPending}},
- t.div({className: "message-container"}, [
- t.div({className: "sender"}, vm => vm.isContinuation ? "" : vm.sender),
- t.div(linkContainer),
- t.p(t.time(vm.date + " " + vm.time)),
- ])
+ return renderMessage(t, vm,
+ [t.div(linkContainer), t.p(t.time(vm.date + " " + vm.time))]
);
}
}
diff --git a/src/ui/web/session/room/timeline/TextMessageView.js b/src/ui/web/session/room/timeline/TextMessageView.js
index d3748d35..d3d8e167 100644
--- a/src/ui/web/session/room/timeline/TextMessageView.js
+++ b/src/ui/web/session/room/timeline/TextMessageView.js
@@ -1,13 +1,26 @@
+/*
+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 {TemplateView} from "../../../general/TemplateView.js";
+import {renderMessage} from "./common.js";
export class TextMessageView extends TemplateView {
render(t, vm) {
- return t.li(
- {className: {"TextMessageView": true, own: vm.isOwn, pending: vm.isPending}},
- t.div({className: "message-container"}, [
- t.div({className: "sender"}, vm => vm.isContinuation ? "" : vm.sender),
- t.p([vm.text, t.time(vm.date + " " + vm.time)]),
- ])
+ return renderMessage(t, vm,
+ [t.p([vm.text, t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time)])]
);
}
}
diff --git a/src/ui/web/session/room/timeline/TimelineTile.js b/src/ui/web/session/room/timeline/TimelineTile.js
index 003f3191..9de10017 100644
--- a/src/ui/web/session/room/timeline/TimelineTile.js
+++ b/src/ui/web/session/room/timeline/TimelineTile.js
@@ -1,3 +1,19 @@
+/*
+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 {tag} from "../../../general/html.js";
export class TimelineTile {
diff --git a/src/ui/web/session/room/timeline/common.js b/src/ui/web/session/room/timeline/common.js
new file mode 100644
index 00000000..18bf0be0
--- /dev/null
+++ b/src/ui/web/session/room/timeline/common.js
@@ -0,0 +1,31 @@
+/*
+Copyright 2020 Bruno Windels
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export function renderMessage(t, vm, children) {
+ const classes = {
+ "TextMessageView": true,
+ own: vm.isOwn,
+ pending: vm.isPending,
+ continuation: vm => vm.isContinuation,
+ };
+ const sender = t.div({className: `sender usercolor${vm.senderColorNumber}`}, vm.sender);
+ children = [sender].concat(children);
+ return t.li(
+ {className: classes},
+ t.div({className: "message-container"}, children)
+ );
+}
diff --git a/src/ui/web/view-gallery.html b/src/ui/web/view-gallery.html
index 4cd8fd79..43827afb 100644
--- a/src/ui/web/view-gallery.html
+++ b/src/ui/web/view-gallery.html
@@ -3,6 +3,7 @@
+