Merge branch 'master' into bwindels/memberlist

This commit is contained in:
Bruno Windels 2020-08-19 12:13:38 +02:00
commit 9ff4f3839c
195 changed files with 8062 additions and 609 deletions

View file

@ -5,10 +5,10 @@ module.exports = {
}, },
"extends": "eslint:recommended", "extends": "eslint:recommended",
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018, "ecmaVersion": 2020,
"sourceType": "module" "sourceType": "module"
}, },
"rules": { "rules": {
"no-console": "off" "no-console": "off"
} }
}; };

177
LICENSE Normal file
View file

@ -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

View file

@ -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 ## 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. 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.
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)
## Why ## 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), 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.
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.
If you find this interesting, feel free to reach me at `@bwindels:matrix.org`. If you find this interesting, feel free to reach me at `@bwindels:matrix.org`.
# How to use # 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`.

13
TODO.md Normal file
View file

@ -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?

View file

@ -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... 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

View file

@ -3,49 +3,28 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="application-name" content="Brawl Chat"/> <meta name="application-name" content="Hydrogen Chat"/>
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Brawl Chat"> <meta name="apple-mobile-web-app-title" content="Hydrogen Chat">
<meta name="description" content="A matrix chat application"> <meta name="description" content="A matrix chat application">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css"> <link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/themes/element/theme.css" title="Element Theme">
<link rel="alternate stylesheet" type="text/css" href="src/ui/web/css/themes/bubbles/theme.css" title="Bubbles Theme">
</head> </head>
<body class="brawl"> <body class="hydrogen">
<script id="version" type="disabled"> <script id="version" type="disabled">
window.BRAWL_VERSION = "%%VERSION%%"; window.HYDROGEN_VERSION = "%%VERSION%%";
</script>
<script id="phone-debug-pre" type="disabled">
window.DEBUG = true;
window.debugConsoleBuffer = "";
console.error = (...params) => {
const lastLines = "...\n" + window.debugConsoleBuffer.split("\n").slice(-10).join("\n");
// window.debugConsoleBuffer = window.debugConsoleBuffer + "ERR " + params.join(" ") + "\n";
// const location = new Error().stack.split("\n")[2];
alert(params.join(" ") +"\n...\n" + lastLines);
};
console.log = console.info = console.warn = (...params) => {
window.debugConsoleBuffer = window.debugConsoleBuffer + params.join(" ") + "\n";
};
</script> </script>
<script id="main" type="module"> <script id="main" type="module">
import main from "./src/main.js"; import {main} from "./src/main.js";
main(document.body); main(document.body);
</script> </script>
<script id="phone-debug-post" type="disabled">
setTimeout(() => {
const showlogs = document.getElementById("showlogs");
showlogs.addEventListener("click", () => {
const lastLines = "...\n" + window.debugConsoleBuffer.split("\n").slice(-20).join("\n");
alert(lastLines);
}, true);
showlogs.innerText = "Show last 20 log lines";
}, 5000);
</script>
<script id="service-worker" type="disabled"> <script id="service-worker" type="disabled">
if('serviceWorker' in navigator) { if('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js') navigator.serviceWorker.register('sw.js')
.then(function() { console.log("Service Worker registered"); }); .then(function() { console.log("Service Worker registered"); });
} }
</script> </script>
</body> </body>
</html> </html>

2313
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "brawl-chat", "name": "hydrogen-web",
"version": "0.0.27", "version": "0.0.29",
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
"main": "index.js", "main": "index.js",
"directories": { "directories": {
@ -13,24 +13,36 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/bwindels/brawl-chat.git" "url": "git@github.com:vector-im/hydrogen-web.git"
}, },
"author": "Bruno Windels", "author": "matrix.org",
"license": "ISC", "license": "Apache-2.0",
"bugs": { "bugs": {
"url": "https://github.com/bwindels/brawl-chat/issues" "url": "https://github.com/vector-im/hydrogen-web/issues"
}, },
"homepage": "https://github.com/bwindels/brawl-chat#readme", "homepage": "https://github.com/vector-im/hydrogen-web/#readme",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.11.1",
"@babel/preset-env": "^7.11.0",
"@rollup/plugin-babel": "^5.1.0",
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-multi-entry": "^4.0.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"commander": "^6.0.0",
"core-js": "^3.6.5",
"finalhandler": "^1.1.1", "finalhandler": "^1.1.1",
"impunity": "^0.0.11", "impunity": "^0.0.11",
"postcss": "^7.0.18", "mdn-polyfills": "^5.20.0",
"postcss": "^7.0.32",
"postcss-css-variables": "^0.17.0",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-import": "^12.0.1", "postcss-import": "^12.0.1",
"rollup": "^1.15.6", "postcss-url": "^8.0.0",
"serve-static": "^1.13.2" "regenerator-runtime": "^0.13.7",
}, "rollup": "^2.26.4",
"dependencies": { "rollup-plugin-cleanup": "^3.1.1",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz" "serve-static": "^1.13.2",
"xxhash": "^0.3.0"
} }
} }

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="non-ie11.css" media="not all and (-ms-high-contrast: none), (-ms-high-contrast: active)">
<link rel="stylesheet" type="text/css" href="ie11.css" media="all and (-ms-high-contrast: none), (-ms-high-contrast: active)">
<style type="text/css">
/*
can't make this work in non-IE browser for now...
*/
@import url('non-ie11.css') screen @supports(--foo: green);
</style>
</head>
<body>
<p>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.</p>
</body>
</html>

3
prototypes/ie11.css Normal file
View file

@ -0,0 +1,3 @@
p {
color: red;
}

3
prototypes/non-ie11.css Normal file
View file

@ -0,0 +1,3 @@
p {
color: green;
}

View file

@ -1,49 +1,162 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 cheerio from "cheerio";
import fsRoot from "fs"; import fsRoot from "fs";
const fs = fsRoot.promises; const fs = fsRoot.promises;
import path from "path"; import path from "path";
import rollup from 'rollup'; import XXHash from 'xxhash';
import { rollup } from 'rollup';
import postcss from "postcss"; import postcss from "postcss";
import postcssImport from "postcss-import"; import postcssImport from "postcss-import";
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; 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 __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../"); 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 program = new commander.Command();
const offline = true; 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() { async function build() {
// only used for CSS for now, using legacy for all targets for now
const legacy = true;
// get version number // get version number
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version; 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 devHtml = await fs.readFile(path.join(projectDir, "index.html"), "utf8");
const doc = cheerio.load(devHtml); 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( doc("script#main").replaceWith(
`<script type="text/javascript" src="brawl.js"></script>` + `<script type="module">import {main} from "./${assetPaths.jsBundle()}"; main(document.body);</script>` +
`<script type="text/javascript">main(document.body);</script>`); `<script type="text/javascript" nomodule src="${assetPaths.jsLegacyBundle()}"></script>` +
removeOrEnableScript(doc("script#phone-debug-pre"), debug); `<script type="text/javascript" nomodule>${PROJECT_ID}Bundle.main(document.body);</script>`);
removeOrEnableScript(doc("script#phone-debug-post"), debug);
removeOrEnableScript(doc("script#service-worker"), offline); removeOrEnableScript(doc("script#service-worker"), offline);
const versionScript = doc("script#version"); const versionScript = doc("script#version");
@ -54,68 +167,149 @@ async function buildHtml(version) {
if (offline) { if (offline) {
doc("html").attr("manifest", "manifest.appcache"); doc("html").attr("manifest", "manifest.appcache");
doc("head").append(`<link rel="manifest" href="manifest.json">`); doc("head").append(`<link rel="manifest" href="${manifestPath.substr(targetDir.length)}">`);
} }
await fs.writeFile(path.join(targetDir, "index.html"), doc.html(), "utf8"); await fs.writeFile(path.join(targetDir, "index.html"), doc.html(), "utf8");
} }
async function buildJs() { async function buildJs() {
// create js bundle // create js bundle
const rollupConfig = { const bundle = await rollup({
input: 'src/main.js', input: 'src/main.js',
output: { plugins: [removeJsComments({comments: "none"})]
file: path.join(targetDir, "brawl.js"), });
format: 'iife', const {output} = await bundle.generate({
name: 'main' format: 'es',
} name: `${PROJECT_ID}Bundle`
}; });
const bundle = await rollup.rollup(rollupConfig); const code = output[0].code;
await bundle.write(rollupConfig); 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 // 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 // write appcache manifest
const manifestLines = [ const appCacheLines = [
`CACHE MANIFEST`, `CACHE MANIFEST`,
`# v${version}`, `# v${version}`,
`NETWORK`, `NETWORK`,
`"*"`, `"*"`,
`CACHE`, `CACHE`,
]; ];
manifestLines.push(...offlineFiles); appCacheLines.push(assetPaths.jsLegacyBundle(), ...offlineFiles);
const manifest = manifestLines.join("\n") + "\n"; const swOfflineFiles = [assetPaths.jsBundle(), ...offlineFiles];
await fs.writeFile(path.join(targetDir, "manifest.appcache"), manifest, "utf8"); const appCacheManifest = appCacheLines.join("\n") + "\n";
await fs.writeFile(path.join(targetDir, "manifest.appcache"), appCacheManifest, "utf8");
// write service worker // write service worker
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8"); let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`); 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"); await fs.writeFile(path.join(targetDir, "sw.js"), swSource, "utf8");
// write web manifest // write web manifest
const webManifest = { const webManifest = {
name: "Brawl Chat", name:PROJECT_NAME,
short_name: "Brawl", short_name: PROJECT_SHORT_NAME,
display: "fullscreen", display: "fullscreen",
start_url: "index.html", start_url: "index.html",
icons: [{"src": "icon-192.png", "sizes": "192x192", "type": "image/png"}], 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 // copy icon
// should this icon have a content hash as well?
let icon = await fs.readFile(path.join(projectDir, "icon.png")); let icon = await fs.readFile(path.join(projectDir, "icon.png"));
await fs.writeFile(path.join(targetDir, "icon-192.png"), icon); await fs.writeFile(path.join(targetDir, "icon-192.png"), icon);
return manifestPath;
} }
async function buildCss() { async function buildCssBundles(buildFn, themes, themeAssets) {
// create css bundle const bundleCss = await buildFn(path.join(cssSrcDir, "main.css"));
const cssMainFile = path.join(projectDir, "src/ui/web/css/main.css"); const mainDstPath = resource(`${PROJECT_ID}.css`, bundleCss);
const preCss = await fs.readFile(cssMainFile, "utf8"); await fs.writeFile(mainDstPath, bundleCss, "utf8");
const cssBundler = postcss([postcssImport]); const bundlePaths = {main: mainDstPath, themes: {}};
const postCss = await cssBundler.process(preCss, {from: cssMainFile}); for (const theme of themes) {
await fs.writeFile(path.join(targetDir, "brawl.css"), postCss, "utf8"); 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) { function removeOrEnableScript(scriptNode, enable) {
if (enable) { if (enable) {
@ -127,9 +321,7 @@ function removeOrEnableScript(scriptNode, enable) {
async function removeDirIfExists(targetDir) { async function removeDirIfExists(targetDir) {
try { try {
const files = await fs.readdir(targetDir); await fs.rmdir(targetDir, {recursive: true});
await Promise.all(files.map(filename => fs.unlink(path.join(targetDir, filename))));
await fs.rmdir(targetDir);
} catch (err) { } catch (err) {
if (err.code !== "ENOENT") { if (err.code !== "ENOENT") {
throw err; 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)); build().catch(err => console.error(err));

6
scripts/deploy.sh Executable file
View file

@ -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

7
scripts/package.sh Executable file
View file

@ -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

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 finalhandler = require('finalhandler')
const http = require('http') const http = require('http')
const serveStatic = require('serve-static') const serveStatic = require('serve-static')
@ -23,4 +39,4 @@ const server = http.createServer(function onRequest (req, res) {
}); });
// Listen // Listen
server.listen(3000); server.listen(3000);

View file

@ -1 +1,17 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; export {WebPlatform as Platform} from "./ui/web/WebPlatform.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SessionViewModel} from "./session/SessionViewModel.js";
import {LoginViewModel} from "./LoginViewModel.js"; import {LoginViewModel} from "./LoginViewModel.js";
import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {ViewModel} from "./ViewModel.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js";
import {SyncStatus} from "../matrix/Sync.js"; import {SyncStatus} from "../matrix/Sync.js";
import {ViewModel} from "./ViewModel.js"; import {ViewModel} from "./ViewModel.js";

View file

@ -1,6 +1,23 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SortedArray} from "../observable/index.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {ViewModel} from "./ViewModel.js"; import {ViewModel} from "./ViewModel.js";
import {avatarInitials, getIdentifierColorNumber} from "./avatar.js";
class SessionItemViewModel extends ViewModel { class SessionItemViewModel extends ViewModel {
constructor(sessionInfo, pickerVM) { constructor(sessionInfo, pickerVM) {
@ -96,6 +113,14 @@ class SessionItemViewModel extends ViewModel {
this.emitChange("exportDataUrl"); this.emitChange("exportDataUrl");
} }
} }
get avatarColorNumber() {
return getIdentifierColorNumber(this._sessionInfo.userId);
}
get avatarInitials() {
return avatarInitials(this._sessionInfo.userId);
}
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 // 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) // 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 // 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"; import {Disposables} from "../utils/Disposables.js";
export class ViewModel extends EventEmitter { export class ViewModel extends EventEmitter {
constructor({clock} = {}) { constructor({clock, emitChange} = {}) {
super(); super();
this.disposables = null; this.disposables = null;
this._options = {clock}; this._options = {clock, emitChange};
} }
childOptions(explicitOptions) { childOptions(explicitOptions) {
@ -54,8 +70,16 @@ export class ViewModel extends EventEmitter {
return result; return result;
} }
updateOptions(options) {
this._options = Object.assign(this._options, options);
}
emitChange(changedProps) { emitChange(changedProps) {
this.emit("change", changedProps); if (this._options.emitChange) {
this._options.emitChange(changedProps);
} else {
this.emit("change", changedProps);
}
} }
get clock() { get clock() {

49
src/domain/avatar.js Normal file
View file

@ -0,0 +1,49 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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;
}

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {ViewModel} from "../ViewModel.js";
import {createEnum} from "../../utils/enum.js"; import {createEnum} from "../../utils/enum.js";
import {ConnectionStatus} from "../../matrix/net/Reconnector.js"; import {ConnectionStatus} from "../../matrix/net/Reconnector.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js";
import {RoomViewModel} from "./room/RoomViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js";
import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
@ -12,12 +28,13 @@ export class SessionViewModel extends ViewModel {
sync: sessionContainer.sync, sync: sessionContainer.sync,
reconnector: sessionContainer.reconnector reconnector: sessionContainer.reconnector
}))); })));
this._currentRoomTileViewModel = null;
this._currentRoomViewModel = null; this._currentRoomViewModel = null;
const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => { const roomTileVMs = this._session.rooms.mapValues((room, emitChange) => {
return new RoomTileViewModel({ return new RoomTileViewModel({
room, room,
emitUpdate, emitChange,
emitOpen: room => this._openRoom(room) emitOpen: this._openRoom.bind(this)
}); });
}); });
this._roomList = roomTileVMs.sortValues((a, b) => a.compare(b)); 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) { if (this._currentRoomViewModel) {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
} }

View file

@ -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(), "");
}

View file

@ -1,5 +1,22 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {TimelineViewModel} from "./timeline/TimelineViewModel.js";
import {avatarInitials} from "../avatar.js"; import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel.js";
export class RoomViewModel extends ViewModel { export class RoomViewModel extends ViewModel {
@ -74,7 +91,9 @@ export class RoomViewModel extends ViewModel {
return avatarInitials(this._room.name); return avatarInitials(this._room.name);
} }
get avatarColorNumber() {
return getIdentifierColorNumber(this._room.id)
}
async _sendMessage(message) { async _sendMessage(message) {
if (message) { if (message) {
@ -97,12 +116,28 @@ export class RoomViewModel extends ViewModel {
} }
} }
class ComposerViewModel { class ComposerViewModel extends ViewModel {
constructor(roomVM) { constructor(roomVM) {
super();
this._roomVM = roomVM; this._roomVM = roomVM;
this._isEmpty = true;
} }
sendMessage(message) { 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");
} }
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseObservableList} from "../../../../observable/list/BaseObservableList.js";
import {sortedIndex} from "../../../../utils/sortedIndex.js"; import {sortedIndex} from "../../../../utils/sortedIndex.js";
@ -185,6 +201,10 @@ export class TilesCollection extends BaseObservableList {
get length() { get length() {
return this._tiles.length; return this._tiles.length;
} }
getFirst() {
return this._tiles[0];
}
} }
import {ObservableArray} from "../../../../observable/list/ObservableArray.js"; import {ObservableArray} from "../../../../observable/list/ObservableArray.js";

View file

@ -1,3 +1,20 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 need better naming, but
entry = event or gap from matrix layer 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 {TilesCollection} from "./TilesCollection.js";
import {tilesCreator} from "./tilesCreator.js"; import {tilesCreator} from "./tilesCreator.js";
import {ViewModel} from "../../../ViewModel.js";
export class TimelineViewModel { export class TimelineViewModel extends ViewModel {
constructor({room, timeline, ownUserId}) { constructor(options) {
super(options);
const {room, timeline, ownUserId} = options;
this._timeline = timeline; this._timeline = timeline;
// once we support sending messages we could do // once we support sending messages we could do
// timeline.entries.concat(timeline.pendingEvents) // timeline.entries.concat(timeline.pendingEvents)
// for an ObservableList that also contains local echos // 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 {bool} startReached if the start of the timeline was reached
return this._timeline.loadAtTop(50); */
async loadAtTop() {
const firstTile = this._tiles.getFirst();
if (firstTile.shape === "gap") {
return firstTile.fill();
} else {
await this._timeline.loadAtTop(50);
return false;
}
} }
unloadAtTop(tileAmount) { unloadAtTop(tileAmount) {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class UpdateAction {
constructor(remove, update, updateParams) { constructor(remove, update, updateParams) {
this._remove = remove; this._remove = remove;

View file

@ -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**`;
}
}

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SimpleTile} from "./SimpleTile.js";
import {UpdateAction} from "../UpdateAction.js"; import {UpdateAction} from "../UpdateAction.js";
@ -13,18 +29,20 @@ export class GapTile extends SimpleTile {
// prevent doing this twice // prevent doing this twice
if (!this._loading) { if (!this._loading) {
this._loading = true; this._loading = true;
this.emitUpdate("isLoading"); this.emitChange("isLoading");
try { try {
await this._timeline.fillGap(this._entry, 10); await this._timeline.fillGap(this._entry, 10);
} catch (err) { } catch (err) {
console.error(`timeline.fillGap(): ${err.message}:\n${err.stack}`); console.error(`timeline.fillGap(): ${err.message}:\n${err.stack}`);
this._error = err; this._error = err;
this.emitUpdate("error"); this.emitChange("error");
} finally { } finally {
this._loading = false; this._loading = false;
this.emitUpdate("isLoading"); this.emitChange("isLoading");
} }
} }
// edgeReached will have been updated by fillGap
return this._entry.edgeReached;
} }
updateEntry(entry, params) { updateEntry(entry, params) {
@ -44,14 +62,6 @@ export class GapTile extends SimpleTile {
return this._loading; return this._loading;
} }
get isUp() {
return this._entry.direction.isBackward;
}
get isDown() {
return this._entry.direction.isForward;
}
get error() { get error() {
if (this._error) { if (this._error) {
const dir = this._entry.prev_batch ? "previous" : "next"; const dir = this._entry.prev_batch ? "previous" : "next";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {MessageTile} from "./MessageTile.js";
const MAX_HEIGHT = 300; const MAX_HEIGHT = 300;
@ -10,32 +26,38 @@ export class ImageTile extends MessageTile {
} }
get thumbnailUrl() { get thumbnailUrl() {
const mxcUrl = this._getContent().url; const mxcUrl = this._getContent()?.url;
return this._room.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale"); if (typeof mxcUrl === "string") {
return this._room.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
}
return null;
} }
get url() { get url() {
const mxcUrl = this._getContent().url; const mxcUrl = this._getContent()?.url;
return this._room.mxcUrl(mxcUrl); if (typeof mxcUrl === "string") {
return this._room.mxcUrl(mxcUrl);
}
return null;
} }
_scaleFactor() { _scaleFactor() {
const {info} = this._getContent(); const info = this._getContent()?.info;
const scaleHeightFactor = MAX_HEIGHT / info.h; const scaleHeightFactor = MAX_HEIGHT / info?.h;
const scaleWidthFactor = MAX_WIDTH / info.w; const scaleWidthFactor = MAX_WIDTH / info?.w;
// take the smallest scale factor, to respect all constraints // take the smallest scale factor, to respect all constraints
// we should not upscale images, so limit scale factor to 1 upwards // we should not upscale images, so limit scale factor to 1 upwards
return Math.min(scaleWidthFactor, scaleHeightFactor, 1); return Math.min(scaleWidthFactor, scaleHeightFactor, 1);
} }
get thumbnailWidth() { get thumbnailWidth() {
const {info} = this._getContent(); const info = this._getContent()?.info;
return Math.round(info.w * this._scaleFactor()); return Math.round(info?.w * this._scaleFactor());
} }
get thumbnailHeight() { get thumbnailHeight() {
const {info} = this._getContent(); const info = this._getContent()?.info;
return Math.round(info.h * this._scaleFactor()); return Math.round(info?.h * this._scaleFactor());
} }
get label() { get label() {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {MessageTile} from "./MessageTile.js";
/* /*

View file

@ -1,10 +1,28 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SimpleTile} from "./SimpleTile.js";
import {getIdentifierColorNumber} from "../../../../avatar.js";
export class MessageTile extends SimpleTile { export class MessageTile extends SimpleTile {
constructor(options) { constructor(options) {
super(options); super(options);
this._clock = options.clock;
this._isOwn = this._entry.sender === options.ownUserId; 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; this._isContinuation = false;
} }
@ -16,12 +34,16 @@ export class MessageTile extends SimpleTile {
return this._entry.sender; return this._entry.sender;
} }
get senderColorNumber() {
return getIdentifierColorNumber(this._entry.sender);
}
get date() { get date() {
return this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"}); return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
} }
get time() { get time() {
return this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"}); return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"});
} }
get isOwn() { get isOwn() {
@ -38,10 +60,17 @@ export class MessageTile extends SimpleTile {
updatePreviousSibling(prev) { updatePreviousSibling(prev) {
super.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) { if (isContinuation !== this._isContinuation) {
this._isContinuation = isContinuation; this._isContinuation = isContinuation;
this.emitUpdate("isContinuation"); this.emitChange("isContinuation");
} }
} }
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SimpleTile} from "./SimpleTile.js";
export class RoomMemberTile extends SimpleTile { export class RoomMemberTile extends SimpleTile {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SimpleTile} from "./SimpleTile.js";
export class RoomNameTile extends SimpleTile { export class RoomNameTile extends SimpleTile {

View file

@ -1,9 +1,26 @@
import {UpdateAction} from "../UpdateAction.js"; /*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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}) { constructor({entry}) {
super();
this._entry = entry; this._entry = entry;
this._emitUpdate = null;
} }
// view model props for all subclasses // view model props for all subclasses
// hmmm, could also do instanceof ... ? // hmmm, could also do instanceof ... ?
@ -22,12 +39,6 @@ export class SimpleTile {
return false; return false;
} }
emitUpdate(paramName) {
if (this._emitUpdate) {
this._emitUpdate(this, paramName);
}
}
get internalId() { get internalId() {
return this._entry.asEventKey().toString(); return this._entry.asEventKey().toString();
} }
@ -37,7 +48,7 @@ export class SimpleTile {
} }
// TilesCollection contract below // TilesCollection contract below
setUpdateEmit(emitUpdate) { setUpdateEmit(emitUpdate) {
this._emitUpdate = emitUpdate; this.updateOptions({emitChange: paramName => emitUpdate(this, paramName)});
} }
get upperEntry() { get upperEntry() {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {MessageTile} from "./MessageTile.js";
export class TextTile extends MessageTile { export class TextTile extends MessageTile {

View file

@ -1,13 +1,30 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {GapTile} from "./tiles/GapTile.js";
import {TextTile} from "./tiles/TextTile.js"; import {TextTile} from "./tiles/TextTile.js";
import {ImageTile} from "./tiles/ImageTile.js"; import {ImageTile} from "./tiles/ImageTile.js";
import {LocationTile} from "./tiles/LocationTile.js"; import {LocationTile} from "./tiles/LocationTile.js";
import {RoomNameTile} from "./tiles/RoomNameTile.js"; import {RoomNameTile} from "./tiles/RoomNameTile.js";
import {RoomMemberTile} from "./tiles/RoomMemberTile.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) { return function tilesCreator(entry, emitUpdate) {
const options = {entry, emitUpdate, ownUserId}; const options = {entry, emitUpdate, ownUserId, clock};
if (entry.isGap) { if (entry.isGap) {
return new GapTile(options, room); return new GapTile(options, room);
} else if (entry.eventType) { } else if (entry.eventType) {
@ -33,6 +50,8 @@ export function tilesCreator({room, ownUserId}) {
return new RoomNameTile(options); return new RoomNameTile(options);
case "m.room.member": case "m.room.member":
return new RoomMemberTile(options); return new RoomMemberTile(options);
case "m.room.encrypted":
return new EncryptedEventTile(options);
default: default:
// unknown type not rendered // unknown type not rendered
return null; return null;

View file

@ -1,23 +1,60 @@
import {avatarInitials} from "../avatar.js"; /*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 // we use callbacks to parent VM instead of emit because
// it would be annoying to keep track of subscriptions in // it would be annoying to keep track of subscriptions in
// parent for all RoomTileViewModels // parent for all RoomTileViewModels
// emitUpdate is ObservableMap/ObservableList update mechanism // emitUpdate is ObservableMap/ObservableList update mechanism
constructor({room, emitUpdate, emitOpen}) { constructor(options) {
super(options);
const {room, emitOpen} = options;
this._room = room; this._room = room;
this._emitUpdate = emitUpdate;
this._emitOpen = emitOpen; 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() { open() {
this._emitOpen(this._room); this._isOpen = true;
this.emitChange("isOpen");
this._emitOpen(this._room, this);
} }
compare(other) { compare(other) {
// sort by name for now // sort alphabetically
return this._room.name.localeCompare(other._room.name); 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() { get name() {
@ -27,4 +64,8 @@ export class RoomTileViewModel {
get avatarInitials() { get avatarInitials() {
return avatarInitials(this._room.name); return avatarInitials(this._room.name);
} }
get avatarColorNumber() {
return getIdentifierColorNumber(this._room.id)
}
} }

26
src/legacy-polyfill.js Normal file
View file

@ -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);
};
}

View file

@ -1,5 +1,23 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {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 {SessionContainer} from "./matrix/SessionContainer.js";
import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js"; import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.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 {Clock} from "./ui/web/dom/Clock.js";
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.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 { try {
// to replay: // to replay:
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json(); // const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
@ -16,13 +37,17 @@ export default async function main(container) {
// const request = replay.request; // const request = replay.request;
// to record: // to record:
// const recorder = new RecordRequester(fetchRequest); // const recorder = new RecordRequester(createFetchRequest(clock.createTimeout));
// const request = recorder.request; // const request = recorder.request;
// window.getBrawlFetchLog = () => recorder.log(); // window.getBrawlFetchLog = () => recorder.log();
// normal network:
const request = fetchRequest;
const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1");
const clock = new Clock(); 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 storageFactory = new StorageFactory();
const vm = new BrawlViewModel({ const vm = new BrawlViewModel({

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {Platform} from "../Platform.js";
import {HomeServerError, ConnectionError} from "./error.js"; import {HomeServerError, ConnectionError} from "./error.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {Room} from "./room/Room.js";
import { ObservableMap } from "../observable/index.js"; import { ObservableMap } from "../observable/index.js";
import { SendScheduler, RateLimitingBackoff } from "./SendScheduler.js"; import { SendScheduler, RateLimitingBackoff } from "./SendScheduler.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {createEnum} from "../utils/enum.js";
import {ObservableValue} from "../observable/ObservableValue.js"; import {ObservableValue} from "../observable/ObservableValue.js";
import {HomeServerApi} from "./net/HomeServerApi.js"; import {HomeServerApi} from "./net/HomeServerApi.js";

View file

@ -1,3 +1,20 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {AbortError} from "./error.js";
import {ObservableValue} from "../observable/ObservableValue.js"; import {ObservableValue} from "../observable/ObservableValue.js";
import {createEnum} from "../utils/enum.js"; import {createEnum} from "../utils/enum.js";
@ -27,6 +44,15 @@ function parseRooms(roomsSection, roomCallback) {
return []; return [];
} }
function timelineIsEmpty(roomResponse) {
try {
const events = roomResponse?.timeline?.events;
return Array.isArray(events) && events.length === 0;
} catch (err) {
return true;
}
}
export class Sync { export class Sync {
constructor({hsApi, session, storage}) { constructor({hsApi, session, storage}) {
this._hsApi = hsApi; 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 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}); this._currentRequest = this._hsApi.sync(syncToken, syncFilterId, timeout, {timeout: totalRequestTimeout});
const response = await this._currentRequest.response(); const response = await this._currentRequest.response();
const isInitialSync = !syncToken;
syncToken = response.next_batch; syncToken = response.next_batch;
const storeNames = this._storage.storeNames; const storeNames = this._storage.storeNames;
const syncTxn = await this._storage.readWriteTxn([ const syncTxn = await this._storage.readWriteTxn([
@ -105,6 +132,11 @@ export class Sync {
// presence // presence
if (response.rooms) { if (response.rooms) {
const promises = parseRooms(response.rooms, async (roomId, roomResponse, membership) => { 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); let room = this._session.rooms.get(roomId);
if (!room) { if (!room) {
room = this._session.createRoom(roomId); room = this._session.createRoom(roomId);

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class User {
constructor(userId) { constructor(userId) {
this._userId = userId; this._userId = userId;

View file

@ -1,3 +1,30 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class HomeServerError extends Error {
constructor(method, url, body, status) { constructor(method, url, body, status) {
super(`${body ? body.error : status} on ${method} ${url}`); super(`${body ? body.error : status} on ${method} ${url}`);

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {AbortError} from "../../utils/error.js";
export class ExponentialRetryDelay { export class ExponentialRetryDelay {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { import {
HomeServerError, HomeServerError,
ConnectionError, ConnectionError,
@ -5,9 +21,9 @@ import {
} from "../error.js"; } from "../error.js";
class RequestWrapper { class RequestWrapper {
constructor(method, url, requestResult, responsePromise) { constructor(method, url, requestResult) {
this._requestResult = requestResult; this._requestResult = requestResult;
this._promise = responsePromise.then(response => { this._promise = requestResult.response().then(response => {
// ok? // ok?
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response.body; return response.body;
@ -44,35 +60,6 @@ export class HomeServerApi {
return `${this._homeserver}/_matrix/client/r0${csPath}`; 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) { _encodeQueryParams(queryParams) {
return Object.entries(queryParams || {}) return Object.entries(queryParams || {})
.filter(([, value]) => value !== undefined) .filter(([, value]) => value !== undefined)
@ -102,19 +89,10 @@ export class HomeServerApi {
method, method,
headers, headers,
body: bodyString, body: bodyString,
timeout: options && options.timeout
}); });
let responsePromise = requestResult.response(); const wrapper = new RequestWrapper(method, url, requestResult);
if (options && options.timeout) {
responsePromise = this._abortOnTimeout(
options.timeout,
requestResult,
responsePromise
);
}
const wrapper = new RequestWrapper(method, url, requestResult, responsePromise);
if (this._reconnector) { if (this._reconnector) {
wrapper.response().catch(err => { wrapper.response().catch(err => {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {createEnum} from "../../utils/enum.js";
import {ObservableValue} from "../../observable/ObservableValue.js"; import {ObservableValue} from "../../observable/ObservableValue.js";

View file

@ -1,7 +1,25 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { import {
AbortError, AbortError,
ConnectionError ConnectionError
} from "../../error.js"; } from "../../error.js";
import {abortOnTimeout} from "../timeout.js";
class RequestResult { class RequestResult {
constructor(promise, controller) { constructor(promise, controller) {
@ -15,9 +33,9 @@ class RequestResult {
} }
}; };
}); });
this._promise = Promise.race([promise, abortPromise]); this.promise = Promise.race([promise, abortPromise]);
} else { } else {
this._promise = promise; this.promise = promise;
this._controller = controller; this._controller = controller;
} }
} }
@ -27,47 +45,55 @@ class RequestResult {
} }
response() { response() {
return this._promise; return this.promise;
} }
} }
export function fetchRequest(url, options) { export function createFetchRequest(createTimeout) {
const controller = typeof AbortController === "function" ? new AbortController() : null; return function fetchRequest(url, options) {
if (controller) { const controller = typeof AbortController === "function" ? new AbortController() : null;
if (controller) {
options = Object.assign(options, {
signal: controller.signal
});
}
options = Object.assign(options, { options = Object.assign(options, {
signal: controller.signal mode: "cors",
credentials: "omit",
referrer: "no-referrer",
cache: "no-cache",
}); });
} if (options.headers) {
options = Object.assign(options, { const headers = new Headers();
mode: "cors", for(const [name, value] of options.headers.entries()) {
credentials: "omit", headers.append(name, value);
referrer: "no-referrer", }
cache: "no-cache", options.headers = headers;
});
if (options.headers) {
const headers = new Headers();
for(const [name, value] of options.headers.entries()) {
headers.append(name, value);
} }
options.headers = headers; const promise = fetch(url, options).then(async response => {
} const {status} = response;
const promise = fetch(url, options).then(async response => { const body = await response.json();
const {status} = response; return {status, body};
const body = await response.json(); }, err => {
return {status, body}; if (err.name === "AbortError") {
}, err => { throw new AbortError();
if (err.name === "AbortError") { } else if (err instanceof TypeError) {
throw new AbortError(); // Network errors are reported as TypeErrors, see
} else if (err instanceof TypeError) { // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful
// Network errors are reported as TypeErrors, see // this can either mean user is offline, server is offline, or a CORS error (server misconfiguration).
// 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.
// One could check navigator.onLine to rule out the first throw new ConnectionError(`${options.method} ${url}: ${err.message}`);
// 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 result;
return new RequestResult(promise, controller); }
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { import {
AbortError, AbortError,
ConnectionError ConnectionError

View file

@ -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");
}
}
}

51
src/matrix/net/timeout.js Normal file
View file

@ -0,0 +1,51 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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;
}
}
);
}

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {EventEmitter} from "../../utils/EventEmitter.js";
import {RoomSummary} from "./RoomSummary.js"; import {RoomSummary} from "./RoomSummary.js";
import {SyncWriter} from "./timeline/persistence/SyncWriter.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 {Timeline} from "./timeline/Timeline.js";
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js"; import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
import {SendQueue} from "./sending/SendQueue.js"; import {SendQueue} from "./sending/SendQueue.js";
import {WrappedError} from "../error.js"
export class Room extends EventEmitter { export class Room extends EventEmitter {
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) { constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) {
@ -51,8 +68,12 @@ export class Room extends EventEmitter {
} }
load(summary, txn) { load(summary, txn) {
this._summary.load(summary); try {
return this._syncWriter.load(txn); this._summary.load(summary);
return this._syncWriter.load(txn);
} catch (err) {
throw new WrappedError(`Could not load room ${this._roomId}`, err);
}
} }
sendEvent(eventType, content) { sendEvent(eventType, content) {
@ -96,6 +117,9 @@ export class Room extends EventEmitter {
/** @public */ /** @public */
async fillGap(fragmentEntry, amount) { async fillGap(fragmentEntry, amount) {
if (fragmentEntry.edgeReached) {
return;
}
const response = await this._hsApi.messages(this._roomId, { const response = await this._hsApi.messages(this._roomId, {
from: fragmentEntry.token, from: fragmentEntry.token,
dir: fragmentEntry.direction.asApiString(), dir: fragmentEntry.direction.asApiString(),

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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) { function applySyncResponse(data, roomResponse, membership) {
if (roomResponse.summary) { if (roomResponse.summary) {
data = updateSummary(data, roomResponse.summary); data = updateSummary(data, roomResponse.summary);

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class PendingEvent {
constructor(data) { constructor(data) {
this._data = data; this._data = data;

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SortedArray} from "../../../observable/list/SortedArray.js";
import {ConnectionError} from "../../error.js"; import {ConnectionError} from "../../error.js";
import {PendingEvent} from "./PendingEvent.js"; import {PendingEvent} from "./PendingEvent.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class Direction {
constructor(isForward) { constructor(isForward) {
this._isForward = isForward; this._isForward = isForward;

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {Platform} from "../../../Platform.js";
// key for events in the timelineEvents store // key for events in the timelineEvents store

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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, lookups will be far more frequent than changing fragment order,
so data structure should be optimized for fast lookup so data structure should be optimized for fast lookup

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SortedArray, MappedList, ConcatList} from "../../../observable/index.js";
import {Direction} from "./Direction.js"; import {Direction} from "./Direction.js";
import {TimelineReader} from "./persistence/TimelineReader.js"; import {TimelineReader} from "./persistence/TimelineReader.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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) { export function isValidFragmentId(id) {
return typeof id === "number"; return typeof id === "number";
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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. //entries can be sorted, first by fragment, then by entry index.
import {EventKey} from "../EventKey.js"; import {EventKey} from "../EventKey.js";
export const PENDING_FRAGMENT_ID = Number.MAX_SAFE_INTEGER; export const PENDING_FRAGMENT_ID = Number.MAX_SAFE_INTEGER;

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseEntry} from "./BaseEntry.js";
export class EventEntry extends BaseEntry { export class EventEntry extends BaseEntry {
@ -19,8 +35,7 @@ export class EventEntry extends BaseEntry {
} }
get prevContent() { get prevContent() {
const unsigned = this._eventEntry.event.unsigned; return this._eventEntry.event.unsigned?.prev_content;
return unsigned && unsigned.prev_content;
} }
get eventType() { get eventType() {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseEntry} from "./BaseEntry.js";
import {Direction} from "../Direction.js"; import {Direction} from "../Direction.js";
import {isValidFragmentId} from "../common.js"; import {isValidFragmentId} from "../common.js";
@ -44,7 +60,7 @@ export class FragmentBoundaryEntry extends BaseEntry {
} }
get isGap() { get isGap() {
return !!this.token; return !!this.token && !this.edgeReached;
} }
get token() { 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() { get linkedFragmentId() {
if (this.started) { if (this.started) {
return this.fragment.previousId; return this.fragment.previousId;

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {BaseEntry, PENDING_FRAGMENT_ID} from "./BaseEntry.js";
export class PendingEventEntry extends BaseEntry { export class PendingEventEntry extends BaseEntry {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {EventKey} from "../EventKey.js";
import {EventEntry} from "../entries/EventEntry.js"; import {EventEntry} from "../entries/EventEntry.js";
import {createEventEntry, directionalAppend} from "./common.js"; import {createEventEntry, directionalAppend} from "./common.js";
@ -162,6 +178,14 @@ export class GapWriter {
if (fragmentEntry.token !== start) { if (fragmentEntry.token !== start) {
throw new Error("start is not equal to prev_batch or next_batch"); 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 // find last event in fragment so we get the eventIndex to begin creating keys at
let lastKey = await this._findFragmentEdgeEventKey(fragmentEntry, txn); let lastKey = await this._findFragmentEdgeEventKey(fragmentEntry, txn);
// find out if any event in chunk is already present using findFirstOrLastOccurringEventId // find out if any event in chunk is already present using findFirstOrLastOccurringEventId

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {EventKey} from "../EventKey.js";
import {EventEntry} from "../entries/EventEntry.js"; import {EventEntry} from "../entries/EventEntry.js";
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js"; import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {directionalConcat, directionalAppend} from "./common.js";
import {Direction} from "../Direction.js"; import {Direction} from "../Direction.js";
import {EventEntry} from "../entries/EventEntry.js"; import {EventEntry} from "../entries/EventEntry.js";
@ -44,8 +60,8 @@ export class TimelineReader {
let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, this._fragmentIdComparer); let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, this._fragmentIdComparer);
// append or prepend fragmentEntry, reuse func from GapWriter? // append or prepend fragmentEntry, reuse func from GapWriter?
directionalAppend(entries, fragmentEntry, direction); directionalAppend(entries, fragmentEntry, direction);
// don't count it in amount perhaps? or do? // only continue loading if the fragment boundary can't be backfilled
if (fragmentEntry.hasLinkedFragment) { if (!fragmentEntry.token && fragmentEntry.hasLinkedFragment) {
const nextFragment = await fragmentStore.get(this._roomId, fragmentEntry.linkedFragmentId); const nextFragment = await fragmentStore.get(this._roomId, fragmentEntry.linkedFragmentId);
this._fragmentIdComparer.add(nextFragment); this._fragmentIdComparer.add(nextFragment);
const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, this._fragmentIdComparer); const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, this._fragmentIdComparer);

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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) { export function createEventEntry(key, roomId, event) {
return { return {
fragmentId: key.fragmentId, fragmentId: key.fragmentId,

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class SessionInfoStorage {
constructor(name) { constructor(name) {
this._name = name; this._name = name;

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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([ export const STORE_NAMES = Object.freeze([
"session", "session",
"roomState", "roomState",
@ -24,6 +40,11 @@ export class StorageError extends Error {
if (typeof cause.code === "number") { if (typeof cause.code === "number") {
fullMessage += `(code: ${cause.name}) `; fullMessage += `(code: ${cause.name}) `;
} }
}
if (value) {
fullMessage += `(value: ${JSON.stringify(value)}) `;
}
if (cause) {
fullMessage += cause.message; fullMessage += cause.message;
} }
super(fullMessage); super(fullMessage);
@ -33,4 +54,8 @@ export class StorageError extends Error {
this.cause = cause; this.cause = cause;
this.value = value; this.value = value;
} }
get name() {
return "StorageError";
}
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {iterateCursor, reqAsPromise} from "./utils.js";
export class QueryTarget { export class QueryTarget {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {Transaction} from "./Transaction.js";
import { STORE_NAMES, StorageError } from "../common.js"; import { STORE_NAMES, StorageError } from "../common.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {Storage} from "./Storage.js";
import { openDatabase, reqAsPromise } from "./utils.js"; import { openDatabase, reqAsPromise } from "./utils.js";
import { exportSession, importSession } from "./export.js"; import { exportSession, importSession } from "./export.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {QueryTarget} from "./QueryTarget.js";
import { reqAsPromise } from "./utils.js"; import { reqAsPromise } from "./utils.js";
import { StorageError } from "../common.js"; import { StorageError } from "../common.js";
@ -98,7 +114,7 @@ export class Store extends QueryTarget {
return await reqAsPromise(this._idbStore.put(value)); return await reqAsPromise(this._idbStore.put(value));
} catch(err) { } catch(err) {
const originalErr = err.cause; 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)); return await reqAsPromise(this._idbStore.add(value));
} catch(err) { } catch(err) {
const originalErr = err.cause; 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) { async delete(keyOrKeyRange) {
return reqAsPromise(this._idbStore.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);
}
} }
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {txnAsPromise} from "./utils.js";
import {StorageError} from "../common.js"; import {StorageError} from "../common.js";
import {Store} from "./Store.js"; import {Store} from "./Store.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { iterateCursor, txnAsPromise } from "./utils.js";
import { STORE_NAMES } from "../common.js"; import { STORE_NAMES } from "../common.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { encodeUint32, decodeUint32 } from "../utils.js";
import {Platform} from "../../../../Platform.js"; import {Platform} from "../../../../Platform.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class RoomStateStore {
constructor(idbStore) { constructor(idbStore) {
this._roomStateStore = idbStore; this._roomStateStore = idbStore;

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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: store contains:
roomId roomId

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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: store contains:
loginData { loginData {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js"; import { StorageError } from "../../common.js";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { StorageError } from "../../common.js";
import {Platform} from "../../../../Platform.js"; import {Platform} from "../../../../Platform.js";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils.js";

View file

@ -1,5 +1,31 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { StorageError } from "../common.js";
class WrappedDOMException extends StorageError {
constructor(request) {
const source = request?.source;
const storeName = source?.name || "<unknown store>";
const databaseName = source?.transaction?.db?.name || "<unknown db>";
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 // storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb
export function encodeUint32(n) { export function encodeUint32(n) {
@ -22,21 +48,17 @@ export function openDatabase(name, createObjectStore, version) {
return reqAsPromise(req); return reqAsPromise(req);
} }
function wrapError(err) {
return new StorageError(`wrapped DOMException`, err);
}
export function reqAsPromise(req) { export function reqAsPromise(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.addEventListener("success", event => resolve(event.target.result)); 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) { export function txnAsPromise(txn) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.addEventListener("complete", resolve); txn.addEventListener("complete", resolve);
txn.addEventListener("abort", event => reject(wrapError(event.target.error))); txn.addEventListener("abort", event => reject(new WrappedDOMException(event.target)));
}); });
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {Transaction} from "./Transaction.js";
import { STORE_MAP, STORE_NAMES } from "../common.js"; import { STORE_MAP, STORE_NAMES } from "../common.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {RoomTimelineStore} from "./stores/RoomTimelineStore.js";
export class Transaction { export class Transaction {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SortKey} from "../../room/timeline/SortKey.js";
import {sortedIndex} from "../../../utils/sortedIndex.js"; import {sortedIndex} from "../../../utils/sortedIndex.js";
import {Store} from "./Store.js"; import {Store} from "./Store.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class Store {
constructor(storeValue, writable) { constructor(storeValue, writable) {
this._storeValue = storeValue; this._storeValue = storeValue;

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {ObservableValue} from "../observable/ObservableValue.js";
class Timeout { class Timeout {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 { export class BaseObservable {
constructor() { constructor() {
this._handlers = new Set(); this._handlers = new Set();

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {AbortError} from "../utils/error.js";
import {BaseObservable} from "./BaseObservable.js"; import {BaseObservable} from "./BaseObservable.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SortedMapList} from "./list/SortedMapList.js";
import {FilteredMap} from "./map/FilteredMap.js"; import {FilteredMap} from "./map/FilteredMap.js";
import {MappedMap} from "./map/MappedMap.js"; import {MappedMap} from "./map/MappedMap.js";

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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"; import {BaseObservable} from "../BaseObservable.js";
export class BaseObservableList extends BaseObservable { export class BaseObservableList extends BaseObservable {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseObservableList} from "./BaseObservableList.js";
export class ConcatList extends BaseObservableList { export class ConcatList extends BaseObservableList {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseObservableList} from "./BaseObservableList.js";
export class MappedList extends BaseObservableList { export class MappedList extends BaseObservableList {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseObservableList} from "./BaseObservableList.js";
export class ObservableArray extends BaseObservableList { export class ObservableArray extends BaseObservableList {

View file

@ -1,3 +1,19 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseObservableList} from "./BaseObservableList.js";
import {sortedIndex} from "../../utils/sortedIndex.js"; import {sortedIndex} from "../../utils/sortedIndex.js";

Some files were not shown because too many files have changed in this diff Show more