Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs

Conflicts:
	package.json
	scripts/sdk/base-manifest.json
This commit is contained in:
Eric Eastwood 2022-04-05 17:15:30 -05:00
commit d247bc4e28
72 changed files with 1063 additions and 373 deletions

150
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,150 @@
Contributing code to hydrogen-web
==================================
Everyone is welcome to contribute code to hydrogen-web, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch.
* If your PR makes visual changes, include both **before** and **after** screenshots
to easily compare and discuss what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
If your PR is a feature then we require that the PR also includes tests.
These need to test that your feature works as expected and ideally test edge cases too.
Tests are written as unit tests by exporting a `tests` function from the file to be tested.
The function returns an object where the key is the test label, and the value is a
function that accepts an [assert](https://nodejs.org/api/assert.html) object, and return a Promise or nothing.
Note that there is currently a limitation that files that are not indirectly included from `src/platform/web/main.js` won't be found by the runner.
You can run the tests by running `yarn test`.
This uses the [impunity](https://github.com/bwindels/impunity) runner.
We don't require tests for bug fixes.
In the future we may formalise this more.
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Please disable any automatic formatting tools you may have active.
If present, you'll be asked to undo any unrelated whitespace changes during code review.
Members should not be exported as a default export in general.
In general, avoid using `export default`.
The remaining code-style for hydrogen is [in the process of being documented](codestyle.md), but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
If you change or create a file, feel free to add yourself to the copyright holders
in the license header of that file.
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```

View File

@ -10,6 +10,7 @@
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
"lint-ci": "eslint src/",
"test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/",
"test:postcss": "impunity --entry-point scripts/postcss/test.js ",
"start": "vite --port 3000",
"build": "vite build",
"build:sdk": "./scripts/sdk/build.sh"
@ -30,6 +31,7 @@
"acorn": "^8.6.0",
"acorn-walk": "^8.2.0",
"aes-js": "^3.1.2",
"bs58": "^4.0.1",
"core-js": "^3.6.5",
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush",
"escodegen": "^2.0.0",
@ -45,13 +47,14 @@
"text-encoding": "^0.7.0",
"typescript": "^4.3.5",
"vite": "todo: wait for next Vite release",
"xxhashjs": "^0.2.2",
"bs58": "^4.0.1"
"xxhashjs": "^0.2.2"
},
"dependencies": {
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"another-json": "^0.2.0",
"base64-arraybuffer": "^0.2.0",
"dompurify": "^2.3.0"
"dompurify": "^2.3.0",
"off-color": "^2.0.0",
"postcss-value-parser": "^4.2.0"
}
}

18
scripts/.eslintrc.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
"env": {
"node": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
"no-console": "off",
"no-empty": "off",
"no-prototype-builtins": "off",
"no-unused-vars": "warn"
},
};

31
scripts/postcss/color.js Normal file
View File

@ -0,0 +1,31 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const offColor = require("off-color").offColor;
module.exports.derive = function (value, operation, argument) {
const argumentAsNumber = parseInt(argument);
switch (operation) {
case "darker": {
const newColorString = offColor(value).darken(argumentAsNumber / 100).hex();
return newColorString;
}
case "lighter": {
const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex();
return newColorString;
}
}
}

View File

@ -0,0 +1,127 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const valueParser = require("postcss-value-parser");
/**
* This plugin derives new css variables from a given set of base variables.
* A derived css variable has the form --base--operation-argument; meaning that the derived
* variable has a value that is generated from the base variable "base" by applying "operation"
* with given "argument".
*
* eg: given the base variable --foo-color: #40E0D0, --foo-color--darker-20 is a css variable
* derived from foo-color by making it 20% more darker.
*
* All derived variables are added to the :root section.
*
* The actual derivation is done outside the plugin in a callback.
*/
let aliasMap;
let resolvedMap;
let baseVariables;
function getValueFromAlias(alias) {
const derivedVariable = aliasMap.get(alias);
return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable);
}
function parseDeclarationValue(value) {
const parsed = valueParser(value);
const variables = [];
parsed.walk(node => {
if (node.type !== "function" && node.value !== "var") {
return;
}
const variable = node.nodes[0];
variables.push(variable.value);
});
return variables;
}
function resolveDerivedVariable(decl, derive) {
const RE_VARIABLE_VALUE = /--((.+)--(.+)-(.+))/;
const variableCollection = parseDeclarationValue(decl.value);
for (const variable of variableCollection) {
const matches = variable.match(RE_VARIABLE_VALUE);
if (matches) {
const [, wholeVariable, baseVariable, operation, argument] = matches;
const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable);
if (!value) {
throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`);
}
const derivedValue = derive(value, operation, argument);
resolvedMap.set(wholeVariable, derivedValue);
}
}
}
function extract(decl) {
if (decl.variable) {
// see if right side is of form "var(--foo)"
const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1];
// remove -- from the prop
const prop = decl.prop.substring(2);
if (wholeVariable) {
aliasMap.set(prop, wholeVariable);
// Since this is an alias, we shouldn't store it in baseVariables
return;
}
baseVariables.set(prop, decl.value);
}
}
function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) {
const newRule = new Rule({ selector: ":root", source: root.source });
// Add derived css variables to :root
resolvedMap.forEach((value, key) => {
const declaration = new Declaration({prop: `--${key}`, value});
newRule.append(declaration);
});
root.append(newRule);
}
/**
* @callback derive
* @param {string} value - The base value on which an operation is applied
* @param {string} operation - The operation to be applied (eg: darker, lighter...)
* @param {string} argument - The argument for this operation
*/
/**
*
* @param {Object} opts - Options for the plugin
* @param {derive} opts.derive - The callback which contains the logic for resolving derived variables
*/
module.exports = (opts = {}) => {
aliasMap = new Map();
resolvedMap = new Map();
baseVariables = new Map();
return {
postcssPlugin: "postcss-compile-variables",
Once(root, {Rule, Declaration}) {
/*
Go through the CSS file once to extract all aliases and base variables.
We use these when resolving derived variables later.
*/
root.walkDecls(decl => extract(decl));
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive));
addResolvedVariablesToRootSelector(root, {Rule, Declaration});
},
};
};
module.exports.postcss = true;

121
scripts/postcss/test.js Normal file
View File

@ -0,0 +1,121 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const offColor = require("off-color").offColor;
const postcss = require("postcss");
const plugin = require("./css-compile-variables");
const derive = require("./color").derive;
async function run(input, output, opts = {}, assert) {
let result = await postcss([plugin({ ...opts, derive })]).process(input, { from: undefined, });
assert.strictEqual(
result.css.replaceAll(/\s/g, ""),
output.replaceAll(/\s/g, "")
);
assert.strictEqual(result.warnings().length, 0);
}
module.exports.tests = function tests() {
return {
"derived variables are resolved": async (assert) => {
const inputCSS = `
:root {
--foo-color: #ff0;
}
div {
background-color: var(--foo-color--lighter-50);
}`;
const transformedColor = offColor("#ff0").lighten(0.5);
const outputCSS =
inputCSS +
`
:root {
--foo-color--lighter-50: ${transformedColor.hex()};
}
`;
await run( inputCSS, outputCSS, {}, assert);
},
"derived variables work with alias": async (assert) => {
const inputCSS = `
:root {
--icon-color: #fff;
}
div {
background: var(--icon-color--darker-20);
--my-alias: var(--icon-color--darker-20);
color: var(--my-alias--lighter-15);
}`;
const colorDarker = offColor("#fff").darken(0.2).hex();
const aliasLighter = offColor(colorDarker).lighten(0.15).hex();
const outputCSS = inputCSS + `:root {
--icon-color--darker-20: ${colorDarker};
--my-alias--lighter-15: ${aliasLighter};
}
`;
await run(inputCSS, outputCSS, { }, assert);
},
"derived variable throws if base not present in config": async (assert) => {
const css = `:root {
color: var(--icon-color--darker-20);
}`;
assert.rejects(async () => await postcss([plugin({ variables: {} })]).process(css, { from: undefined, }));
},
"multiple derived variable in single declaration is parsed correctly": async (assert) => {
const inputCSS = `
:root {
--foo-color: #ff0;
}
div {
background-color: linear-gradient(var(--foo-color--lighter-50), var(--foo-color--darker-20));
}`;
const transformedColor1 = offColor("#ff0").lighten(0.5);
const transformedColor2 = offColor("#ff0").darken(0.2);
const outputCSS =
inputCSS +
`
:root {
--foo-color--lighter-50: ${transformedColor1.hex()};
--foo-color--darker-20: ${transformedColor2.hex()};
}
`;
await run( inputCSS, outputCSS, { }, assert);
},
"multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => {
const inputCSS = `
:root {
--foo-color: #ff0;
}
div {
--my-alias: var(--foo-color);
background-color: linear-gradient(var(--my-alias--lighter-50), var(--my-alias--darker-20));
}`;
const transformedColor1 = offColor("#ff0").lighten(0.5);
const transformedColor2 = offColor("#ff0").darken(0.2);
const outputCSS =
inputCSS +
`
:root {
--my-alias--lighter-50: ${transformedColor1.hex()};
--my-alias--darker-20: ${transformedColor2.hex()};
}
`;
await run( inputCSS, outputCSS, { }, assert);
}
};
};

View File

@ -1,7 +1,7 @@
{
"name": "hydrogen-view-sdk",
"description": "Embeddable matrix client library, including view components",
"version": "0.0.5",
"version": "0.0.9",
"main": "./hydrogen.cjs.js",
"exports": {
".": {

View File

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "./ViewModel.js";
import {ViewModel} from "./ViewModel";
import {KeyType} from "../matrix/ssss/index";
import {Status} from "./session/settings/KeyBackupViewModel.js";
export class AccountSetupViewModel extends ViewModel {
constructor(accountSetup) {
super();
this._accountSetup = accountSetup;
constructor(options) {
super(options);
this._accountSetup = options.accountSetup;
this._dehydratedDevice = undefined;
this._decryptDehydratedDeviceViewModel = undefined;
if (this._accountSetup.encryptedDehydratedDevice) {
@ -53,7 +53,7 @@ export class AccountSetupViewModel extends ViewModel {
// this vm adopts the same shape as KeyBackupViewModel so the same view can be reused.
class DecryptDehydratedDeviceViewModel extends ViewModel {
constructor(accountSetupViewModel, decryptedCallback) {
super();
super(accountSetupViewModel.options);
this._accountSetupViewModel = accountSetupViewModel;
this._isBusy = false;
this._status = Status.SetupKey;

View File

@ -14,11 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "./ViewModel.js";
import {Options, ViewModel} from "./ViewModel";
import {Client} from "../matrix/Client.js";
export class LogoutViewModel extends ViewModel {
constructor(options) {
type LogoutOptions = { sessionId: string; } & Options;
export class LogoutViewModel extends ViewModel<LogoutOptions> {
private _sessionId: string;
private _busy: boolean;
private _showConfirm: boolean;
private _error?: Error;
constructor(options: LogoutOptions) {
super(options);
this._sessionId = options.sessionId;
this._busy = false;
@ -26,19 +33,19 @@ export class LogoutViewModel extends ViewModel {
this._error = undefined;
}
get showConfirm() {
get showConfirm(): boolean {
return this._showConfirm;
}
get busy() {
get busy(): boolean {
return this._busy;
}
get cancelUrl() {
get cancelUrl(): string {
return this.urlCreator.urlForSegment("session", true);
}
async logout() {
async logout(): Promise<void> {
this._busy = true;
this._showConfirm = false;
this.emitChange("busy");
@ -53,7 +60,7 @@ export class LogoutViewModel extends ViewModel {
}
}
get status() {
get status(): string {
if (this._error) {
return this.i18n`Could not log out of device: ${this._error.message}`;
} else {

View File

@ -18,9 +18,9 @@ import {Client} from "../matrix/Client.js";
import {SessionViewModel} from "./session/SessionViewModel.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {LoginViewModel} from "./login/LoginViewModel.js";
import {LogoutViewModel} from "./LogoutViewModel.js";
import {LogoutViewModel} from "./LogoutViewModel";
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
import {ViewModel} from "./ViewModel.js";
import {ViewModel} from "./ViewModel";
export class RootViewModel extends ViewModel {
constructor(options) {

View File

@ -17,7 +17,7 @@ limitations under the License.
import {AccountSetupViewModel} from "./AccountSetupViewModel.js";
import {LoadStatus} from "../matrix/Client.js";
import {SyncStatus} from "../matrix/Sync.js";
import {ViewModel} from "./ViewModel.js";
import {ViewModel} from "./ViewModel";
export class SessionLoadViewModel extends ViewModel {
constructor(options) {
@ -43,7 +43,7 @@ export class SessionLoadViewModel extends ViewModel {
this.emitChange("loading");
this._waitHandle = this._client.loadStatus.waitFor(s => {
if (s === LoadStatus.AccountSetup) {
this._accountSetupViewModel = new AccountSetupViewModel(this._client.accountSetup);
this._accountSetupViewModel = new AccountSetupViewModel(this.childOptions({accountSetup: this._client.accountSetup}));
} else {
this._accountSetupViewModel = undefined;
}

View File

@ -15,8 +15,8 @@ limitations under the License.
*/
import {SortedArray} from "../observable/index.js";
import {ViewModel} from "./ViewModel.js";
import {avatarInitials, getIdentifierColorNumber} from "./avatar.js";
import {ViewModel} from "./ViewModel";
import {avatarInitials, getIdentifierColorNumber} from "./avatar";
class SessionItemViewModel extends ViewModel {
constructor(options, pickerVM) {

View File

@ -1,5 +1,6 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,54 +22,72 @@ limitations under the License.
import {EventEmitter} from "../utils/EventEmitter";
import {Disposables} from "../utils/Disposables";
export class ViewModel extends EventEmitter {
constructor(options = {}) {
import type {Disposable} from "../utils/Disposables";
import type {Platform} from "../platform/web/Platform";
import type {Clock} from "../platform/web/dom/Clock";
import type {ILogger} from "../logging/types";
import type {Navigation} from "./navigation/Navigation";
import type {URLRouter} from "./navigation/URLRouter";
export type Options = {
platform: Platform
logger: ILogger
urlCreator: URLRouter
navigation: Navigation
emitChange?: (params: any) => void
}
export class ViewModel<O extends Options = Options> extends EventEmitter<{change: never}> {
private disposables?: Disposables;
private _isDisposed = false;
private _options: O;
constructor(options: O) {
super();
this.disposables = null;
this._isDisposed = false;
this._options = options;
}
childOptions(explicitOptions) {
const {navigation, urlCreator, platform} = this._options;
return Object.assign({navigation, urlCreator, platform}, explicitOptions);
childOptions<T extends Object>(explicitOptions: T): T & Options {
return Object.assign({}, this._options, explicitOptions);
}
get options(): O { return this._options; }
// makes it easier to pass through dependencies of a sub-view model
getOption(name) {
getOption<N extends keyof O>(name: N): O[N] {
return this._options[name];
}
track(disposable) {
track<D extends Disposable>(disposable: D): D {
if (!this.disposables) {
this.disposables = new Disposables();
}
return this.disposables.track(disposable);
}
untrack(disposable) {
untrack(disposable: Disposable): undefined {
if (this.disposables) {
return this.disposables.untrack(disposable);
}
return null;
return undefined;
}
dispose() {
dispose(): void {
if (this.disposables) {
this.disposables.dispose();
}
this._isDisposed = true;
}
get isDisposed() {
get isDisposed(): boolean {
return this._isDisposed;
}
disposeTracked(disposable) {
disposeTracked(disposable: Disposable | undefined): undefined {
if (this.disposables) {
return this.disposables.disposeTracked(disposable);
}
return null;
return undefined;
}
// TODO: this will need to support binding
@ -76,7 +95,7 @@ export class ViewModel extends EventEmitter {
//
// translated string should probably always be bindings, unless we're fine with a refresh when changing the language?
// we probably are, if we're using routing with a url, we could just refresh.
i18n(parts, ...expr) {
i18n(parts: TemplateStringsArray, ...expr: any[]) {
// just concat for now
let result = "";
for (let i = 0; i < parts.length; ++i) {
@ -88,11 +107,11 @@ export class ViewModel extends EventEmitter {
return result;
}
updateOptions(options) {
updateOptions(options: O): void {
this._options = Object.assign(this._options, options);
}
emitChange(changedProps) {
emitChange(changedProps: any): void {
if (this._options.emitChange) {
this._options.emitChange(changedProps);
} else {
@ -100,27 +119,23 @@ export class ViewModel extends EventEmitter {
}
}
get platform() {
get platform(): Platform {
return this._options.platform;
}
get clock() {
get clock(): Clock {
return this._options.platform.clock;
}
get logger() {
get logger(): ILogger {
return this.platform.logger;
}
/**
* The url router, only meant to be used to create urls with from view models.
* @return {URLRouter}
*/
get urlCreator() {
get urlCreator(): URLRouter {
return this._options.urlCreator;
}
get navigation() {
get navigation(): Navigation {
return this._options.navigation;
}
}

View File

@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export function avatarInitials(name) {
import { Platform } from "../platform/web/Platform";
import { MediaRepository } from "../matrix/net/MediaRepository";
export function avatarInitials(name: string): string {
let firstChar = name.charAt(0);
if (firstChar === "!" || firstChar === "@" || firstChar === "#") {
firstChar = name.charAt(1);
@ -29,10 +32,10 @@ export function avatarInitials(name) {
*
* @return {number}
*/
function hashCode(str) {
function hashCode(str: string): number {
let hash = 0;
let i;
let chr;
let i: number;
let chr: number;
if (str.length === 0) {
return hash;
}
@ -44,11 +47,11 @@ function hashCode(str) {
return Math.abs(hash);
}
export function getIdentifierColorNumber(id) {
export function getIdentifierColorNumber(id: string): number {
return (hashCode(id) % 8) + 1;
}
export function getAvatarHttpUrl(avatarUrl, cssSize, platform, mediaRepository) {
export function getAvatarHttpUrl(avatarUrl: string, cssSize: number, platform: Platform, mediaRepository: MediaRepository): string | null {
if (avatarUrl) {
const imageSize = cssSize * platform.devicePixelRatio;
return mediaRepository.mxcUrlThumbnail(avatarUrl, imageSize, imageSize, "crop");

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
import {LoginFailure} from "../../matrix/Client.js";
export class CompleteSSOLoginViewModel extends ViewModel {

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import {Client} from "../../matrix/Client.js";
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js";
import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js";
import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js";

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
import {LoginFailure} from "../../matrix/Client.js";
export class PasswordLoginViewModel extends ViewModel {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
export class StartSSOLoginViewModel extends ViewModel{
constructor(options) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
import {imageToInfo} from "./common.js";
import {RoomType} from "../../matrix/room/common";

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
import {addPanelIfNeeded} from "../navigation/index.js";
function dedupeSparse(roomIds) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
import {createEnum} from "../../utils/enum";
import {ConnectionStatus} from "../../matrix/net/Reconnector";
import {SyncStatus} from "../../matrix/Sync.js";

View File

@ -25,7 +25,7 @@ import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
import {RoomGridViewModel} from "./RoomGridViewModel.js";
import {SettingsViewModel} from "./settings/SettingsViewModel.js";
import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
import {ViewModel} from "../ViewModel.js";
import {ViewModel} from "../ViewModel";
import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";

View File

@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {ViewModel} from "../../ViewModel";
const KIND_ORDER = ["roomBeingCreated", "invite", "room"];

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
import {RoomTileViewModel} from "./RoomTileViewModel.js";
import {InviteTileViewModel} from "./InviteTileViewModel.js";
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";

View File

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
import {RoomType} from "../../../matrix/room/common";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
export class MemberDetailsViewModel extends ViewModel {
constructor(options) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
import {MemberTileViewModel} from "./MemberTileViewModel.js";
import {createMemberComparator} from "./members/comparator.js";
import {Disambiguator} from "./members/disambiguator.js";

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {ViewModel} from "../../ViewModel";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
export class MemberTileViewModel extends ViewModel {
constructor(options) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js";
import {MemberListViewModel} from "./MemberListViewModel.js";
import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js";

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {ViewModel} from "../../ViewModel";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
export class RoomDetailsViewModel extends ViewModel {
constructor(options) {

View File

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
export class ComposerViewModel extends ViewModel {
constructor(roomVM) {
super();
super(roomVM.options);
this._roomVM = roomVM;
this._isEmpty = true;
this._replyVM = null;

View File

@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {ViewModel} from "../../ViewModel";
export class InviteViewModel extends ViewModel {
constructor(options) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
export class LightboxViewModel extends ViewModel {
constructor(options) {

View File

@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {ViewModel} from "../../ViewModel";
export class RoomBeingCreatedViewModel extends ViewModel {
constructor(options) {

View File

@ -17,9 +17,9 @@ limitations under the License.
import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
import {ComposerViewModel} from "./ComposerViewModel.js"
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {tilesCreator} from "./timeline/tilesCreator.js";
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
import {imageToInfo} from "../common.js";
export class RoomViewModel extends ViewModel {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
export class UnknownRoomViewModel extends ViewModel {
constructor(options) {
@ -55,4 +55,4 @@ export class UnknownRoomViewModel extends ViewModel {
get kind() {
return "unknown";
}
}
}

View File

@ -1,5 +1,5 @@
import { linkify } from "./linkify/linkify.js";
import { getIdentifierColorNumber, avatarInitials } from "../../../avatar.js";
import { getIdentifierColorNumber, avatarInitials } from "../../../avatar";
/**
* Parse text into parts such as newline, links and text.

View File

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {ObservableMap} from "../../../../observable/map/ObservableMap.js";
import {ObservableMap} from "../../../../observable/map/ObservableMap";
export class ReactionsViewModel {
constructor(parentTile) {

View File

@ -32,7 +32,7 @@ to the room timeline, which unload entries from memory.
when loading, it just reads events from a sortkey backwards or forwards...
*/
import {TilesCollection} from "./TilesCollection.js";
import {ViewModel} from "../../../ViewModel.js";
import {ViewModel} from "../../../ViewModel";
export class TimelineViewModel extends ViewModel {
constructor(options) {

View File

@ -16,7 +16,7 @@ limitations under the License.
import {SimpleTile} from "./SimpleTile.js";
import {ReactionsViewModel} from "../ReactionsViewModel.js";
import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar.js";
import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar";
export class BaseMessageTile extends SimpleTile {
constructor(options) {

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import {UpdateAction} from "../UpdateAction.js";
import {ViewModel} from "../../../../ViewModel.js";
import {ViewModel} from "../../../../ViewModel";
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
export class SimpleTile extends ViewModel {
@ -44,6 +44,10 @@ export class SimpleTile extends ViewModel {
return this._entry.asEventKey();
}
get eventId() {
return this._entry.id;
}
get isPending() {
return this._entry.isPending;
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
import {KeyType} from "../../../matrix/ssss/index";
import {createEnum} from "../../../utils/enum";

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ViewModel} from "../../ViewModel.js";
import {ViewModel} from "../../ViewModel";
import {KeyBackupViewModel} from "./KeyBackupViewModel.js";
class PushNotificationStatus {

View File

@ -16,6 +16,7 @@ limitations under the License.
export {Platform} from "./platform/web/Platform.js";
export {Client, LoadStatus} from "./matrix/Client.js";
export {RoomStatus} from "./matrix/room/common";
// export main view & view models
export {createNavigation, createRouter} from "./domain/navigation/index.js";
export {RootViewModel} from "./domain/RootViewModel.js";
@ -30,6 +31,23 @@ export {Navigation} from "./domain/navigation/Navigation.js";
export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js";
export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js";
export {TemplateView} from "./platform/web/ui/general/TemplateView";
export {ViewModel} from "./domain/ViewModel.js";
export {ViewModel} from "./domain/ViewModel";
export {LoadingView} from "./platform/web/ui/general/LoadingView.js";
export {AvatarView} from "./platform/web/ui/AvatarView.js";
export {RoomType} from "./matrix/room/common";
export {EventEmitter} from "./utils/EventEmitter";
export {Disposables} from "./utils/Disposables";
// these should eventually be moved to another library
export {
ObservableArray,
SortedArray,
MappedList,
AsyncMappedList,
ConcatList,
ObservableMap
} from "./observable/index";
export {
BaseObservableValue,
ObservableValue,
RetainedObservableValue
} from "./observable/ObservableValue";

View File

@ -26,8 +26,8 @@ import {User} from "./User.js";
import {DeviceMessageHandler} from "./DeviceMessageHandler.js";
import {Account as E2EEAccount} from "./e2ee/Account.js";
import {uploadAccountAsDehydratedDevice} from "./e2ee/Dehydration.js";
import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js";
import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js";
import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption";
import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption";
import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption";
import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader";
import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup";
@ -123,25 +123,24 @@ export class Session {
// TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account
// and can create RoomEncryption objects and handle encrypted to_device messages and device list changes.
const senderKeyLock = new LockMap();
const olmDecryption = new OlmDecryption({
account: this._e2eeAccount,
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._platform.clock.now,
ownUserId: this._user.id,
const olmDecryption = new OlmDecryption(
this._e2eeAccount,
PICKLE_KEY,
this._platform.clock.now,
this._user.id,
this._olm,
senderKeyLock
});
this._olmEncryption = new OlmEncryption({
account: this._e2eeAccount,
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._platform.clock.now,
ownUserId: this._user.id,
olmUtil: this._olmUtil,
);
this._olmEncryption = new OlmEncryption(
this._e2eeAccount,
PICKLE_KEY,
this._olm,
this._storage,
this._platform.clock.now,
this._user.id,
this._olmUtil,
senderKeyLock
});
);
this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20);
this._megolmEncryption = new MegOlmEncryption({
account: this._e2eeAccount,

View File

@ -26,35 +26,41 @@ limitations under the License.
* see DeviceTracker
*/
import type {DeviceIdentity} from "../storage/idb/stores/DeviceIdentityStore";
type DecryptedEvent = {
type?: string,
content?: Record<string, any>
}
export class DecryptionResult {
constructor(event, senderCurve25519Key, claimedEd25519Key) {
this.event = event;
this.senderCurve25519Key = senderCurve25519Key;
this.claimedEd25519Key = claimedEd25519Key;
this._device = null;
this._roomTracked = true;
private device?: DeviceIdentity;
private roomTracked: boolean = true;
constructor(
public readonly event: DecryptedEvent,
public readonly senderCurve25519Key: string,
public readonly claimedEd25519Key: string
) {}
setDevice(device: DeviceIdentity): void {
this.device = device;
}
setDevice(device) {
this._device = device;
setRoomNotTrackedYet(): void {
this.roomTracked = false;
}
setRoomNotTrackedYet() {
this._roomTracked = false;
}
get isVerified() {
if (this._device) {
const comesFromDevice = this._device.ed25519Key === this.claimedEd25519Key;
get isVerified(): boolean {
if (this.device) {
const comesFromDevice = this.device.ed25519Key === this.claimedEd25519Key;
return comesFromDevice;
}
return false;
}
get isUnverified() {
if (this._device) {
get isUnverified(): boolean {
if (this.device) {
return !this.isVerified;
} else if (this.isVerificationUnknown) {
return false;
@ -63,8 +69,8 @@ export class DecryptionResult {
}
}
get isVerificationUnknown() {
get isVerificationUnknown(): boolean {
// verification is unknown if we haven't yet fetched the devices for the room
return !this._device && !this._roomTracked;
return !this.device && !this.roomTracked;
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {DecryptionResult} from "../../DecryptionResult.js";
import {DecryptionResult} from "../../DecryptionResult";
import {DecryptionError} from "../../common.js";
import {ReplayDetectionEntry} from "./ReplayDetectionEntry";
import type {RoomKey} from "./RoomKey";

View File

@ -16,32 +16,47 @@ limitations under the License.
import {DecryptionError} from "../common.js";
import {groupBy} from "../../../utils/groupBy";
import {MultiLock} from "../../../utils/Lock";
import {Session} from "./Session.js";
import {DecryptionResult} from "../DecryptionResult.js";
import {MultiLock, ILock} from "../../../utils/Lock";
import {Session} from "./Session";
import {DecryptionResult} from "../DecryptionResult";
import {OlmPayloadType} from "./types";
import type {OlmMessage, OlmPayload} from "./types";
import type {Account} from "../Account";
import type {LockMap} from "../../../utils/LockMap";
import type {Transaction} from "../../storage/idb/Transaction";
import type {OlmEncryptedEvent} from "./types";
import type * as OlmNamespace from "@matrix-org/olm";
type Olm = typeof OlmNamespace;
const SESSION_LIMIT_PER_SENDER_KEY = 4;
function isPreKeyMessage(message) {
return message.type === 0;
}
type DecryptionResults = {
results: DecryptionResult[],
errors: DecryptionError[],
senderKeyDecryption: SenderKeyDecryption
};
function sortSessions(sessions) {
type CreateAndDecryptResult = {
session: Session,
plaintext: string
};
function sortSessions(sessions: Session[]): void {
sessions.sort((a, b) => {
return b.data.lastUsed - a.data.lastUsed;
});
}
export class Decryption {
constructor({account, pickleKey, now, ownUserId, storage, olm, senderKeyLock}) {
this._account = account;
this._pickleKey = pickleKey;
this._now = now;
this._ownUserId = ownUserId;
this._storage = storage;
this._olm = olm;
this._senderKeyLock = senderKeyLock;
}
constructor(
private readonly account: Account,
private readonly pickleKey: string,
private readonly now: () => number,
private readonly ownUserId: string,
private readonly olm: Olm,
private readonly senderKeyLock: LockMap<string>
) {}
// we need to lock because both encryption and decryption can't be done in one txn,
// so for them not to step on each other toes, we need to lock.
@ -50,8 +65,8 @@ export class Decryption {
// - decryptAll below fails (to release the lock as early as we can)
// - DecryptionChanges.write succeeds
// - Sync finishes the writeSync phase (or an error was thrown, in case we never get to DecryptionChanges.write)
async obtainDecryptionLock(events) {
const senderKeys = new Set();
async obtainDecryptionLock(events: OlmEncryptedEvent[]): Promise<ILock> {
const senderKeys = new Set<string>();
for (const event of events) {
const senderKey = event.content?.["sender_key"];
if (senderKey) {
@ -61,7 +76,7 @@ export class Decryption {
// take a lock on all senderKeys so encryption or other calls to decryptAll (should not happen)
// don't modify the sessions at the same time
const locks = await Promise.all(Array.from(senderKeys).map(senderKey => {
return this._senderKeyLock.takeLock(senderKey);
return this.senderKeyLock.takeLock(senderKey);
}));
return new MultiLock(locks);
}
@ -83,18 +98,18 @@ export class Decryption {
* @param {[type]} events
* @return {Promise<DecryptionChanges>} [description]
*/
async decryptAll(events, lock, txn) {
async decryptAll(events: OlmEncryptedEvent[], lock: ILock, txn: Transaction): Promise<DecryptionChanges> {
try {
const eventsPerSenderKey = groupBy(events, event => event.content?.["sender_key"]);
const timestamp = this._now();
const eventsPerSenderKey = groupBy(events, (event: OlmEncryptedEvent) => event.content?.["sender_key"]);
const timestamp = this.now();
// decrypt events for different sender keys in parallel
const senderKeyOperations = await Promise.all(Array.from(eventsPerSenderKey.entries()).map(([senderKey, events]) => {
return this._decryptAllForSenderKey(senderKey, events, timestamp, txn);
return this._decryptAllForSenderKey(senderKey!, events, timestamp, txn);
}));
const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), []);
const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), []);
const results = senderKeyOperations.reduce((all, r) => all.concat(r.results), [] as DecryptionResult[]);
const errors = senderKeyOperations.reduce((all, r) => all.concat(r.errors), [] as DecryptionError[]);
const senderKeyDecryptions = senderKeyOperations.map(r => r.senderKeyDecryption);
return new DecryptionChanges(senderKeyDecryptions, results, errors, this._account, lock);
return new DecryptionChanges(senderKeyDecryptions, results, errors, this.account, lock);
} catch (err) {
// make sure the locks are release if something throws
// otherwise they will be released in DecryptionChanges after having written
@ -104,11 +119,11 @@ export class Decryption {
}
}
async _decryptAllForSenderKey(senderKey, events, timestamp, readSessionsTxn) {
async _decryptAllForSenderKey(senderKey: string, events: OlmEncryptedEvent[], timestamp: number, readSessionsTxn: Transaction): Promise<DecryptionResults> {
const sessions = await this._getSessions(senderKey, readSessionsTxn);
const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, this._olm, timestamp);
const results = [];
const errors = [];
const senderKeyDecryption = new SenderKeyDecryption(senderKey, sessions, timestamp);
const results: DecryptionResult[] = [];
const errors: DecryptionError[] = [];
// events for a single senderKey need to be decrypted one by one
for (const event of events) {
try {
@ -121,10 +136,10 @@ export class Decryption {
return {results, errors, senderKeyDecryption};
}
_decryptForSenderKey(senderKeyDecryption, event, timestamp) {
_decryptForSenderKey(senderKeyDecryption: SenderKeyDecryption, event: OlmEncryptedEvent, timestamp: number): DecryptionResult {
const senderKey = senderKeyDecryption.senderKey;
const message = this._getMessageAndValidateEvent(event);
let plaintext;
let plaintext: string | undefined;
try {
plaintext = senderKeyDecryption.decrypt(message);
} catch (err) {
@ -132,8 +147,8 @@ export class Decryption {
throw new DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", event, {senderKey, error: err.message});
}
// could not decrypt with any existing session
if (typeof plaintext !== "string" && isPreKeyMessage(message)) {
let createResult;
if (typeof plaintext !== "string" && message.type === OlmPayloadType.PreKey) {
let createResult: CreateAndDecryptResult;
try {
createResult = this._createSessionAndDecrypt(senderKey, message, timestamp);
} catch (error) {
@ -143,14 +158,14 @@ export class Decryption {
plaintext = createResult.plaintext;
}
if (typeof plaintext === "string") {
let payload;
let payload: OlmPayload;
try {
payload = JSON.parse(plaintext);
} catch (error) {
throw new DecryptionError("PLAINTEXT_NOT_JSON", event, {plaintext, error});
}
this._validatePayload(payload, event);
return new DecryptionResult(payload, senderKey, payload.keys.ed25519);
return new DecryptionResult(payload, senderKey, payload.keys!.ed25519!);
} else {
throw new DecryptionError("OLM_NO_MATCHING_SESSION", event,
{knownSessionIds: senderKeyDecryption.sessions.map(s => s.id)});
@ -158,16 +173,16 @@ export class Decryption {
}
// only for pre-key messages after having attempted decryption with existing sessions
_createSessionAndDecrypt(senderKey, message, timestamp) {
_createSessionAndDecrypt(senderKey: string, message: OlmMessage, timestamp: number): CreateAndDecryptResult {
let plaintext;
// if we have multiple messages encrypted with the same new session,
// this could create multiple sessions as the OTK isn't removed yet
// (this only happens in DecryptionChanges.write)
// This should be ok though as we'll first try to decrypt with the new session
const olmSession = this._account.createInboundOlmSession(senderKey, message.body);
const olmSession = this.account.createInboundOlmSession(senderKey, message.body);
try {
plaintext = olmSession.decrypt(message.type, message.body);
const session = Session.create(senderKey, olmSession, this._olm, this._pickleKey, timestamp);
const session = Session.create(senderKey, olmSession, this.olm, this.pickleKey, timestamp);
session.unload(olmSession);
return {session, plaintext};
} catch (err) {
@ -176,12 +191,12 @@ export class Decryption {
}
}
_getMessageAndValidateEvent(event) {
_getMessageAndValidateEvent(event: OlmEncryptedEvent): OlmMessage {
const ciphertext = event.content?.ciphertext;
if (!ciphertext) {
throw new DecryptionError("OLM_MISSING_CIPHERTEXT", event);
}
const message = ciphertext?.[this._account.identityKeys.curve25519];
const message = ciphertext?.[this.account.identityKeys.curve25519];
if (!message) {
throw new DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", event);
}
@ -189,22 +204,22 @@ export class Decryption {
return message;
}
async _getSessions(senderKey, txn) {
async _getSessions(senderKey: string, txn: Transaction): Promise<Session[]> {
const sessionEntries = await txn.olmSessions.getAll(senderKey);
// sort most recent used sessions first
const sessions = sessionEntries.map(s => new Session(s, this._pickleKey, this._olm));
const sessions = sessionEntries.map(s => new Session(s, this.pickleKey, this.olm));
sortSessions(sessions);
return sessions;
}
_validatePayload(payload, event) {
_validatePayload(payload: OlmPayload, event: OlmEncryptedEvent): void {
if (payload.sender !== event.sender) {
throw new DecryptionError("OLM_FORWARDED_MESSAGE", event, {sentBy: event.sender, encryptedBy: payload.sender});
}
if (payload.recipient !== this._ownUserId) {
if (payload.recipient !== this.ownUserId) {
throw new DecryptionError("OLM_BAD_RECIPIENT", event, {recipient: payload.recipient});
}
if (payload.recipient_keys?.ed25519 !== this._account.identityKeys.ed25519) {
if (payload.recipient_keys?.ed25519 !== this.account.identityKeys.ed25519) {
throw new DecryptionError("OLM_BAD_RECIPIENT_KEY", event, {key: payload.recipient_keys?.ed25519});
}
// TODO: check room_id
@ -219,21 +234,20 @@ export class Decryption {
// decryption helper for a single senderKey
class SenderKeyDecryption {
constructor(senderKey, sessions, olm, timestamp) {
this.senderKey = senderKey;
this.sessions = sessions;
this._olm = olm;
this._timestamp = timestamp;
}
constructor(
public readonly senderKey: string,
public readonly sessions: Session[],
private readonly timestamp: number
) {}
addNewSession(session) {
addNewSession(session: Session): void {
// add at top as it is most recent
this.sessions.unshift(session);
}
decrypt(message) {
decrypt(message: OlmMessage): string | undefined {
for (const session of this.sessions) {
const plaintext = this._decryptWithSession(session, message);
const plaintext = this.decryptWithSession(session, message);
if (typeof plaintext === "string") {
// keep them sorted so will try the same session first for other messages
// and so we can assume the excess ones are at the end
@ -244,11 +258,11 @@ class SenderKeyDecryption {
}
}
getModifiedSessions() {
getModifiedSessions(): Session[] {
return this.sessions.filter(session => session.isModified);
}
get hasNewSessions() {
get hasNewSessions(): boolean {
return this.sessions.some(session => session.isNew);
}
@ -257,19 +271,22 @@ class SenderKeyDecryption {
// if this turns out to be a real cost for IE11,
// we could look into adding a less expensive serialization mechanism
// for olm sessions to libolm
_decryptWithSession(session, message) {
private decryptWithSession(session: Session, message: OlmMessage): string | undefined {
if (message.type === undefined || message.body === undefined) {
throw new Error("Invalid message without type or body");
}
const olmSession = session.load();
try {
if (isPreKeyMessage(message) && !olmSession.matches_inbound(message.body)) {
if (message.type === OlmPayloadType.PreKey && !olmSession.matches_inbound(message.body)) {
return;
}
try {
const plaintext = olmSession.decrypt(message.type, message.body);
const plaintext = olmSession.decrypt(message.type as number, message.body!);
session.save(olmSession);
session.lastUsed = this._timestamp;
session.data.lastUsed = this.timestamp;
return plaintext;
} catch (err) {
if (isPreKeyMessage(message)) {
if (message.type === OlmPayloadType.PreKey) {
throw new Error(`Error decrypting prekey message with existing session id ${session.id}: ${err.message}`);
}
// decryption failed, bail out
@ -286,27 +303,27 @@ class SenderKeyDecryption {
* @property {Array<DecryptionError>} errors see DecryptionError.event to retrieve the event that failed to decrypt.
*/
class DecryptionChanges {
constructor(senderKeyDecryptions, results, errors, account, lock) {
this._senderKeyDecryptions = senderKeyDecryptions;
this._account = account;
this.results = results;
this.errors = errors;
this._lock = lock;
constructor(
private readonly senderKeyDecryptions: SenderKeyDecryption[],
public readonly results: DecryptionResult[],
public readonly errors: DecryptionError[],
private readonly account: Account,
private readonly lock: ILock
) {}
get hasNewSessions(): boolean {
return this.senderKeyDecryptions.some(skd => skd.hasNewSessions);
}
get hasNewSessions() {
return this._senderKeyDecryptions.some(skd => skd.hasNewSessions);
}
write(txn) {
write(txn: Transaction): void {
try {
for (const senderKeyDecryption of this._senderKeyDecryptions) {
for (const senderKeyDecryption of this.senderKeyDecryptions) {
for (const session of senderKeyDecryption.getModifiedSessions()) {
txn.olmSessions.set(session.data);
if (session.isNew) {
const olmSession = session.load();
try {
this._account.writeRemoveOneTimeKey(olmSession, txn);
this.account.writeRemoveOneTimeKey(olmSession, txn);
} finally {
session.unload(olmSession);
}
@ -322,7 +339,7 @@ class DecryptionChanges {
}
}
} finally {
this._lock.release();
this.lock.release();
}
}
}

View File

@ -16,7 +16,33 @@ limitations under the License.
import {groupByWithCreator} from "../../../utils/groupBy";
import {verifyEd25519Signature, OLM_ALGORITHM} from "../common.js";
import {createSessionEntry} from "./Session.js";
import {createSessionEntry} from "./Session";
import type {OlmMessage, OlmPayload, OlmEncryptedMessageContent} from "./types";
import type {Account} from "../Account";
import type {LockMap} from "../../../utils/LockMap";
import type {Storage} from "../../storage/idb/Storage";
import type {Transaction} from "../../storage/idb/Transaction";
import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore";
import type {HomeServerApi} from "../../net/HomeServerApi";
import type {ILogItem} from "../../../logging/types";
import type * as OlmNamespace from "@matrix-org/olm";
type Olm = typeof OlmNamespace;
type ClaimedOTKResponse = {
[userId: string]: {
[deviceId: string]: {
[algorithmAndOtk: string]: {
key: string,
signatures: {
[userId: string]: {
[algorithmAndDevice: string]: string
}
}
}
}
}
};
function findFirstSessionId(sessionIds) {
return sessionIds.reduce((first, sessionId) => {
@ -36,19 +62,19 @@ const OTK_ALGORITHM = "signed_curve25519";
const MAX_BATCH_SIZE = 20;
export class Encryption {
constructor({account, olm, olmUtil, ownUserId, storage, now, pickleKey, senderKeyLock}) {
this._account = account;
this._olm = olm;
this._olmUtil = olmUtil;
this._ownUserId = ownUserId;
this._storage = storage;
this._now = now;
this._pickleKey = pickleKey;
this._senderKeyLock = senderKeyLock;
}
constructor(
private readonly account: Account,
private readonly pickleKey: string,
private readonly olm: Olm,
private readonly storage: Storage,
private readonly now: () => number,
private readonly ownUserId: string,
private readonly olmUtil: Olm.Utility,
private readonly senderKeyLock: LockMap<string>
) {}
async encrypt(type, content, devices, hsApi, log) {
let messages = [];
async encrypt(type: string, content: Record<string, any>, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise<EncryptedMessage[]> {
let messages: EncryptedMessage[] = [];
for (let i = 0; i < devices.length ; i += MAX_BATCH_SIZE) {
const batchDevices = devices.slice(i, i + MAX_BATCH_SIZE);
const batchMessages = await this._encryptForMaxDevices(type, content, batchDevices, hsApi, log);
@ -57,12 +83,12 @@ export class Encryption {
return messages;
}
async _encryptForMaxDevices(type, content, devices, hsApi, log) {
async _encryptForMaxDevices(type: string, content: Record<string, any>, devices: DeviceIdentity[], hsApi: HomeServerApi, log: ILogItem): Promise<EncryptedMessage[]> {
// TODO: see if we can only hold some of the locks until after the /keys/claim call (if needed)
// take a lock on all senderKeys so decryption and other calls to encrypt (should not happen)
// don't modify the sessions at the same time
const locks = await Promise.all(devices.map(device => {
return this._senderKeyLock.takeLock(device.curve25519Key);
return this.senderKeyLock.takeLock(device.curve25519Key);
}));
try {
const {
@ -70,9 +96,9 @@ export class Encryption {
existingEncryptionTargets,
} = await this._findExistingSessions(devices);
const timestamp = this._now();
const timestamp = this.now();
let encryptionTargets = [];
let encryptionTargets: EncryptionTarget[] = [];
try {
if (devicesWithoutSession.length) {
const newEncryptionTargets = await log.wrap("create sessions", log => this._createNewSessions(
@ -100,8 +126,8 @@ export class Encryption {
}
}
async _findExistingSessions(devices) {
const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]);
async _findExistingSessions(devices: DeviceIdentity[]): Promise<{devicesWithoutSession: DeviceIdentity[], existingEncryptionTargets: EncryptionTarget[]}> {
const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]);
const sessionIdsForDevice = await Promise.all(devices.map(async device => {
return await txn.olmSessions.getSessionIds(device.curve25519Key);
}));
@ -116,18 +142,18 @@ export class Encryption {
const sessionId = findFirstSessionId(sessionIds);
return EncryptionTarget.fromSessionId(device, sessionId);
}
}).filter(target => !!target);
}).filter(target => !!target) as EncryptionTarget[];
return {devicesWithoutSession, existingEncryptionTargets};
}
_encryptForDevice(type, content, target) {
_encryptForDevice(type: string, content: Record<string, any>, target: EncryptionTarget): OlmEncryptedMessageContent {
const {session, device} = target;
const plaintext = JSON.stringify(this._buildPlainTextMessageForDevice(type, content, device));
const message = session.encrypt(plaintext);
const message = session!.encrypt(plaintext);
const encryptedContent = {
algorithm: OLM_ALGORITHM,
sender_key: this._account.identityKeys.curve25519,
sender_key: this.account.identityKeys.curve25519,
ciphertext: {
[device.curve25519Key]: message
}
@ -135,27 +161,27 @@ export class Encryption {
return encryptedContent;
}
_buildPlainTextMessageForDevice(type, content, device) {
_buildPlainTextMessageForDevice(type: string, content: Record<string, any>, device: DeviceIdentity): OlmPayload {
return {
keys: {
"ed25519": this._account.identityKeys.ed25519
"ed25519": this.account.identityKeys.ed25519
},
recipient_keys: {
"ed25519": device.ed25519Key
},
recipient: device.userId,
sender: this._ownUserId,
sender: this.ownUserId,
content,
type
}
}
async _createNewSessions(devicesWithoutSession, hsApi, timestamp, log) {
async _createNewSessions(devicesWithoutSession: DeviceIdentity[], hsApi: HomeServerApi, timestamp: number, log: ILogItem): Promise<EncryptionTarget[]> {
const newEncryptionTargets = await log.wrap("claim", log => this._claimOneTimeKeys(hsApi, devicesWithoutSession, log));
try {
for (const target of newEncryptionTargets) {
const {device, oneTimeKey} = target;
target.session = await this._account.createOutboundOlmSession(device.curve25519Key, oneTimeKey);
target.session = await this.account.createOutboundOlmSession(device.curve25519Key, oneTimeKey);
}
await this._storeSessions(newEncryptionTargets, timestamp);
} catch (err) {
@ -167,12 +193,12 @@ export class Encryption {
return newEncryptionTargets;
}
async _claimOneTimeKeys(hsApi, deviceIdentities, log) {
async _claimOneTimeKeys(hsApi: HomeServerApi, deviceIdentities: DeviceIdentity[], log: ILogItem): Promise<EncryptionTarget[]> {
// create a Map<userId, Map<deviceId, deviceIdentity>>
const devicesByUser = groupByWithCreator(deviceIdentities,
device => device.userId,
() => new Map(),
(deviceMap, device) => deviceMap.set(device.deviceId, device)
(device: DeviceIdentity) => device.userId,
(): Map<string, DeviceIdentity> => new Map(),
(deviceMap: Map<string, DeviceIdentity>, device: DeviceIdentity) => deviceMap.set(device.deviceId, device)
);
const oneTimeKeys = Array.from(devicesByUser.entries()).reduce((usersObj, [userId, deviceMap]) => {
usersObj[userId] = Array.from(deviceMap.values()).reduce((devicesObj, device) => {
@ -188,12 +214,12 @@ export class Encryption {
if (Object.keys(claimResponse.failures).length) {
log.log({l: "failures", servers: Object.keys(claimResponse.failures)}, log.level.Warn);
}
const userKeyMap = claimResponse?.["one_time_keys"];
const userKeyMap = claimResponse?.["one_time_keys"] as ClaimedOTKResponse;
return this._verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log);
}
_verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log) {
const verifiedEncryptionTargets = [];
_verifyAndCreateOTKTargets(userKeyMap: ClaimedOTKResponse, devicesByUser: Map<string, Map<string, DeviceIdentity>>, log: ILogItem): EncryptionTarget[] {
const verifiedEncryptionTargets: EncryptionTarget[] = [];
for (const [userId, userSection] of Object.entries(userKeyMap)) {
for (const [deviceId, deviceSection] of Object.entries(userSection)) {
const [firstPropName, keySection] = Object.entries(deviceSection)[0];
@ -202,7 +228,7 @@ export class Encryption {
const device = devicesByUser.get(userId)?.get(deviceId);
if (device) {
const isValidSignature = verifyEd25519Signature(
this._olmUtil, userId, deviceId, device.ed25519Key, keySection, log);
this.olmUtil, userId, deviceId, device.ed25519Key, keySection, log);
if (isValidSignature) {
const target = EncryptionTarget.fromOTK(device, keySection.key);
verifiedEncryptionTargets.push(target);
@ -214,8 +240,8 @@ export class Encryption {
return verifiedEncryptionTargets;
}
async _loadSessions(encryptionTargets) {
const txn = await this._storage.readTxn([this._storage.storeNames.olmSessions]);
async _loadSessions(encryptionTargets: EncryptionTarget[]): Promise<void> {
const txn = await this.storage.readTxn([this.storage.storeNames.olmSessions]);
// given we run loading in parallel, there might still be some
// storage requests that will finish later once one has failed.
// those should not allocate a session anymore.
@ -223,10 +249,10 @@ export class Encryption {
try {
await Promise.all(encryptionTargets.map(async encryptionTarget => {
const sessionEntry = await txn.olmSessions.get(
encryptionTarget.device.curve25519Key, encryptionTarget.sessionId);
encryptionTarget.device.curve25519Key, encryptionTarget.sessionId!);
if (sessionEntry && !failed) {
const olmSession = new this._olm.Session();
olmSession.unpickle(this._pickleKey, sessionEntry.session);
const olmSession = new this.olm.Session();
olmSession.unpickle(this.pickleKey, sessionEntry.session);
encryptionTarget.session = olmSession;
}
}));
@ -240,12 +266,12 @@ export class Encryption {
}
}
async _storeSessions(encryptionTargets, timestamp) {
const txn = await this._storage.readWriteTxn([this._storage.storeNames.olmSessions]);
async _storeSessions(encryptionTargets: EncryptionTarget[], timestamp: number): Promise<void> {
const txn = await this.storage.readWriteTxn([this.storage.storeNames.olmSessions]);
try {
for (const target of encryptionTargets) {
const sessionEntry = createSessionEntry(
target.session, target.device.curve25519Key, timestamp, this._pickleKey);
target.session!, target.device.curve25519Key, timestamp, this.pickleKey);
txn.olmSessions.set(sessionEntry);
}
} catch (err) {
@ -261,23 +287,24 @@ export class Encryption {
// (and later converted to a session) in case of a new session
// or an existing session
class EncryptionTarget {
constructor(device, oneTimeKey, sessionId) {
this.device = device;
this.oneTimeKey = oneTimeKey;
this.sessionId = sessionId;
// an olmSession, should probably be called olmSession
this.session = null;
}
public session: Olm.Session | null = null;
static fromOTK(device, oneTimeKey) {
constructor(
public readonly device: DeviceIdentity,
public readonly oneTimeKey: string | null,
public readonly sessionId: string | null
) {}
static fromOTK(device: DeviceIdentity, oneTimeKey: string): EncryptionTarget {
return new EncryptionTarget(device, oneTimeKey, null);
}
static fromSessionId(device, sessionId) {
static fromSessionId(device: DeviceIdentity, sessionId: string): EncryptionTarget {
return new EncryptionTarget(device, null, sessionId);
}
dispose() {
dispose(): void {
if (this.session) {
this.session.free();
}
@ -285,8 +312,8 @@ class EncryptionTarget {
}
class EncryptedMessage {
constructor(content, device) {
this.content = content;
this.device = device;
}
constructor(
public readonly content: OlmEncryptedMessageContent,
public readonly device: DeviceIdentity
) {}
}

View File

@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export function createSessionEntry(olmSession, senderKey, timestamp, pickleKey) {
import type {OlmSessionEntry} from "../../storage/idb/stores/OlmSessionStore";
import type * as OlmNamespace from "@matrix-org/olm";
type Olm = typeof OlmNamespace;
export function createSessionEntry(olmSession: Olm.Session, senderKey: string, timestamp: number, pickleKey: string): OlmSessionEntry {
return {
session: olmSession.pickle(pickleKey),
sessionId: olmSession.session_id(),
@ -24,35 +28,38 @@ export function createSessionEntry(olmSession, senderKey, timestamp, pickleKey)
}
export class Session {
constructor(data, pickleKey, olm, isNew = false) {
this.data = data;
this._olm = olm;
this._pickleKey = pickleKey;
this.isNew = isNew;
public isModified: boolean;
constructor(
public readonly data: OlmSessionEntry,
private readonly pickleKey: string,
private readonly olm: Olm,
public isNew: boolean = false
) {
this.isModified = isNew;
}
static create(senderKey, olmSession, olm, pickleKey, timestamp) {
static create(senderKey: string, olmSession: Olm.Session, olm: Olm, pickleKey: string, timestamp: number): Session {
const data = createSessionEntry(olmSession, senderKey, timestamp, pickleKey);
return new Session(data, pickleKey, olm, true);
}
get id() {
get id(): string {
return this.data.sessionId;
}
load() {
const session = new this._olm.Session();
session.unpickle(this._pickleKey, this.data.session);
load(): Olm.Session {
const session = new this.olm.Session();
session.unpickle(this.pickleKey, this.data.session);
return session;
}
unload(olmSession) {
unload(olmSession: Olm.Session): void {
olmSession.free();
}
save(olmSession) {
this.data.session = olmSession.pickle(this._pickleKey);
save(olmSession: Olm.Session): void {
this.data.session = olmSession.pickle(this.pickleKey);
this.isModified = true;
}
}

View File

@ -0,0 +1,48 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const enum OlmPayloadType {
PreKey = 0,
Normal = 1
}
export type OlmMessage = {
type?: OlmPayloadType,
body?: string
}
export type OlmEncryptedMessageContent = {
algorithm?: "m.olm.v1.curve25519-aes-sha2"
sender_key?: string,
ciphertext?: {
[deviceCurve25519Key: string]: OlmMessage
}
}
export type OlmEncryptedEvent = {
type?: "m.room.encrypted",
content?: OlmEncryptedMessageContent
sender?: string
}
export type OlmPayload = {
type?: string;
content?: Record<string, any>;
sender?: string;
recipient?: string;
recipient_keys?: {ed25519?: string};
keys?: {ed25519?: string};
}

View File

@ -18,6 +18,7 @@ import type {HomeServerApi} from "../net/HomeServerApi";
import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage";
import {DummyAuth} from "./stages/DummyAuth";
import {TermsAuth} from "./stages/TermsAuth";
import {TokenAuth} from "./stages/TokenAuth";
import type {
AccountDetails,
RegistrationFlow,
@ -108,6 +109,9 @@ export class Registration {
return new DummyAuth(session, params?.[type]);
case "m.login.terms":
return new TermsAuth(session, params?.[type]);
case "org.matrix.msc3231.login.registration_token":
case "m.login.registration_token":
return new TokenAuth(session, params?.[type], type);
default:
throw new Error(`Unknown stage: ${type}`);
}

View File

@ -0,0 +1,48 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {AuthenticationData, RegistrationParams} from "../types";
import {BaseRegistrationStage} from "./BaseRegistrationStage";
export class TokenAuth extends BaseRegistrationStage {
private _token?: string;
private readonly _type: string;
constructor(session: string, params: RegistrationParams | undefined, type: string) {
super(session, params);
this._type = type;
}
generateAuthenticationData(): AuthenticationData {
if (!this._token) {
throw new Error("No token provided for TokenAuth");
}
return {
session: this._session,
type: this._type,
token: this._token,
};
}
setToken(token: string) {
this._token = token;
}
get type(): string {
return this._type;
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ObservableMap} from "../../../observable/map/ObservableMap.js";
import {ObservableMap} from "../../../observable/map/ObservableMap";
import {RetainedValue} from "../../../utils/RetainedValue";
export class MemberList extends RetainedValue {

View File

@ -24,7 +24,6 @@ import {RoomMember} from "../members/RoomMember.js";
import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js";
import {REDACTION_TYPE} from "../common";
import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js";
import {DecryptionSource} from "../../e2ee/common.js";
import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js";
export class Timeline {

View File

@ -24,19 +24,19 @@ function decodeKey(key: string): { senderKey: string, sessionId: string } {
return {senderKey, sessionId};
}
interface OlmSession {
export type OlmSessionEntry = {
session: string;
sessionId: string;
senderKey: string;
lastUsed: number;
}
type OlmSessionEntry = OlmSession & { key: string };
type OlmSessionStoredEntry = OlmSessionEntry & { key: string };
export class OlmSessionStore {
private _store: Store<OlmSessionEntry>;
private _store: Store<OlmSessionStoredEntry>;
constructor(store: Store<OlmSessionEntry>) {
constructor(store: Store<OlmSessionStoredEntry>) {
this._store = store;
}
@ -55,20 +55,20 @@ export class OlmSessionStore {
return sessionIds;
}
getAll(senderKey: string): Promise<OlmSession[]> {
getAll(senderKey: string): Promise<OlmSessionEntry[]> {
const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
return this._store.selectWhile(range, session => {
return session.senderKey === senderKey;
});
}
get(senderKey: string, sessionId: string): Promise<OlmSession | undefined> {
get(senderKey: string, sessionId: string): Promise<OlmSessionEntry | undefined> {
return this._store.get(encodeKey(senderKey, sessionId));
}
set(session: OlmSession): void {
(session as OlmSessionEntry).key = encodeKey(session.senderKey, session.sessionId);
this._store.put(session as OlmSessionEntry);
set(session: OlmSessionEntry): void {
(session as OlmSessionStoredEntry).key = encodeKey(session.senderKey, session.sessionId);
this._store.put(session as OlmSessionStoredEntry);
}
remove(senderKey: string, sessionId: string): void {

View File

@ -18,14 +18,14 @@ import {SortedMapList} from "./list/SortedMapList.js";
import {FilteredMap} from "./map/FilteredMap.js";
import {MappedMap} from "./map/MappedMap.js";
import {JoinedMap} from "./map/JoinedMap.js";
import {BaseObservableMap} from "./map/BaseObservableMap.js";
import {BaseObservableMap} from "./map/BaseObservableMap";
// re-export "root" (of chain) collections
export { ObservableArray } from "./list/ObservableArray";
export { SortedArray } from "./list/SortedArray";
export { MappedList } from "./list/MappedList";
export { AsyncMappedList } from "./list/AsyncMappedList";
export { ConcatList } from "./list/ConcatList";
export { ObservableMap } from "./map/ObservableMap.js";
export { ObservableMap } from "./map/ObservableMap";
// avoid circular dependency between these classes
// and BaseObservableMap (as they extend it)

View File

@ -133,7 +133,7 @@ export class SortedMapList extends BaseObservableList {
}
}
import {ObservableMap} from "../map/ObservableMap.js";
import {ObservableMap} from "../map/ObservableMap";
export function tests() {
return {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap.js";
import {BaseObservableMap} from "./BaseObservableMap";
export class ApplyMap extends BaseObservableMap {
constructor(source, apply) {

View File

@ -16,7 +16,14 @@ limitations under the License.
import {BaseObservable} from "../BaseObservable";
export class BaseObservableMap extends BaseObservable {
export interface IMapObserver<K, V> {
onReset(): void;
onAdd(key: K, value:V): void;
onUpdate(key: K, value: V, params: any): void;
onRemove(key: K, value: V): void
}
export abstract class BaseObservableMap<K, V> extends BaseObservable<IMapObserver<K, V>> {
emitReset() {
for(let h of this._handlers) {
h.onReset();
@ -24,15 +31,15 @@ export class BaseObservableMap extends BaseObservable {
}
// we need batch events, mostly on index based collection though?
// maybe we should get started without?
emitAdd(key, value) {
emitAdd(key: K, value: V) {
for(let h of this._handlers) {
h.onAdd(key, value);
}
}
emitUpdate(key, value, ...params) {
emitUpdate(key, value, params) {
for(let h of this._handlers) {
h.onUpdate(key, value, ...params);
h.onUpdate(key, value, params);
}
}
@ -42,16 +49,7 @@ export class BaseObservableMap extends BaseObservable {
}
}
[Symbol.iterator]() {
throw new Error("unimplemented");
}
get size() {
throw new Error("unimplemented");
}
// eslint-disable-next-line no-unused-vars
get(key) {
throw new Error("unimplemented");
}
abstract [Symbol.iterator](): Iterator<[K, V]>;
abstract get size(): number;
abstract get(key: K): V | undefined;
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap.js";
import {BaseObservableMap} from "./BaseObservableMap";
export class FilteredMap extends BaseObservableMap {
constructor(source, filter) {
@ -166,7 +166,7 @@ class FilterIterator {
}
}
import {ObservableMap} from "./ObservableMap.js";
import {ObservableMap} from "./ObservableMap";
export function tests() {
return {
"filter preloaded list": assert => {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap.js";
import {BaseObservableMap} from "./BaseObservableMap";
export class JoinedMap extends BaseObservableMap {
constructor(sources) {
@ -191,7 +191,7 @@ class SourceSubscriptionHandler {
}
import { ObservableMap } from "./ObservableMap.js";
import { ObservableMap } from "./ObservableMap";
export function tests() {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap.js";
import {BaseObservableMap} from "./BaseObservableMap";
export class LogMap extends BaseObservableMap {
constructor(source, log) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap.js";
import {BaseObservableMap} from "./BaseObservableMap";
/*
so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate that is passed in the mapping function
how should the mapped value be notified of an update though? and can it then decide to not propagate the update?

View File

@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap.js";
import {BaseObservableMap} from "./BaseObservableMap";
export class ObservableMap extends BaseObservableMap {
constructor(initialValues) {
export class ObservableMap<K, V> extends BaseObservableMap<K, V> {
private readonly _values: Map<K, V>;
constructor(initialValues?: (readonly [K, V])[]) {
super();
this._values = new Map(initialValues);
}
update(key, params) {
update(key: K, params?: any): boolean {
const value = this._values.get(key);
if (value !== undefined) {
// could be the same value, so it's already updated
@ -34,7 +36,7 @@ export class ObservableMap extends BaseObservableMap {
return false; // or return existing value?
}
add(key, value) {
add(key: K, value: V): boolean {
if (!this._values.has(key)) {
this._values.set(key, value);
this.emitAdd(key, value);
@ -43,7 +45,7 @@ export class ObservableMap extends BaseObservableMap {
return false; // or return existing value?
}
remove(key) {
remove(key: K): boolean {
const value = this._values.get(key);
if (value !== undefined) {
this._values.delete(key);
@ -54,39 +56,39 @@ export class ObservableMap extends BaseObservableMap {
}
}
set(key, value) {
set(key: K, value: V): boolean {
if (this._values.has(key)) {
// We set the value here because update only supports inline updates
this._values.set(key, value);
return this.update(key);
return this.update(key, undefined);
}
else {
return this.add(key, value);
}
}
reset() {
reset(): void {
this._values.clear();
this.emitReset();
}
get(key) {
get(key: K): V | undefined {
return this._values.get(key);
}
get size() {
get size(): number {
return this._values.size;
}
[Symbol.iterator]() {
[Symbol.iterator](): Iterator<[K, V]> {
return this._values.entries();
}
values() {
values(): Iterator<V> {
return this._values.values();
}
keys() {
keys(): Iterator<K> {
return this._values.keys();
}
}
@ -105,13 +107,16 @@ export function tests() {
test_add(assert) {
let fired = 0;
const map = new ObservableMap();
const map = new ObservableMap<number, {value: number}>();
map.subscribe({
onAdd(key, value) {
fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {value: 5});
}
},
onUpdate() {},
onRemove() {},
onReset() {}
});
map.add(1, {value: 5});
assert.equal(map.size, 1);
@ -120,7 +125,7 @@ export function tests() {
test_update(assert) {
let fired = 0;
const map = new ObservableMap();
const map = new ObservableMap<number, {number: number}>();
const value = {number: 5};
map.add(1, value);
map.subscribe({
@ -129,7 +134,10 @@ export function tests() {
assert.equal(key, 1);
assert.deepEqual(value, {number: 6});
assert.equal(params, "test");
}
},
onAdd() {},
onRemove() {},
onReset() {}
});
value.number = 6;
map.update(1, "test");
@ -138,9 +146,12 @@ export function tests() {
test_update_unknown(assert) {
let fired = 0;
const map = new ObservableMap();
const map = new ObservableMap<number, {number: number}>();
map.subscribe({
onUpdate() { fired += 1; }
onUpdate() { fired += 1; },
onAdd() {},
onRemove() {},
onReset() {}
});
const result = map.update(1);
assert.equal(fired, 0);
@ -149,7 +160,7 @@ export function tests() {
test_set(assert) {
let add_fired = 0, update_fired = 0;
const map = new ObservableMap();
const map = new ObservableMap<number, {value: number}>();
map.subscribe({
onAdd(key, value) {
add_fired += 1;
@ -160,7 +171,9 @@ export function tests() {
update_fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {value: 7});
}
},
onRemove() {},
onReset() {}
});
// Add
map.set(1, {value: 5});
@ -174,7 +187,7 @@ export function tests() {
test_remove(assert) {
let fired = 0;
const map = new ObservableMap();
const map = new ObservableMap<number, {value: number}>();
const value = {value: 5};
map.add(1, value);
map.subscribe({
@ -182,7 +195,10 @@ export function tests() {
fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {value: 5});
}
},
onAdd() {},
onUpdate() {},
onReset() {}
});
map.remove(1);
assert.equal(map.size, 0);
@ -190,8 +206,8 @@ export function tests() {
},
test_iterate(assert) {
const results = [];
const map = new ObservableMap();
const results: any[] = [];
const map = new ObservableMap<number, {number: number}>();
map.add(1, {number: 5});
map.add(2, {number: 6});
map.add(3, {number: 7});
@ -204,7 +220,7 @@ export function tests() {
assert.equal(results.find(([key]) => key === 3)[1].number, 7);
},
test_size(assert) {
const map = new ObservableMap();
const map = new ObservableMap<number, {number: number}>();
map.add(1, {number: 5});
map.add(2, {number: 6});
assert.equal(map.size, 2);

View File

@ -37,7 +37,7 @@ import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandl
import {downloadInIframe} from "./dom/download.js";
import {Disposables} from "../../utils/Disposables";
import {parseHTML} from "./parsehtml.js";
import {handleAvatarError} from "./ui/avatar.js";
import {handleAvatarError} from "./ui/avatar";
function addScript(src) {
return new Promise(function (resolve, reject) {
@ -143,7 +143,10 @@ export class Platform {
this._serviceWorkerHandler.registerAndStart(assetPaths.serviceWorker);
}
this.notificationService = new NotificationService(this._serviceWorkerHandler, config.push);
this.crypto = new Crypto(cryptoExtras);
// Only try to use crypto when olm is provided
if(this._assetPaths.olm) {
this.crypto = new Crypto(cryptoExtras);
}
this.storageFactory = new StorageFactory(this._serviceWorkerHandler);
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
this.estimateStorageUsage = estimateStorageUsage;

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import {BaseUpdateView} from "./general/BaseUpdateView";
import {renderStaticAvatar, renderImg} from "./avatar.js";
import {renderStaticAvatar, renderImg} from "./avatar";
/*
optimization to not use a sub view when changing between img and text

View File

@ -104,8 +104,9 @@ export const TAG_NAMES = {
"br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
"p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote",
"table", "thead", "tbody", "tr", "th", "td", "hr",
"pre", "code", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"],
[SVG_NS]: ["svg", "circle"]
"pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form",
"progress", "output", "video"],
[SVG_NS]: ["svg", "g", "path", "circle", "ellipse", "rect", "use"]
} as const;
export const tag: { [tagName in typeof TAG_NAMES[string][number]]: (attributes?: BasicAttributes<never> | Child | Child[], children?: Child | Child[]) => Element } = {} as any;

View File

@ -16,7 +16,7 @@ limitations under the License.
*/
import {TemplateView} from "../../general/TemplateView";
import {renderStaticAvatar} from "../../avatar.js";
import {renderStaticAvatar} from "../../avatar";
export class InviteView extends TemplateView {
render(t, vm) {

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {renderStaticAvatar} from "../../../avatar.js";
import {renderStaticAvatar} from "../../../avatar";
import {tag} from "../../../general/html";
import {mountView} from "../../../general/utils";
import {TemplateView} from "../../../general/TemplateView";
@ -40,14 +40,17 @@ export class BaseMessageView extends TemplateView {
if (this._interactive) {
children.push(t.button({className: "Timeline_messageOptions"}, "⋯"));
}
const li = t.el(this._tagName, {className: {
"Timeline_message": true,
own: vm.isOwn,
unsent: vm.isUnsent,
unverified: vm.isUnverified,
disabled: !this._interactive,
continuation: vm => vm.isContinuation,
}}, children);
const li = t.el(this._tagName, {
className: {
"Timeline_message": true,
own: vm.isOwn,
unsent: vm.isUnsent,
unverified: vm.isUnverified,
disabled: !this._interactive,
continuation: vm => vm.isContinuation,
},
'data-event-id': vm.eventId
}, children);
// given that there can be many tiles, we don't add
// unneeded DOM nodes in case of a continuation, and we add it
// with a side-effect binding to not have to create sub views,

View File

@ -1,5 +1,6 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,7 +19,7 @@ export interface IDisposable {
dispose(): void;
}
type Disposable = IDisposable | (() => void);
export type Disposable = IDisposable | (() => void);
function disposeValue(value: Disposable): void {
if (typeof value === "function") {
@ -33,9 +34,9 @@ function isDisposable(value: Disposable): boolean {
}
export class Disposables {
private _disposables: Disposable[] | null = [];
private _disposables?: Disposable[] = [];
track(disposable: Disposable): Disposable {
track<D extends Disposable>(disposable: D): D {
if (!isDisposable(disposable)) {
throw new Error("Not a disposable");
}
@ -48,16 +49,16 @@ export class Disposables {
return disposable;
}
untrack(disposable: Disposable): null {
untrack(disposable: Disposable): undefined {
if (this.isDisposed) {
console.warn("Disposables already disposed, cannot untrack");
return null;
return undefined;
}
const idx = this._disposables!.indexOf(disposable);
if (idx >= 0) {
this._disposables!.splice(idx, 1);
}
return null;
return undefined;
}
dispose(): void {
@ -65,17 +66,17 @@ export class Disposables {
for (const d of this._disposables) {
disposeValue(d);
}
this._disposables = null;
this._disposables = undefined;
}
}
get isDisposed(): boolean {
return this._disposables === null;
return this._disposables === undefined;
}
disposeTracked(value: Disposable): null {
disposeTracked(value: Disposable | undefined): undefined {
if (value === undefined || value === null || this.isDisposed) {
return null;
return undefined;
}
const idx = this._disposables!.indexOf(value);
if (idx !== -1) {
@ -84,6 +85,6 @@ export class Disposables {
} else {
console.warn("disposable not found, did it leak?", value);
}
return null;
return undefined;
}
}

View File

@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export class Lock {
export interface ILock {
release(): void;
}
export class Lock implements ILock {
private _promise?: Promise<void>;
private _resolve?: (() => void);
@ -52,7 +56,7 @@ export class Lock {
}
}
export class MultiLock {
export class MultiLock implements ILock {
constructor(public readonly locks: Lock[]) {
}

View File

@ -1133,6 +1133,13 @@ nth-check@^2.0.0:
dependencies:
boolbase "^1.0.0"
off-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/off-color/-/off-color-2.0.0.tgz#ecf3bda52e9a78dde535db86361e048741a56631"
integrity sha512-JJ9ObbY2CzgT7F8PpdpHGNjQa7QbU8f4DkY3cCxYUq9NezYUMmL/oSofCc5MMaiUnNNBEFCc4w1unMA+R8syvw==
dependencies:
core-js "^3.6.5"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -1215,6 +1222,11 @@ postcss-flexbugs-fixes@^5.0.2:
resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d"
integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==
postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.3.8:
version "8.3.9"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.9.tgz#98754caa06c4ee9eb59cc48bd073bb6bd3437c31"