Compare commits

...

No commits in common. "librepages" and "master" have entirely different histories.

645 changed files with 64267 additions and 554 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
target

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
# Matches multiple files with brace expansion notation
# Set default charset
# [*.{js,py}]

25
.eslintrc.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
"env": {
"browser": 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"
},
"globals": {
"DEFINE_VERSION": "readonly",
"DEFINE_GLOBAL_HASH": "readonly",
// only available in sw.js
"DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly"
}
};

47
.github/workflows/codechecks.js.yml vendored Normal file
View File

@ -0,0 +1,47 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# yarn cache setup from https://www.karltarvas.com/2020/12/09/github-actions-cache-yarn-install.html
name: Code checks
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- name: Checkout source
uses: actions/checkout@v2
- name: Install tools
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
# See: https://github.com/actions/cache/blob/main/examples.md#node---yarn
- name: Get Yarn cache directory
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Use Yarn cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn install --prefer-offline --frozen-lockfile
- name: Unit tests
run: yarn test
- name: Lint
run: yarn run lint-ci
- name: Typescript
run: yarn run tsc

44
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Container Image
on:
push:
branches: [ master ]
tags: [ 'v*' ]
pull_request:
branches: [ master ]
env:
IMAGE_NAME: ${{ github.repository }}
REGISTRY: ghcr.io
jobs:
push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
*.sublime-project
*.sublime-workspace
.DS_Store
node_modules
fetchlogs
sessionexports
bundle.js
target
lib
*.tar.gz
.eslintcache
.tmp
tmp/

61
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,61 @@
image: docker.io/alpine
stages:
- test
- build
.yarn-template:
image: docker.io/node
before_script:
- yarn install
cache:
paths:
- node_modules
test:
extends: .yarn-template
stage: test
script:
- yarn test
build:
extends: .yarn-template
stage: build
script:
- yarn build
artifacts:
paths:
- target
.docker-template:
image: docker.io/docker
stage: build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
docker-release:
extends: .docker-template
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
script:
- docker build --pull -t "${CI_REGISTRY_IMAGE}:latest" -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}" .
- docker push "${CI_REGISTRY_IMAGE}:latest"
- docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}"
docker-tags:
extends: .docker-template
rules:
- if: '$CI_COMMIT_TAG && $CI_COMMIT_TAG !~ /^v\d+\.\d+\.\d+$/'
script:
- docker build --pull -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}" .
- docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}"
docker-branches:
extends: .docker-template
rules:
- if: $CI_COMMIT_BRANCH
script:
- docker build --pull -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}" .
- docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}"

25
.ts-eslintrc.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
root: true,
env: {
"browser": true,
"es6": true
},
extends: [
// "plugin:@typescript-eslint/recommended",
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
],
parser: '@typescript-eslint/parser',
parserOptions: {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
plugins: [
'@typescript-eslint',
],
rules: {
"@typescript-eslint/no-floating-promises": 2,
"@typescript-eslint/no-misused-promises": 2,
"semi": ["error", "always"]
}
};

18
.woodpecker.yml Normal file
View File

@ -0,0 +1,18 @@
pipeline:
buildfrontend:
image: node:16
commands:
- yarn install --prefer-offline --frozen-lockfile
- yarn test
- yarn run lint-ci
- yarn run tsc
- yarn build
deploy:
image: python
when:
event: push
branch: master
commands:
- make ci-deploy
secrets: [ GITEA_WRITE_DEPLOY_KEY, LIBREPAGES_DEPLOY_SECRET ]

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

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM docker.io/node:alpine as builder
RUN apk add --no-cache git python3 build-base
COPY . /app
WORKDIR /app
RUN yarn install \
&& yarn build
FROM docker.io/nginx:alpine
COPY --from=builder /app/target /usr/share/nginx/html

7
Dockerfile-dev Normal file
View File

@ -0,0 +1,7 @@
FROM docker.io/node:alpine
RUN apk add --no-cache git python3 build-base
COPY . /code
WORKDIR /code
RUN yarn install
EXPOSE 3000
ENTRYPOINT ["yarn", "start"]

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

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
ci-deploy: ## Deploy from CI/CD. Only call from within CI
@if [ "${CI}" != "woodpecker" ]; \
then echo "Only call from within CI. Will re-write your local Git configuration. To override, set export CI=woodpecker"; \
exit 1; \
fi
git config --global user.email "${CI_COMMIT_AUTHOR_EMAIL}"
git config --global user.name "${CI_COMMIT_AUTHOR}"
./scripts/ci.sh --commit-files librepages target "${CI_COMMIT_AUTHOR} <${CI_COMMIT_AUTHOR_EMAIL}>"
./scripts/ci.sh --init "$$GITEA_WRITE_DEPLOY_KEY"
./scripts/ci.sh --deploy ${LIBREPAGES_DEPLOY_SECRET} librepages
./scripts/ci.sh --clean
help: ## Prints help for targets with comments
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

46
README.md Normal file
View File

@ -0,0 +1,46 @@
[![status-badge](https://ci.batsense.net/api/badges/mystiq/hydrogen-web/status.svg)](https://ci.batsense.net/mystiq/hydrogen-web)
# Hydrogen
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. Bug reports are welcome, but please don't file any feature requests or other missing things to be on par with Element Web.
## 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
For embedded usage, see the [SDK instructions](doc/SDK.md).
If you find this interesting, come and discuss on [`#hydrogen:matrix.org`](https://matrix.to/#/#hydrogen:matrix.org).
# How to use
Hydrogen is deployed to [hydrogen.element.io](https://hydrogen.element.io). You can also deploy Hydrogen on your own web server:
1. Download the [latest release package](https://github.com/vector-im/hydrogen-web/releases).
1. Extract the package to the public directory of your web server.
1. If this is your first deploy:
1. copy `config.sample.json` to `config.json` and if needed, make any modifications (unless you've set up your own [sygnal](https://github.com/matrix-org/sygnal) instance, you don't need to change anything in the `push` section).
1. Disable caching entirely on the server for:
- `index.html`
- `sw.js`
- `config.json`
- All theme manifests referenced in the `themeManifests` of `config.json`, these files are typically called `theme-{name}.json`.
These resources will still be cached client-side by the service worker. Because of this; you'll still need to refresh the app twice before config.json changes are applied.
## Set up a dev environment
You can run Hydrogen locally by the following commands in the terminal:
- `yarn install` (only the first time)
- `yarn start` in the terminal
Now point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md).
# FAQ
Some frequently asked questions are answered [here](doc/FAQ.md).

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

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="17"
height="9"
viewBox="0 0 17 9"
fill="none"
version="1.1"
id="svg839"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<g
clip-path="url(#clip0)"
id="g832"
transform="rotate(-90,4.3001277,4.8826258)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 8.20723,2.70711 C 8.59775,3.09763 8.59878,3.73182 8.20952,4.1236 L 3.27581,9.08934 8.22556,14.0391 c 0.39052,0.3905 0.39155,1.0247 0.00229,1.4165 -0.38926,0.3918 -1.0214,0.3928 -1.41192,0.0023 L 1.15907,9.80101 C 0.768549,9.41049 0.767523,8.7763 1.15678,8.38452 L 6.79531,2.70939 C 7.18457,2.31761 7.8167,2.31658 8.20723,2.70711 Z"
fill="#909090"
id="path830" />
</g>
<defs
id="defs837">
<clipPath
id="clip0">
<rect
width="8"
height="17"
fill="#ffffff"
transform="rotate(180,4.25,8.5)"
id="rect834"
x="0"
y="0" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +0,0 @@
<svg width="17" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)" transform="rotate(-90 4.3 4.883)"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.207 2.707c.39.39.392 1.025.003 1.417L3.276 9.089l4.95 4.95c.39.39.391 1.025.002 1.417a.996.996 0 0 1-1.412.002L1.159 9.8a1.004 1.004 0 0 1-.002-1.416l5.638-5.676a.996.996 0 0 1 1.412-.002Z" fill="#ff00ff"/></g><defs><clipPath id="a"><path fill="#ffffff" transform="rotate(180 4.25 8.5)" d="M0 0h8v17H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 512 B

View File

@ -1,10 +0,0 @@
<svg width="9" height="17" viewBox="0 0 9 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.20723 2.70711C8.59775 3.09763 8.59878 3.73182 8.20952 4.1236L3.27581 9.08934L8.22556 14.0391C8.61608 14.4296 8.61711 15.0638 8.22785 15.4556C7.83859 15.8474 7.20645 15.8484 6.81593 15.4579L1.15907 9.80101C0.768549 9.41049 0.767523 8.7763 1.15678 8.38452L6.79531 2.70939C7.18457 2.31761 7.8167 2.31658 8.20723 2.70711Z" fill="#8d97a5"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="8" height="17" fill="white" transform="translate(8.5 17) rotate(-180)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 657 B

View File

@ -1 +0,0 @@
<svg width="9" height="17" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.207 2.707c.39.39.392 1.025.003 1.417L3.276 9.089l4.95 4.95c.39.39.391 1.025.002 1.417a.996.996 0 0 1-1.412.002L1.159 9.8a1.004 1.004 0 0 1-.002-1.416l5.638-5.676a.996.996 0 0 1 1.412-.002Z" fill="#ff00ff"/></g><defs><clipPath id="a"><path fill="white" transform="rotate(-180 4.25 8.5)" d="M0 0h8v17H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 477 B

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18L15 12L9 6" stroke="#909090" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 212 B

View File

@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m9 18 6-6-6-6" stroke="#ff00ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 187 B

View File

@ -1,3 +0,0 @@
<svg width="7" height="11" viewBox="0 0 7 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.96967 10.7197C0.676777 10.4268 0.676007 9.95117 0.967952 9.65733L4.66823 5.93303L0.955922 2.22072C0.663029 1.92782 0.662259 1.45218 0.954204 1.15834C1.24615 0.864504 1.72025 0.863737 2.01315 1.15663L6.25579 5.39927C6.54868 5.69216 6.54945 6.1678 6.2575 6.46164L2.02861 10.718C1.73667 11.0118 1.26256 11.0126 0.96967 10.7197Z" fill="#909090"/>
</svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@ -1 +0,0 @@
<svg width="7" height="11" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M.97 10.72a.753.753 0 0 1-.002-1.063l3.7-3.724L.956 2.221a.753.753 0 0 1-.002-1.063.747.747 0 0 1 1.06-.001l4.242 4.242a.753.753 0 0 1 .002 1.063l-4.23 4.256a.747.747 0 0 1-1.058.002Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 330 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 6L7.5 9L10.5 12" stroke="#909090" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 220 B

View File

@ -1 +0,0 @@
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m10.5 6-3 3 3 3" stroke="#ff00ff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 211 B

View File

@ -1 +0,0 @@
<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m1.333 1.333 5.333 5.333M6.667 1.333 1.334 6.666" stroke="#ff00ff" stroke-width="1.5" stroke-linecap="round"/></svg>

Before

Width:  |  Height:  |  Size: 198 B

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 4C0 1.79086 1.79086 0 4 0H12C14.2091 0 16 1.79086 16 4V12C16 14.2091 14.2091 16 12 16H4C1.79086 16 0 14.2091 0 12V4ZM1.5 4.75C1.5 4.19772 1.94772 3.75 2.5 3.75H3.5C4.05228 3.75 4.5 4.19772 4.5 4.75V11.25C4.5 11.8023 4.05228 12.25 3.5 12.25H2.5C1.94772 12.25 1.5 11.8023 1.5 11.25V4.75ZM7 3.75C6.44772 3.75 6 4.19772 6 4.75V11.25C6 11.8023 6.44772 12.25 7 12.25H13.5C14.0523 12.25 14.5 11.8023 14.5 11.25V4.75C14.5 4.19772 14.0523 3.75 13.5 3.75H7Z" fill="#8d97a5"/>
</svg>

Before

Width:  |  Height:  |  Size: 621 B

View File

@ -1 +0,0 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 4a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4Zm1.5.75a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v6.5a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-6.5Zm5.5-1a1 1 0 0 0-1 1v6.5a1 1 0 0 0 1 1h6.5a1 1 0 0 0 1-1v-6.5a1 1 0 0 0-1-1H7Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 373 B

View File

@ -1,4 +0,0 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33313 1.33313L6.66646 6.66646" stroke="#21262b" stroke-width="1.5" stroke-linecap="round"/>
<path d="M6.66699 1.33313L1.33366 6.66646" stroke="#21262b" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 307 B

View File

@ -1,4 +0,0 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33313 1.33313L6.66646 6.66646" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
<path d="M6.66699 1.33313L1.33366 6.66646" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 301 B

View File

@ -1 +0,0 @@
<svg width="18" height="18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.38 12.27c.38-.85.62-1.84.62-3V3.05L9 1 5.22 2.11l10.16 10.16ZM2.21 2.99 2 3.05v6.22C2 15.63 9 17 9 17s2.71-.53 4.76-2.47L2.21 2.99Z" fill="#ff00ff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M.47.47a.75.75 0 0 1 1.06 0l15.19 15.19a.75.75 0 1 1-1.06 1.06L.47 1.53a.75.75 0 0 1 0-1.06Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 402 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 9.27V3.05L9 1l7 2.05v6.22C16 15.63 9 17 9 17s-7-1.37-7-7.73Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 168 B

View File

@ -1,6 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.28 2.88C17.28 1.28942 18.5694 0 20.16 0C30.7639 0 39.36 8.59613 39.36 19.2C39.36 20.7906 38.0706 22.08 36.48 22.08C34.8894 22.08 33.6 20.7906 33.6 19.2C33.6 11.7773 27.5827 5.76 20.16 5.76C18.5694 5.76 17.28 4.47058 17.28 2.88Z" fill="#03b381"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.72 45.12C30.72 46.7106 29.4306 48 27.84 48C17.2361 48 8.64 39.4039 8.64 28.8C8.64 27.2094 9.92942 25.92 11.52 25.92C13.1106 25.92 14.4 27.2094 14.4 28.8C14.4 36.2227 20.4173 42.24 27.84 42.24C29.4306 42.24 30.72 43.5294 30.72 45.12Z" fill="#03b381"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.88 30.72C1.28942 30.72 -5.63623e-08 29.4306 -1.25889e-07 27.84C-5.89399e-07 17.2361 8.59613 8.63997 19.2 8.63997C20.7906 8.63997 22.08 9.92939 22.08 11.52C22.08 13.1106 20.7906 14.4 19.2 14.4C11.7773 14.4 5.76 20.4173 5.76 27.84C5.76 29.4306 4.47058 30.72 2.88 30.72Z" fill="#03b381"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.12 17.28C46.7106 17.28 48 18.5694 48 20.16C48 30.7639 39.4039 39.36 28.8 39.36C27.2094 39.36 25.92 38.0706 25.92 36.48C25.92 34.8894 27.2094 33.6 28.8 33.6C36.2227 33.6 42.24 27.5827 42.24 20.16C42.24 18.5694 43.5294 17.28 45.12 17.28Z" fill="#03b381"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M17.28 2.88A2.88 2.88 0 0 1 20.16 0c10.604 0 19.2 8.596 19.2 19.2a2.88 2.88 0 1 1-5.76 0c0-7.423-6.017-13.44-13.44-13.44a2.88 2.88 0 0 1-2.88-2.88ZM30.72 45.12A2.88 2.88 0 0 1 27.84 48c-10.604 0-19.2-8.596-19.2-19.2a2.88 2.88 0 1 1 5.76 0c0 7.423 6.017 13.44 13.44 13.44a2.88 2.88 0 0 1 2.88 2.88ZM2.88 30.72A2.88 2.88 0 0 1 0 27.84c0-10.604 8.596-19.2 19.2-19.2a2.88 2.88 0 1 1 0 5.76c-7.423 0-13.44 6.017-13.44 13.44a2.88 2.88 0 0 1-2.88 2.88ZM45.12 17.28A2.88 2.88 0 0 1 48 20.16c0 10.604-8.596 19.2-19.2 19.2a2.88 2.88 0 1 1 0-5.76c7.423 0 13.44-6.017 13.44-13.44a2.88 2.88 0 0 1 2.88-2.88Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 742 B

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 0C1.79086 0 0 1.79086 0 4V12C0 14.2091 1.79086 16 4 16H12C14.2091 16 16 14.2091 16 12V4C16 1.79086 14.2091 0 12 0H4ZM2.5 3.75C1.94772 3.75 1.5 4.19772 1.5 4.75V11.25C1.5 11.8023 1.94772 12.25 2.5 12.25H3.5C4.05228 12.25 4.5 11.8023 4.5 11.25V4.75C4.5 4.19772 4.05228 3.75 3.5 3.75H2.5ZM11 9.75C11 9.19771 11.4477 8.75 12 8.75H13.5C14.0523 8.75 14.5 9.19772 14.5 9.75V11.25C14.5 11.8023 14.0523 12.25 13.5 12.25H12C11.4477 12.25 11 11.8023 11 11.25V9.75ZM7 8.75C6.44772 8.75 6 9.19771 6 9.75V11.25C6 11.8023 6.44772 12.25 7 12.25H8.5C9.05228 12.25 9.5 11.8023 9.5 11.25V9.75C9.5 9.19772 9.05229 8.75 8.5 8.75H7ZM11 4.75C11 4.19772 11.4477 3.75 12 3.75H13.5C14.0523 3.75 14.5 4.19772 14.5 4.75V6.25C14.5 6.80228 14.0523 7.25 13.5 7.25H12C11.4477 7.25 11 6.80228 11 6.25V4.75ZM7 3.75C6.44772 3.75 6 4.19772 6 4.75V6.25C6 6.80228 6.44772 7.25 7 7.25H8.5C9.05228 7.25 9.5 6.80228 9.5 6.25V4.75C9.5 4.19772 9.05229 3.75 8.5 3.75H7Z" fill="#909090"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 0a4 4 0 0 0-4 4v8a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4H4ZM2.5 3.75a1 1 0 0 0-1 1v6.5a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1v-6.5a1 1 0 0 0-1-1h-1Zm8.5 6a1 1 0 0 1 1-1h1.5a1 1 0 0 1 1 1v1.5a1 1 0 0 1-1 1H12a1 1 0 0 1-1-1v-1.5Zm-4-1a1 1 0 0 0-1 1v1.5a1 1 0 0 0 1 1h1.5a1 1 0 0 0 1-1v-1.5a1 1 0 0 0-1-1H7Zm4-4a1 1 0 0 1 1-1h1.5a1 1 0 0 1 1 1v1.5a1 1 0 0 1-1 1H12a1 1 0 0 1-1-1v-1.5Zm-4-1a1 1 0 0 0-1 1v1.5a1 1 0 0 0 1 1h1.5a1 1 0 0 0 1-1v-1.5a1 1 0 0 0-1-1H7Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 607 B

View File

@ -1,3 +0,0 @@
<svg width="25" height="24" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 19C9 19 18 15.2 18 9.50002V2.85001L9 1.52588e-05L0 2.85001L0 9.50002C0 15.2 9 19 9 19Z" fill="#909090"/>
</svg>

Before

Width:  |  Height:  |  Size: 260 B

View File

@ -1 +0,0 @@
<svg width="25" height="24" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9 19s9-3.8 9-9.5V2.85L9 0 0 2.85V9.5C0 15.2 9 19 9 19Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 223 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"name":"Hydrogen","short_name":"Hydrogen","display":"standalone","description":"Lightweight matrix client with legacy and mobile browser support","start_url":"../index.html","icons":[{"src":"icon.2a39c64c.png","sizes":"384x384","type":"image/png"},{"src":"icon-maskable.965d12c4.png","sizes":"384x384","type":"image/png","purpose":"maskable"}],"theme_color":"#0DBD8B"}

View File

@ -1,161 +0,0 @@
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
// @source: https://gitlab.matrix.org/matrix-org/olm/-/tree/3.2.8
var Olm = (function() {
var olm_exports = {};
var onInitSuccess;
var onInitFail;
var Module = (function() {
var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename;
return (
function(Module) {
Module = Module || {};
var a;a||(a=typeof Module !== 'undefined' ? Module : {});var aa,ba;a.ready=new Promise(function(b,c){aa=b;ba=c});var g;if("undefined"!==typeof window)g=function(b){window.crypto.getRandomValues(b)};else if(module.exports){var ca=require("crypto");g=function(b){var c=ca.randomBytes(b.length);b.set(c)};process=global.process}else throw Error("Cannot find global to attach library to");
if("undefined"!==typeof OLM_OPTIONS)for(var da in OLM_OPTIONS)OLM_OPTIONS.hasOwnProperty(da)&&(a[da]=OLM_OPTIONS[da]);a.onRuntimeInitialized=function(){h=a._olm_error();olm_exports.PRIVATE_KEY_LENGTH=a._olm_pk_private_key_length();onInitSuccess&&onInitSuccess()};a.onAbort=function(b){onInitFail&&onInitFail(b)};var ea={},l;for(l in a)a.hasOwnProperty(l)&&(ea[l]=a[l]);var ha="object"===typeof window,ia="function"===typeof importScripts,m="",ja,ka,la,n,q;
if("object"===typeof process&&"object"===typeof process.versions&&"string"===typeof process.versions.node)m=ia?require("path").dirname(m)+"/":__dirname+"/",ja=function(b,c){n||(n=require("fs"));q||(q=require("path"));b=q.normalize(b);return n.readFileSync(b,c?null:"utf8")},la=function(b){b=ja(b,!0);b.buffer||(b=new Uint8Array(b));b.buffer||r("Assertion failed: undefined");return b},ka=function(b,c,d){n||(n=require("fs"));q||(q=require("path"));b=q.normalize(b);n.readFile(b,function(e,f){e?d(e):c(f.buffer)})},
1<process.argv.length&&process.argv[1].replace(/\\/g,"/"),process.argv.slice(2),process.on("uncaughtException",function(b){throw b;}),process.on("unhandledRejection",r),a.inspect=function(){return"[Emscripten Module object]"};else if(ha||ia)ia?m=self.location.href:"undefined"!==typeof document&&document.currentScript&&(m=document.currentScript.src),_scriptDir&&(m=_scriptDir),0!==m.indexOf("blob:")?m=m.substr(0,m.lastIndexOf("/")+1):m="",ja=function(b){var c=new XMLHttpRequest;c.open("GET",b,!1);c.send(null);
return c.responseText},ia&&(la=function(b){var c=new XMLHttpRequest;c.open("GET",b,!1);c.responseType="arraybuffer";c.send(null);return new Uint8Array(c.response)}),ka=function(b,c,d){var e=new XMLHttpRequest;e.open("GET",b,!0);e.responseType="arraybuffer";e.onload=function(){200==e.status||0==e.status&&e.response?c(e.response):d()};e.onerror=d;e.send(null)};a.print||console.log.bind(console);var ma=a.printErr||console.warn.bind(console);for(l in ea)ea.hasOwnProperty(l)&&(a[l]=ea[l]);ea=null;var na;
a.wasmBinary&&(na=a.wasmBinary);var noExitRuntime=a.noExitRuntime||!0;"object"!==typeof WebAssembly&&r("no native wasm support detected");
function t(b){var c="i8";"*"===c.charAt(c.length-1)&&(c="i32");switch(c){case "i1":u[b>>0]=0;break;case "i8":u[b>>0]=0;break;case "i16":oa[b>>1]=0;break;case "i32":v[b>>2]=0;break;case "i64":pa=[0,(x=0,1<=+Math.abs(x)?0<x?(Math.min(+Math.floor(x/4294967296),4294967295)|0)>>>0:~~+Math.ceil((x-+(~~x>>>0))/4294967296)>>>0:0)];v[b>>2]=pa[0];v[b+4>>2]=pa[1];break;case "float":qa[b>>2]=0;break;case "double":ra[b>>3]=0;break;default:r("invalid type for setValue: "+c)}}
function sa(b,c){c=c||"i8";"*"===c.charAt(c.length-1)&&(c="i32");switch(c){case "i1":return u[b>>0];case "i8":return u[b>>0];case "i16":return oa[b>>1];case "i32":return v[b>>2];case "i64":return v[b>>2];case "float":return qa[b>>2];case "double":return ra[b>>3];default:r("invalid type for getValue: "+c)}return null}var ta,ua=!1,va="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0;
function y(b,c){if(b){var d=z,e=b+c;for(c=b;d[c]&&!(c>=e);)++c;if(16<c-b&&d.subarray&&va)b=va.decode(d.subarray(b,c));else{for(e="";b<c;){var f=d[b++];if(f&128){var k=d[b++]&63;if(192==(f&224))e+=String.fromCharCode((f&31)<<6|k);else{var p=d[b++]&63;f=224==(f&240)?(f&15)<<12|k<<6|p:(f&7)<<18|k<<12|p<<6|d[b++]&63;65536>f?e+=String.fromCharCode(f):(f-=65536,e+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else e+=String.fromCharCode(f)}b=e}}else b="";return b}
function A(b,c,d,e){if(!(0<e))return 0;var f=d;e=d+e-1;for(var k=0;k<b.length;++k){var p=b.charCodeAt(k);if(55296<=p&&57343>=p){var w=b.charCodeAt(++k);p=65536+((p&1023)<<10)|w&1023}if(127>=p){if(d>=e)break;c[d++]=p}else{if(2047>=p){if(d+1>=e)break;c[d++]=192|p>>6}else{if(65535>=p){if(d+2>=e)break;c[d++]=224|p>>12}else{if(d+3>=e)break;c[d++]=240|p>>18;c[d++]=128|p>>12&63}c[d++]=128|p>>6&63}c[d++]=128|p&63}}c[d]=0;return d-f}
function B(b){for(var c=0,d=0;d<b.length;++d){var e=b.charCodeAt(d);55296<=e&&57343>=e&&(e=65536+((e&1023)<<10)|b.charCodeAt(++d)&1023);127>=e?++c:c=2047>=e?c+2:65535>=e?c+3:c+4}return c}function wa(b,c){for(var d=0;d<b.length;++d)u[c++>>0]=b.charCodeAt(d)}var u,z,oa,v,qa,ra,xa,ya=[],za=[],Aa=[];function Ca(){var b=a.preRun.shift();ya.unshift(b)}var C=0,Da=null,Ea=null;a.preloadedImages={};a.preloadedAudios={};
function r(b){if(a.onAbort)a.onAbort(b);ma(b);ua=!0;b=new WebAssembly.RuntimeError("abort("+b+"). Build with -s ASSERTIONS=1 for more info.");ba(b);throw b;}function Fa(){return D.startsWith("data:application/octet-stream;base64,")}var D;D="olm.wasm";if(!Fa()){var Ga=D;D=a.locateFile?a.locateFile(Ga,m):m+Ga}function Ha(){var b=D;try{if(b==D&&na)return new Uint8Array(na);if(la)return la(b);throw"both async and sync fetching of the wasm failed";}catch(c){r(c)}}
function Ia(){if(!na&&(ha||ia)){if("function"===typeof fetch&&!D.startsWith("file://"))return fetch(D,{credentials:"same-origin"}).then(function(b){if(!b.ok)throw"failed to load wasm binary file at '"+D+"'";return b.arrayBuffer()}).catch(function(){return Ha()});if(ka)return new Promise(function(b,c){ka(D,function(d){b(new Uint8Array(d))},c)})}return Promise.resolve().then(function(){return Ha()})}var x,pa;
function Ja(b){for(;0<b.length;){var c=b.shift();if("function"==typeof c)c(a);else{var d=c.cc;"number"===typeof d?void 0===c.bc?xa.get(d)():xa.get(d)(c.bc):d(void 0===c.bc?null:c.bc)}}}var Ka={a:function(b,c,d){z.copyWithin(b,c,c+d)},b:function(){r("OOM")}};
(function(){function b(f){a.asm=f.exports;ta=a.asm.c;f=ta.buffer;a.HEAP8=u=new Int8Array(f);a.HEAP16=oa=new Int16Array(f);a.HEAP32=v=new Int32Array(f);a.HEAPU8=z=new Uint8Array(f);a.HEAPU16=new Uint16Array(f);a.HEAPU32=new Uint32Array(f);a.HEAPF32=qa=new Float32Array(f);a.HEAPF64=ra=new Float64Array(f);xa=a.asm.e;za.unshift(a.asm.d);C--;a.monitorRunDependencies&&a.monitorRunDependencies(C);0==C&&(null!==Da&&(clearInterval(Da),Da=null),Ea&&(f=Ea,Ea=null,f()))}function c(f){b(f.instance)}function d(f){return Ia().then(function(k){return WebAssembly.instantiate(k,
e)}).then(function(k){return k}).then(f,function(k){ma("failed to asynchronously prepare wasm: "+k);r(k)})}var e={a:Ka};C++;a.monitorRunDependencies&&a.monitorRunDependencies(C);if(a.instantiateWasm)try{return a.instantiateWasm(e,b)}catch(f){return ma("Module.instantiateWasm callback failed with error: "+f),!1}(function(){return na||"function"!==typeof WebAssembly.instantiateStreaming||Fa()||D.startsWith("file://")||"function"!==typeof fetch?d(c):fetch(D,{credentials:"same-origin"}).then(function(f){return WebAssembly.instantiateStreaming(f,
e).then(c,function(k){ma("wasm streaming compile failed: "+k);ma("falling back to ArrayBuffer instantiation");return d(c)})})})().catch(ba);return{}})();a.___wasm_call_ctors=function(){return(a.___wasm_call_ctors=a.asm.d).apply(null,arguments)};a._olm_pk_encryption_last_error=function(){return(a._olm_pk_encryption_last_error=a.asm.f).apply(null,arguments)};a.__olm_error_to_string=function(){return(a.__olm_error_to_string=a.asm.g).apply(null,arguments)};
a._olm_pk_encryption_last_error_code=function(){return(a._olm_pk_encryption_last_error_code=a.asm.h).apply(null,arguments)};a._olm_pk_encryption_size=function(){return(a._olm_pk_encryption_size=a.asm.i).apply(null,arguments)};a._olm_pk_encryption=function(){return(a._olm_pk_encryption=a.asm.j).apply(null,arguments)};a._olm_clear_pk_encryption=function(){return(a._olm_clear_pk_encryption=a.asm.k).apply(null,arguments)};
a._olm_pk_encryption_set_recipient_key=function(){return(a._olm_pk_encryption_set_recipient_key=a.asm.l).apply(null,arguments)};a._olm_pk_key_length=function(){return(a._olm_pk_key_length=a.asm.m).apply(null,arguments)};a._olm_pk_ciphertext_length=function(){return(a._olm_pk_ciphertext_length=a.asm.n).apply(null,arguments)};a._olm_pk_mac_length=function(){return(a._olm_pk_mac_length=a.asm.o).apply(null,arguments)};
a._olm_pk_encrypt_random_length=function(){return(a._olm_pk_encrypt_random_length=a.asm.p).apply(null,arguments)};a._olm_pk_encrypt=function(){return(a._olm_pk_encrypt=a.asm.q).apply(null,arguments)};a._olm_pk_decryption_last_error=function(){return(a._olm_pk_decryption_last_error=a.asm.r).apply(null,arguments)};a._olm_pk_decryption_last_error_code=function(){return(a._olm_pk_decryption_last_error_code=a.asm.s).apply(null,arguments)};
a._olm_pk_decryption_size=function(){return(a._olm_pk_decryption_size=a.asm.t).apply(null,arguments)};a._olm_pk_decryption=function(){return(a._olm_pk_decryption=a.asm.u).apply(null,arguments)};a._olm_clear_pk_decryption=function(){return(a._olm_clear_pk_decryption=a.asm.v).apply(null,arguments)};a._olm_pk_private_key_length=function(){return(a._olm_pk_private_key_length=a.asm.w).apply(null,arguments)};
a._olm_pk_generate_key_random_length=function(){return(a._olm_pk_generate_key_random_length=a.asm.x).apply(null,arguments)};a._olm_pk_key_from_private=function(){return(a._olm_pk_key_from_private=a.asm.y).apply(null,arguments)};a._olm_pk_generate_key=function(){return(a._olm_pk_generate_key=a.asm.z).apply(null,arguments)};a._olm_pickle_pk_decryption_length=function(){return(a._olm_pickle_pk_decryption_length=a.asm.A).apply(null,arguments)};
a._olm_pickle_pk_decryption=function(){return(a._olm_pickle_pk_decryption=a.asm.B).apply(null,arguments)};a._olm_unpickle_pk_decryption=function(){return(a._olm_unpickle_pk_decryption=a.asm.C).apply(null,arguments)};a._olm_pk_max_plaintext_length=function(){return(a._olm_pk_max_plaintext_length=a.asm.D).apply(null,arguments)};a._olm_pk_decrypt=function(){return(a._olm_pk_decrypt=a.asm.E).apply(null,arguments)};
a._olm_pk_get_private_key=function(){return(a._olm_pk_get_private_key=a.asm.F).apply(null,arguments)};a._olm_pk_signing_size=function(){return(a._olm_pk_signing_size=a.asm.G).apply(null,arguments)};a._olm_pk_signing=function(){return(a._olm_pk_signing=a.asm.H).apply(null,arguments)};a._olm_pk_signing_last_error=function(){return(a._olm_pk_signing_last_error=a.asm.I).apply(null,arguments)};a._olm_pk_signing_last_error_code=function(){return(a._olm_pk_signing_last_error_code=a.asm.J).apply(null,arguments)};
a._olm_clear_pk_signing=function(){return(a._olm_clear_pk_signing=a.asm.K).apply(null,arguments)};a._olm_pk_signing_seed_length=function(){return(a._olm_pk_signing_seed_length=a.asm.L).apply(null,arguments)};a._olm_pk_signing_public_key_length=function(){return(a._olm_pk_signing_public_key_length=a.asm.M).apply(null,arguments)};a._olm_pk_signing_key_from_seed=function(){return(a._olm_pk_signing_key_from_seed=a.asm.N).apply(null,arguments)};
a._olm_pk_signature_length=function(){return(a._olm_pk_signature_length=a.asm.O).apply(null,arguments)};a._olm_pk_sign=function(){return(a._olm_pk_sign=a.asm.P).apply(null,arguments)};a._olm_get_library_version=function(){return(a._olm_get_library_version=a.asm.Q).apply(null,arguments)};a._olm_error=function(){return(a._olm_error=a.asm.R).apply(null,arguments)};a._olm_account_last_error=function(){return(a._olm_account_last_error=a.asm.S).apply(null,arguments)};
a._olm_account_last_error_code=function(){return(a._olm_account_last_error_code=a.asm.T).apply(null,arguments)};a._olm_session_last_error=function(){return(a._olm_session_last_error=a.asm.U).apply(null,arguments)};a._olm_session_last_error_code=function(){return(a._olm_session_last_error_code=a.asm.V).apply(null,arguments)};a._olm_utility_last_error=function(){return(a._olm_utility_last_error=a.asm.W).apply(null,arguments)};
a._olm_utility_last_error_code=function(){return(a._olm_utility_last_error_code=a.asm.X).apply(null,arguments)};a._olm_account_size=function(){return(a._olm_account_size=a.asm.Y).apply(null,arguments)};a._olm_session_size=function(){return(a._olm_session_size=a.asm.Z).apply(null,arguments)};a._olm_utility_size=function(){return(a._olm_utility_size=a.asm._).apply(null,arguments)};a._olm_account=function(){return(a._olm_account=a.asm.$).apply(null,arguments)};
a._olm_session=function(){return(a._olm_session=a.asm.aa).apply(null,arguments)};a._olm_utility=function(){return(a._olm_utility=a.asm.ba).apply(null,arguments)};a._olm_clear_account=function(){return(a._olm_clear_account=a.asm.ca).apply(null,arguments)};a._olm_clear_session=function(){return(a._olm_clear_session=a.asm.da).apply(null,arguments)};a._olm_clear_utility=function(){return(a._olm_clear_utility=a.asm.ea).apply(null,arguments)};
a._olm_pickle_account_length=function(){return(a._olm_pickle_account_length=a.asm.fa).apply(null,arguments)};a._olm_pickle_session_length=function(){return(a._olm_pickle_session_length=a.asm.ga).apply(null,arguments)};a._olm_pickle_account=function(){return(a._olm_pickle_account=a.asm.ha).apply(null,arguments)};a._olm_pickle_session=function(){return(a._olm_pickle_session=a.asm.ia).apply(null,arguments)};a._olm_unpickle_account=function(){return(a._olm_unpickle_account=a.asm.ja).apply(null,arguments)};
a._olm_unpickle_session=function(){return(a._olm_unpickle_session=a.asm.ka).apply(null,arguments)};a._olm_create_account_random_length=function(){return(a._olm_create_account_random_length=a.asm.la).apply(null,arguments)};a._olm_create_account=function(){return(a._olm_create_account=a.asm.ma).apply(null,arguments)};a._olm_account_identity_keys_length=function(){return(a._olm_account_identity_keys_length=a.asm.na).apply(null,arguments)};
a._olm_account_identity_keys=function(){return(a._olm_account_identity_keys=a.asm.oa).apply(null,arguments)};a._olm_account_signature_length=function(){return(a._olm_account_signature_length=a.asm.pa).apply(null,arguments)};a._olm_account_sign=function(){return(a._olm_account_sign=a.asm.qa).apply(null,arguments)};a._olm_account_one_time_keys_length=function(){return(a._olm_account_one_time_keys_length=a.asm.ra).apply(null,arguments)};
a._olm_account_one_time_keys=function(){return(a._olm_account_one_time_keys=a.asm.sa).apply(null,arguments)};a._olm_account_mark_keys_as_published=function(){return(a._olm_account_mark_keys_as_published=a.asm.ta).apply(null,arguments)};a._olm_account_max_number_of_one_time_keys=function(){return(a._olm_account_max_number_of_one_time_keys=a.asm.ua).apply(null,arguments)};
a._olm_account_generate_one_time_keys_random_length=function(){return(a._olm_account_generate_one_time_keys_random_length=a.asm.va).apply(null,arguments)};a._olm_account_generate_one_time_keys=function(){return(a._olm_account_generate_one_time_keys=a.asm.wa).apply(null,arguments)};a._olm_account_generate_fallback_key_random_length=function(){return(a._olm_account_generate_fallback_key_random_length=a.asm.xa).apply(null,arguments)};
a._olm_account_generate_fallback_key=function(){return(a._olm_account_generate_fallback_key=a.asm.ya).apply(null,arguments)};a._olm_account_fallback_key_length=function(){return(a._olm_account_fallback_key_length=a.asm.za).apply(null,arguments)};a._olm_account_fallback_key=function(){return(a._olm_account_fallback_key=a.asm.Aa).apply(null,arguments)};a._olm_account_unpublished_fallback_key_length=function(){return(a._olm_account_unpublished_fallback_key_length=a.asm.Ba).apply(null,arguments)};
a._olm_account_unpublished_fallback_key=function(){return(a._olm_account_unpublished_fallback_key=a.asm.Ca).apply(null,arguments)};a._olm_account_forget_old_fallback_key=function(){return(a._olm_account_forget_old_fallback_key=a.asm.Da).apply(null,arguments)};a._olm_create_outbound_session_random_length=function(){return(a._olm_create_outbound_session_random_length=a.asm.Ea).apply(null,arguments)};
a._olm_create_outbound_session=function(){return(a._olm_create_outbound_session=a.asm.Fa).apply(null,arguments)};a._olm_create_inbound_session=function(){return(a._olm_create_inbound_session=a.asm.Ga).apply(null,arguments)};a._olm_create_inbound_session_from=function(){return(a._olm_create_inbound_session_from=a.asm.Ha).apply(null,arguments)};a._olm_session_id_length=function(){return(a._olm_session_id_length=a.asm.Ia).apply(null,arguments)};
a._olm_session_id=function(){return(a._olm_session_id=a.asm.Ja).apply(null,arguments)};a._olm_session_has_received_message=function(){return(a._olm_session_has_received_message=a.asm.Ka).apply(null,arguments)};a._olm_session_describe=function(){return(a._olm_session_describe=a.asm.La).apply(null,arguments)};a._olm_matches_inbound_session=function(){return(a._olm_matches_inbound_session=a.asm.Ma).apply(null,arguments)};
a._olm_matches_inbound_session_from=function(){return(a._olm_matches_inbound_session_from=a.asm.Na).apply(null,arguments)};a._olm_remove_one_time_keys=function(){return(a._olm_remove_one_time_keys=a.asm.Oa).apply(null,arguments)};a._olm_encrypt_message_type=function(){return(a._olm_encrypt_message_type=a.asm.Pa).apply(null,arguments)};a._olm_encrypt_random_length=function(){return(a._olm_encrypt_random_length=a.asm.Qa).apply(null,arguments)};
a._olm_encrypt_message_length=function(){return(a._olm_encrypt_message_length=a.asm.Ra).apply(null,arguments)};a._olm_encrypt=function(){return(a._olm_encrypt=a.asm.Sa).apply(null,arguments)};a._olm_decrypt_max_plaintext_length=function(){return(a._olm_decrypt_max_plaintext_length=a.asm.Ta).apply(null,arguments)};a._olm_decrypt=function(){return(a._olm_decrypt=a.asm.Ua).apply(null,arguments)};a._olm_sha256_length=function(){return(a._olm_sha256_length=a.asm.Va).apply(null,arguments)};
a._olm_sha256=function(){return(a._olm_sha256=a.asm.Wa).apply(null,arguments)};a._olm_ed25519_verify=function(){return(a._olm_ed25519_verify=a.asm.Xa).apply(null,arguments)};a._olm_inbound_group_session_size=function(){return(a._olm_inbound_group_session_size=a.asm.Ya).apply(null,arguments)};a._olm_inbound_group_session=function(){return(a._olm_inbound_group_session=a.asm.Za).apply(null,arguments)};
a._olm_clear_inbound_group_session=function(){return(a._olm_clear_inbound_group_session=a.asm._a).apply(null,arguments)};a._olm_inbound_group_session_last_error=function(){return(a._olm_inbound_group_session_last_error=a.asm.$a).apply(null,arguments)};a._olm_inbound_group_session_last_error_code=function(){return(a._olm_inbound_group_session_last_error_code=a.asm.ab).apply(null,arguments)};a._olm_init_inbound_group_session=function(){return(a._olm_init_inbound_group_session=a.asm.bb).apply(null,arguments)};
a._olm_import_inbound_group_session=function(){return(a._olm_import_inbound_group_session=a.asm.cb).apply(null,arguments)};a._olm_pickle_inbound_group_session_length=function(){return(a._olm_pickle_inbound_group_session_length=a.asm.db).apply(null,arguments)};a._olm_pickle_inbound_group_session=function(){return(a._olm_pickle_inbound_group_session=a.asm.eb).apply(null,arguments)};a._olm_unpickle_inbound_group_session=function(){return(a._olm_unpickle_inbound_group_session=a.asm.fb).apply(null,arguments)};
a._olm_group_decrypt_max_plaintext_length=function(){return(a._olm_group_decrypt_max_plaintext_length=a.asm.gb).apply(null,arguments)};a._olm_group_decrypt=function(){return(a._olm_group_decrypt=a.asm.hb).apply(null,arguments)};a._olm_inbound_group_session_id_length=function(){return(a._olm_inbound_group_session_id_length=a.asm.ib).apply(null,arguments)};a._olm_inbound_group_session_id=function(){return(a._olm_inbound_group_session_id=a.asm.jb).apply(null,arguments)};
a._olm_inbound_group_session_first_known_index=function(){return(a._olm_inbound_group_session_first_known_index=a.asm.kb).apply(null,arguments)};a._olm_inbound_group_session_is_verified=function(){return(a._olm_inbound_group_session_is_verified=a.asm.lb).apply(null,arguments)};a._olm_export_inbound_group_session_length=function(){return(a._olm_export_inbound_group_session_length=a.asm.mb).apply(null,arguments)};
a._olm_export_inbound_group_session=function(){return(a._olm_export_inbound_group_session=a.asm.nb).apply(null,arguments)};a._olm_sas_last_error=function(){return(a._olm_sas_last_error=a.asm.ob).apply(null,arguments)};a._olm_sas_last_error_code=function(){return(a._olm_sas_last_error_code=a.asm.pb).apply(null,arguments)};a._olm_sas_size=function(){return(a._olm_sas_size=a.asm.qb).apply(null,arguments)};a._olm_sas=function(){return(a._olm_sas=a.asm.rb).apply(null,arguments)};
a._olm_clear_sas=function(){return(a._olm_clear_sas=a.asm.sb).apply(null,arguments)};a._olm_create_sas_random_length=function(){return(a._olm_create_sas_random_length=a.asm.tb).apply(null,arguments)};a._olm_create_sas=function(){return(a._olm_create_sas=a.asm.ub).apply(null,arguments)};a._olm_sas_pubkey_length=function(){return(a._olm_sas_pubkey_length=a.asm.vb).apply(null,arguments)};a._olm_sas_get_pubkey=function(){return(a._olm_sas_get_pubkey=a.asm.wb).apply(null,arguments)};
a._olm_sas_set_their_key=function(){return(a._olm_sas_set_their_key=a.asm.xb).apply(null,arguments)};a._olm_sas_is_their_key_set=function(){return(a._olm_sas_is_their_key_set=a.asm.yb).apply(null,arguments)};a._olm_sas_generate_bytes=function(){return(a._olm_sas_generate_bytes=a.asm.zb).apply(null,arguments)};a._olm_sas_mac_length=function(){return(a._olm_sas_mac_length=a.asm.Ab).apply(null,arguments)};
a._olm_sas_calculate_mac_fixed_base64=function(){return(a._olm_sas_calculate_mac_fixed_base64=a.asm.Bb).apply(null,arguments)};a._olm_sas_calculate_mac=function(){return(a._olm_sas_calculate_mac=a.asm.Cb).apply(null,arguments)};a._olm_sas_calculate_mac_long_kdf=function(){return(a._olm_sas_calculate_mac_long_kdf=a.asm.Db).apply(null,arguments)};a._olm_outbound_group_session_size=function(){return(a._olm_outbound_group_session_size=a.asm.Eb).apply(null,arguments)};
a._olm_outbound_group_session=function(){return(a._olm_outbound_group_session=a.asm.Fb).apply(null,arguments)};a._olm_clear_outbound_group_session=function(){return(a._olm_clear_outbound_group_session=a.asm.Gb).apply(null,arguments)};a._olm_outbound_group_session_last_error=function(){return(a._olm_outbound_group_session_last_error=a.asm.Hb).apply(null,arguments)};a._olm_outbound_group_session_last_error_code=function(){return(a._olm_outbound_group_session_last_error_code=a.asm.Ib).apply(null,arguments)};
a._olm_pickle_outbound_group_session_length=function(){return(a._olm_pickle_outbound_group_session_length=a.asm.Jb).apply(null,arguments)};a._olm_pickle_outbound_group_session=function(){return(a._olm_pickle_outbound_group_session=a.asm.Kb).apply(null,arguments)};a._olm_unpickle_outbound_group_session=function(){return(a._olm_unpickle_outbound_group_session=a.asm.Lb).apply(null,arguments)};
a._olm_init_outbound_group_session_random_length=function(){return(a._olm_init_outbound_group_session_random_length=a.asm.Mb).apply(null,arguments)};a._olm_init_outbound_group_session=function(){return(a._olm_init_outbound_group_session=a.asm.Nb).apply(null,arguments)};a._olm_group_encrypt_message_length=function(){return(a._olm_group_encrypt_message_length=a.asm.Ob).apply(null,arguments)};a._olm_group_encrypt=function(){return(a._olm_group_encrypt=a.asm.Pb).apply(null,arguments)};
a._olm_outbound_group_session_id_length=function(){return(a._olm_outbound_group_session_id_length=a.asm.Qb).apply(null,arguments)};a._olm_outbound_group_session_id=function(){return(a._olm_outbound_group_session_id=a.asm.Rb).apply(null,arguments)};a._olm_outbound_group_session_message_index=function(){return(a._olm_outbound_group_session_message_index=a.asm.Sb).apply(null,arguments)};
a._olm_outbound_group_session_key_length=function(){return(a._olm_outbound_group_session_key_length=a.asm.Tb).apply(null,arguments)};a._olm_outbound_group_session_key=function(){return(a._olm_outbound_group_session_key=a.asm.Ub).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Vb).apply(null,arguments)};a._free=function(){return(a._free=a.asm.Wb).apply(null,arguments)};
var La=a.stackSave=function(){return(La=a.stackSave=a.asm.Xb).apply(null,arguments)},Ma=a.stackRestore=function(){return(Ma=a.stackRestore=a.asm.Yb).apply(null,arguments)},Na=a.stackAlloc=function(){return(Na=a.stackAlloc=a.asm.Zb).apply(null,arguments)};a.ALLOC_STACK=1;var Oa;Ea=function Pa(){Oa||Qa();Oa||(Ea=Pa)};
function Qa(){function b(){if(!Oa&&(Oa=!0,a.calledRun=!0,!ua)){Ja(za);aa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;){var c=a.postRun.shift();Aa.unshift(c)}Ja(Aa)}}if(!(0<C)){if(a.preRun)for("function"==typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)Ca();Ja(ya);0<C||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);b()},1)):b())}}a.run=Qa;
if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();Qa();function E(){var b=a._olm_outbound_group_session_size();this.ac=F(b);this.$b=a._olm_outbound_group_session(this.ac)}function G(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_outbound_group_session_last_error(arguments[0])),Error("OLM."+c);return c}}E.prototype.free=function(){a._olm_clear_outbound_group_session(this.$b);I(this.$b)};
E.prototype.pickle=J(function(b){b=K(b);var c=G(a._olm_pickle_outbound_group_session_length)(this.$b),d=L(b),e=L(c+1);try{G(a._olm_pickle_outbound_group_session)(this.$b,d,b.length,e,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(e,c)});E.prototype.unpickle=J(function(b,c){b=K(b);var d=L(b);c=K(c);var e=L(c);try{G(a._olm_unpickle_outbound_group_session)(this.$b,d,b.length,e,c.length)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}});
E.prototype.create=J(function(){var b=G(a._olm_init_outbound_group_session_random_length)(this.$b),c=N(b,g);try{G(a._olm_init_outbound_group_session)(this.$b,c,b)}finally{M(c,b)}});E.prototype.encrypt=function(b){try{var c=B(b);var d=G(a._olm_group_encrypt_message_length)(this.$b,c);var e=F(c+1);A(b,z,e,c+1);var f=F(d+1);G(a._olm_group_encrypt)(this.$b,e,c,f,d);t(f+d);return y(f,d)}finally{void 0!==e&&(M(e,c+1),I(e)),void 0!==f&&I(f)}};
E.prototype.session_id=J(function(){var b=G(a._olm_outbound_group_session_id_length)(this.$b),c=L(b+1);G(a._olm_outbound_group_session_id)(this.$b,c,b);return y(c,b)});E.prototype.session_key=J(function(){var b=G(a._olm_outbound_group_session_key_length)(this.$b),c=L(b+1);G(a._olm_outbound_group_session_key)(this.$b,c,b);var d=y(c,b);M(c,b);return d});E.prototype.message_index=function(){return G(a._olm_outbound_group_session_message_index)(this.$b)};olm_exports.OutboundGroupSession=E;
function O(){var b=a._olm_inbound_group_session_size();this.ac=F(b);this.$b=a._olm_inbound_group_session(this.ac)}function P(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_inbound_group_session_last_error(arguments[0])),Error("OLM."+c);return c}}O.prototype.free=function(){a._olm_clear_inbound_group_session(this.$b);I(this.$b)};
O.prototype.pickle=J(function(b){b=K(b);var c=P(a._olm_pickle_inbound_group_session_length)(this.$b),d=L(b),e=L(c+1);try{P(a._olm_pickle_inbound_group_session)(this.$b,d,b.length,e,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(e,c)});O.prototype.unpickle=J(function(b,c){b=K(b);var d=L(b);c=K(c);var e=L(c);try{P(a._olm_unpickle_inbound_group_session)(this.$b,d,b.length,e,c.length)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}});
O.prototype.create=J(function(b){b=K(b);var c=L(b);try{P(a._olm_init_inbound_group_session)(this.$b,c,b.length)}finally{for(M(c,b.length),c=0;c<b.length;c++)b[c]=0}});O.prototype.import_session=J(function(b){b=K(b);var c=L(b);try{P(a._olm_import_inbound_group_session)(this.$b,c,b.length)}finally{for(M(c,b.length),c=0;c<b.length;c++)b[c]=0}});
O.prototype.decrypt=J(function(b){try{var c=F(b.length);wa(b,c);var d=P(a._olm_group_decrypt_max_plaintext_length)(this.$b,c,b.length);wa(b,c);var e=F(d+1);var f=L(4);var k=P(a._olm_group_decrypt)(this.$b,c,b.length,e,d,f);t(e+k);return{plaintext:y(e,k),message_index:sa(f,"i32")}}finally{void 0!==c&&I(c),void 0!==e&&(M(e,k),I(e))}});O.prototype.session_id=J(function(){var b=P(a._olm_inbound_group_session_id_length)(this.$b),c=L(b+1);P(a._olm_inbound_group_session_id)(this.$b,c,b);return y(c,b)});
O.prototype.first_known_index=J(function(){return P(a._olm_inbound_group_session_first_known_index)(this.$b)});O.prototype.export_session=J(function(b){var c=P(a._olm_export_inbound_group_session_length)(this.$b),d=L(c+1);G(a._olm_export_inbound_group_session)(this.$b,d,c,b);b=y(d,c);M(d,c);return b});olm_exports.InboundGroupSession=O;function Ra(){var b=a._olm_pk_encryption_size();this.ac=F(b);this.$b=a._olm_pk_encryption(this.ac)}
function Q(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_pk_encryption_last_error(arguments[0])),Error("OLM."+c);return c}}Ra.prototype.free=function(){a._olm_clear_pk_encryption(this.$b);I(this.$b)};Ra.prototype.set_recipient_key=J(function(b){b=K(b);var c=L(b);Q(a._olm_pk_encryption_set_recipient_key)(this.$b,c,b.length)});
Ra.prototype.encrypt=J(function(b){try{var c=B(b);var d=F(c+1);A(b,z,d,c+1);var e=Q(a._olm_pk_encrypt_random_length)();var f=N(e,g);var k=Q(a._olm_pk_ciphertext_length)(this.$b,c);var p=F(k+1);var w=Q(a._olm_pk_mac_length)(this.$b),fa=L(w+1);t(fa+w);var S=Q(a._olm_pk_key_length)(),H=L(S+1);t(H+S);Q(a._olm_pk_encrypt)(this.$b,d,c,p,k,fa,w,H,S,f,e);t(p+k);return{ciphertext:y(p,k),mac:y(fa,w),ephemeral:y(H,S)}}finally{void 0!==f&&M(f,e),void 0!==d&&(M(d,c+1),I(d)),void 0!==p&&I(p)}});
function R(){var b=a._olm_pk_decryption_size();this.ac=F(b);this.$b=a._olm_pk_decryption(this.ac)}function T(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_pk_decryption_last_error(arguments[0])),Error("OLM."+c);return c}}R.prototype.free=function(){a._olm_clear_pk_decryption(this.$b);I(this.$b)};
R.prototype.init_with_private_key=J(function(b){var c=L(b.length);a.HEAPU8.set(b,c);var d=T(a._olm_pk_key_length)(),e=L(d+1);try{T(a._olm_pk_key_from_private)(this.$b,e,d,c,b.length)}finally{M(c,b.length)}return y(e,d)});R.prototype.generate_key=J(function(){var b=T(a._olm_pk_private_key_length)(),c=N(b,g),d=T(a._olm_pk_key_length)(),e=L(d+1);try{T(a._olm_pk_key_from_private)(this.$b,e,d,c,b)}finally{M(c,b)}return y(e,d)});
R.prototype.get_private_key=J(function(){var b=Q(a._olm_pk_private_key_length)(),c=L(b);T(a._olm_pk_get_private_key)(this.$b,c,b);var d=new Uint8Array(new Uint8Array(a.HEAPU8.buffer,c,b));M(c,b);return d});R.prototype.pickle=J(function(b){b=K(b);var c=T(a._olm_pickle_pk_decryption_length)(this.$b),d=L(b),e=L(c+1);try{T(a._olm_pickle_pk_decryption)(this.$b,d,b.length,e,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(e,c)});
R.prototype.unpickle=J(function(b,c){b=K(b);var d=L(b),e=K(c),f=L(e);c=T(a._olm_pk_key_length)();var k=L(c+1);try{T(a._olm_unpickle_pk_decryption)(this.$b,d,b.length,f,e.length,k,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(k,c)});
R.prototype.decrypt=J(function(b,c,d){try{var e=B(d);var f=F(e+1);A(d,z,f,e+1);var k=K(b),p=L(k),w=K(c),fa=L(w);var S=T(a._olm_pk_max_plaintext_length)(this.$b,e);var H=F(S+1);var Ba=T(a._olm_pk_decrypt)(this.$b,p,k.length,fa,w.length,f,e,H,S);t(H+Ba);return y(H,Ba)}finally{void 0!==H&&(M(H,Ba+1),I(H)),void 0!==f&&I(f)}});function Sa(){var b=a._olm_pk_signing_size();this.ac=F(b);this.$b=a._olm_pk_signing(this.ac)}
function Ta(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_pk_signing_last_error(arguments[0])),Error("OLM."+c);return c}}Sa.prototype.free=function(){a._olm_clear_pk_signing(this.$b);I(this.$b)};Sa.prototype.init_with_seed=J(function(b){var c=L(b.length);a.HEAPU8.set(b,c);var d=Ta(a._olm_pk_signing_public_key_length)(),e=L(d+1);try{Ta(a._olm_pk_signing_key_from_seed)(this.$b,e,d,c,b.length)}finally{M(c,b.length)}return y(e,d)});
Sa.prototype.generate_seed=J(function(){var b=Ta(a._olm_pk_signing_seed_length)(),c=N(b,g),d=new Uint8Array(new Uint8Array(a.HEAPU8.buffer,c,b));M(c,b);return d});Sa.prototype.sign=J(function(b){try{var c=B(b);var d=F(c+1);A(b,z,d,c+1);var e=Ta(a._olm_pk_signature_length)(),f=L(e+1);Ta(a._olm_pk_sign)(this.$b,d,c,f,e);return y(f,e)}finally{void 0!==d&&(M(d,c+1),I(d))}});
function U(){var b=a._olm_sas_size(),c=a._olm_create_sas_random_length(),d=N(c,g);this.ac=F(b);this.$b=a._olm_sas(this.ac);a._olm_create_sas(this.$b,d,c);M(d,c)}function V(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_sas_last_error(arguments[0])),Error("OLM."+c);return c}}U.prototype.free=function(){a._olm_clear_sas(this.$b);I(this.$b)};
U.prototype.get_pubkey=J(function(){var b=V(a._olm_sas_pubkey_length)(this.$b),c=L(b+1);V(a._olm_sas_get_pubkey)(this.$b,c,b);return y(c,b)});U.prototype.set_their_key=J(function(b){b=K(b);var c=L(b);V(a._olm_sas_set_their_key)(this.$b,c,b.length)});U.prototype.is_their_key_set=J(function(){return V(a._olm_sas_is_their_key_set)(this.$b)?!0:!1});
U.prototype.generate_bytes=J(function(b,c){b=K(b);var d=L(b),e=L(c);V(a._olm_sas_generate_bytes)(this.$b,d,b.length,e,c);return new Uint8Array(new Uint8Array(a.HEAPU8.buffer,e,c))});U.prototype.calculate_mac=J(function(b,c){b=K(b);var d=L(b);c=K(c);var e=L(c),f=V(a._olm_sas_mac_length)(this.$b),k=L(f+1);V(a._olm_sas_calculate_mac)(this.$b,d,b.length,e,c.length,k,f);return y(k,f)});
U.prototype.calculate_mac_long_kdf=J(function(b,c){b=K(b);var d=L(b);c=K(c);var e=L(c),f=V(a._olm_sas_mac_length)(this.$b),k=L(f+1);V(a._olm_sas_calculate_mac_long_kdf)(this.$b,d,b.length,e,c.length,k,f);return y(k,f)});var F=a._malloc,I=a._free,h;function N(b,c){var d=Na(b);c(new Uint8Array(a.HEAPU8.buffer,d,b));return d}function L(b){return"number"==typeof b?N(b,function(c){c.fill(0)}):N(b.length,function(c){c.set(b)})}
function K(b){if(b instanceof Uint8Array)var c=b;else c=Array(B(b)+1),b=A(b,c,0,c.length),c.length=b;return c}function J(b){return function(){var c=La();try{return b.apply(this,arguments)}finally{Ma(c)}}}function M(b,c){for(;0<c--;)a.HEAP8[b++]=0}function W(){var b=a._olm_account_size();this.ac=F(b);this.$b=a._olm_account(this.ac)}function X(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_account_last_error(arguments[0])),Error("OLM."+c);return c}}
W.prototype.free=function(){a._olm_clear_account(this.$b);I(this.$b)};W.prototype.create=J(function(){var b=X(a._olm_create_account_random_length)(this.$b),c=N(b,g);try{X(a._olm_create_account)(this.$b,c,b)}finally{M(c,b)}});W.prototype.identity_keys=J(function(){var b=X(a._olm_account_identity_keys_length)(this.$b),c=L(b+1);X(a._olm_account_identity_keys)(this.$b,c,b);return y(c,b)});
W.prototype.sign=J(function(b){var c=X(a._olm_account_signature_length)(this.$b);b=K(b);var d=L(b),e=L(c+1);try{X(a._olm_account_sign)(this.$b,d,b.length,e,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(e,c)});W.prototype.one_time_keys=J(function(){var b=X(a._olm_account_one_time_keys_length)(this.$b),c=L(b+1);X(a._olm_account_one_time_keys)(this.$b,c,b);return y(c,b)});W.prototype.mark_keys_as_published=J(function(){X(a._olm_account_mark_keys_as_published)(this.$b)});
W.prototype.max_number_of_one_time_keys=J(function(){return X(a._olm_account_max_number_of_one_time_keys)(this.$b)});W.prototype.generate_one_time_keys=J(function(b){var c=X(a._olm_account_generate_one_time_keys_random_length)(this.$b,b),d=N(c,g);try{X(a._olm_account_generate_one_time_keys)(this.$b,b,d,c)}finally{M(d,c)}});W.prototype.remove_one_time_keys=J(function(b){X(a._olm_remove_one_time_keys)(this.$b,b.$b)});
W.prototype.generate_fallback_key=J(function(){var b=X(a._olm_account_generate_fallback_key_random_length)(this.$b),c=N(b,g);try{X(a._olm_account_generate_fallback_key)(this.$b,c,b)}finally{M(c,b)}});W.prototype.fallback_key=J(function(){var b=X(a._olm_account_fallback_key_length)(this.$b),c=L(b+1);X(a._olm_account_fallback_key)(this.$b,c,b);return y(c,b)});
W.prototype.unpublished_fallback_key=J(function(){var b=X(a._olm_account_unpublished_fallback_key_length)(this.$b),c=L(b+1);X(a._olm_account_unpublished_fallback_key)(this.$b,c,b);return y(c,b)});W.prototype.forget_old_fallback_key=J(function(){X(a._olm_account_forget_old_fallback_key)(this.$b)});
W.prototype.pickle=J(function(b){b=K(b);var c=X(a._olm_pickle_account_length)(this.$b),d=L(b),e=L(c+1);try{X(a._olm_pickle_account)(this.$b,d,b.length,e,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(e,c)});W.prototype.unpickle=J(function(b,c){b=K(b);var d=L(b);c=K(c);var e=L(c);try{X(a._olm_unpickle_account)(this.$b,d,b.length,e,c.length)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}});function Y(){var b=a._olm_session_size();this.ac=F(b);this.$b=a._olm_session(this.ac)}
function Z(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_session_last_error(arguments[0])),Error("OLM."+c);return c}}Y.prototype.free=function(){a._olm_clear_session(this.$b);I(this.$b)};Y.prototype.pickle=J(function(b){b=K(b);var c=Z(a._olm_pickle_session_length)(this.$b),d=L(b),e=L(c+1);try{Z(a._olm_pickle_session)(this.$b,d,b.length,e,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(e,c)});
Y.prototype.unpickle=J(function(b,c){b=K(b);var d=L(b);c=K(c);var e=L(c);try{Z(a._olm_unpickle_session)(this.$b,d,b.length,e,c.length)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}});Y.prototype.create_outbound=J(function(b,c,d){var e=Z(a._olm_create_outbound_session_random_length)(this.$b),f=N(e,g);c=K(c);d=K(d);var k=L(c),p=L(d);try{Z(a._olm_create_outbound_session)(this.$b,b.$b,k,c.length,p,d.length,f,e)}finally{M(f,e)}});
Y.prototype.create_inbound=J(function(b,c){c=K(c);var d=L(c);try{Z(a._olm_create_inbound_session)(this.$b,b.$b,d,c.length)}finally{for(M(d,c.length),b=0;b<c.length;b++)c[b]=0}});Y.prototype.create_inbound_from=J(function(b,c,d){c=K(c);var e=L(c);d=K(d);var f=L(d);try{Z(a._olm_create_inbound_session_from)(this.$b,b.$b,e,c.length,f,d.length)}finally{for(M(f,d.length),b=0;b<d.length;b++)d[b]=0}});
Y.prototype.session_id=J(function(){var b=Z(a._olm_session_id_length)(this.$b),c=L(b+1);Z(a._olm_session_id)(this.$b,c,b);return y(c,b)});Y.prototype.has_received_message=function(){return Z(a._olm_session_has_received_message)(this.$b)?!0:!1};Y.prototype.matches_inbound=J(function(b){b=K(b);var c=L(b);return Z(a._olm_matches_inbound_session)(this.$b,c,b.length)?!0:!1});
Y.prototype.matches_inbound_from=J(function(b,c){b=K(b);var d=L(b);c=K(c);var e=L(c);return Z(a._olm_matches_inbound_session_from)(this.$b,d,b.length,e,c.length)?!0:!1});
Y.prototype.encrypt=J(function(b){try{var c=Z(a._olm_encrypt_random_length)(this.$b);var d=Z(a._olm_encrypt_message_type)(this.$b);var e=B(b);var f=Z(a._olm_encrypt_message_length)(this.$b,e);var k=N(c,g);var p=F(e+1);A(b,z,p,e+1);var w=F(f+1);Z(a._olm_encrypt)(this.$b,p,e,k,c,w,f);t(w+f);return{type:d,body:y(w,f)}}finally{void 0!==k&&M(k,c),void 0!==p&&(M(p,e+1),I(p)),void 0!==w&&I(w)}});
Y.prototype.decrypt=J(function(b,c){try{var d=F(c.length);wa(c,d);var e=Z(a._olm_decrypt_max_plaintext_length)(this.$b,b,d,c.length);wa(c,d);var f=F(e+1);var k=Z(a._olm_decrypt)(this.$b,b,d,c.length,f,e);t(f+k);return y(f,k)}finally{void 0!==d&&I(d),void 0!==f&&(M(f,e),I(f))}});Y.prototype.describe=J(function(){try{var b=F(256);Z(a._olm_session_describe)(this.$b,b,256);return y(b)}finally{void 0!==b&&I(b)}});function Ua(){var b=a._olm_utility_size();this.ac=F(b);this.$b=a._olm_utility(this.ac)}
function Va(b){return function(){var c=b.apply(this,arguments);if(c===h)throw c=y(a._olm_utility_last_error(arguments[0])),Error("OLM."+c);return c}}Ua.prototype.free=function(){a._olm_clear_utility(this.$b);I(this.$b)};Ua.prototype.sha256=J(function(b){var c=Va(a._olm_sha256_length)(this.$b);b=K(b);var d=L(b),e=L(c+1);try{Va(a._olm_sha256)(this.$b,d,b.length,e,c)}finally{for(M(d,b.length),d=0;d<b.length;d++)b[d]=0}return y(e,c)});
Ua.prototype.ed25519_verify=J(function(b,c,d){b=K(b);var e=L(b);c=K(c);var f=L(c);d=K(d);var k=L(d);try{Va(a._olm_ed25519_verify)(this.$b,e,b.length,f,c.length,k,d.length)}finally{for(M(f,c.length),b=0;b<c.length;b++)c[b]=0}});olm_exports.Account=W;olm_exports.Session=Y;olm_exports.Utility=Ua;olm_exports.PkEncryption=Ra;olm_exports.PkDecryption=R;olm_exports.PkSigning=Sa;olm_exports.SAS=U;
olm_exports.get_library_version=J(function(){var b=L(3);a._olm_get_library_version(b,b+1,b+2);return[sa(b,"i8"),sa(b+1,"i8"),sa(b+2,"i8")]});
return Module.ready
}
);
})();
if (typeof exports === 'object' && typeof module === 'object')
module.exports = Module;
else if (typeof define === 'function' && define['amd'])
define([], function() { return Module; });
else if (typeof exports === 'object')
exports["Module"] = Module;
var olmInitPromise;
olm_exports['init'] = function(opts) {
if (olmInitPromise) return olmInitPromise;
if (opts) OLM_OPTIONS = opts;
olmInitPromise = new Promise(function(resolve, reject) {
onInitSuccess = function() {
resolve();
};
onInitFail = function(err) {
reject(err);
};
Module();
});
return olmInitPromise;
};
return olm_exports;
})();
if (typeof(window) !== 'undefined') {
// We've been imported directly into a browser. Define the global 'Olm' object.
// (we do this even if module.exports was defined, because it's useful to have
// Olm in the global scope for browserified and webpacked apps.)
window["Olm"] = Olm;
}
if (typeof module === 'object') {
// Emscripten sets the module exports to be its module
// with wrapped c functions. Clobber it with our higher
// level wrapper class.
module.exports = Olm;
}
// @license-end

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.099 9.51084L10.4407 17.1692C8.48696 19.1229 5.31938 19.1229 3.36567 17.1692C1.41196 15.2155 1.41196 12.0479 3.36567 10.0942L11.024 2.43584C12.3265 1.13337 14.4382 1.13337 15.7407 2.43584C17.0431 3.73831 17.0431 5.85003 15.7407 7.1525L8.074 14.8108C7.42277 15.4621 6.36691 15.4621 5.71567 14.8108C5.06444 14.1596 5.06444 13.1037 5.71567 12.4525L12.7907 5.38584" stroke="#909090" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 543 B

View File

@ -1 +0,0 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m18.099 9.51-7.658 7.66a5.003 5.003 0 0 1-7.075-7.076l7.658-7.658a3.335 3.335 0 0 1 4.717 4.716l-7.667 7.659a1.667 1.667 0 1 1-2.358-2.358l7.075-7.067" stroke="#ff00ff" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 307 B

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.74986 3.55554C8.74986 3.14133 8.41408 2.80554 7.99986 2.80554C7.58565 2.80554 7.24986 3.14133 7.24986 3.55554V7.24999L3.55542 7.24999C3.14121 7.24999 2.80542 7.58577 2.80542 7.99999C2.80542 8.4142 3.14121 8.74999 3.55542 8.74999L7.24987 8.74999V12.4444C7.24987 12.8586 7.58565 13.1944 7.99987 13.1944C8.41408 13.1944 8.74987 12.8586 8.74987 12.4444V8.74999L12.4443 8.74999C12.8585 8.74999 13.1943 8.4142 13.1943 7.99999C13.1943 7.58577 12.8585 7.24999 12.4443 7.24999L8.74986 7.24999V3.55554Z" fill="#909090"/>
</svg>

Before

Width:  |  Height:  |  Size: 670 B

View File

@ -1 +0,0 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 3.556a.75.75 0 1 0-1.5 0V7.25H3.555a.75.75 0 1 0 0 1.5H7.25v3.694a.75.75 0 0 0 1.5 0V8.75h3.694a.75.75 0 0 0 0-1.5H8.75V3.556Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 280 B

View File

@ -1,7 +0,0 @@
<svg width="25" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1502 21.1214C16.3946 22.3074 14.2782 23 12 23C9.52367 23 7.23845 22.1817 5.4 20.8008C2.72821 18.794 1 15.5988 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 15.797 21.0762 19.1446 18.1502 21.1214ZM12 12.55C13.8225 12.55 15.3 10.9494 15.3 8.975C15.3 7.00058 13.8225 5.4 12 5.4C10.1775 5.4 8.7 7.00058 8.7 8.975C8.7 10.9494 10.1775 12.55 12 12.55ZM12 20.8C14.3782 20.8 16.536 19.8566 18.1197 18.3237C17.1403 15.9056 14.7693 14.2 12 14.2C9.23066 14.2 6.85969 15.9056 5.88028 18.3237C7.46399 19.8566 9.62183 20.8 12 20.8Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1502 21.1214C16.3946 22.3074 14.2782 23 12 23C9.52367 23 7.23845 22.1817 5.4 20.8008C2.72821 18.794 1 15.5988 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 15.797 21.0762 19.1446 18.1502 21.1214ZM12 12.55C13.8225 12.55 15.3 10.9494 15.3 8.975C15.3 7.00058 13.8225 5.4 12 5.4C10.1775 5.4 8.7 7.00058 8.7 8.975C8.7 10.9494 10.1775 12.55 12 12.55ZM12 20.8C14.3782 20.8 16.536 19.8566 18.1197 18.3237C17.1403 15.9056 14.7693 14.2 12 14.2C9.23066 14.2 6.85969 15.9056 5.88028 18.3237C7.46399 19.8566 9.62183 20.8 12 20.8Z" fill="#909090"/>
<path d="M18.1502 21.1214L18.9339 22.2814L18.1502 21.1214ZM5.4 20.8008L4.55919 21.9202H4.55919L5.4 20.8008ZM18.1197 18.3237L19.0934 19.3296L19.7717 18.6731L19.4173 17.7981L18.1197 18.3237ZM5.88028 18.3237L4.58268 17.7981L4.22829 18.6731L4.90659 19.3296L5.88028 18.3237ZM12 24.4C14.5662 24.4 16.9541 23.619 18.9339 22.2814L17.3665 19.9613C15.835 20.9959 13.9902 21.6 12 21.6V24.4ZM4.55919 21.9202C6.63176 23.477 9.21011 24.4 12 24.4V21.6C9.83723 21.6 7.84514 20.8865 6.24081 19.6814L4.55919 21.9202ZM-0.399998 12C-0.399998 16.0577 1.55052 19.6603 4.55919 21.9202L6.24081 19.6814C3.90591 17.9276 2.4 15.1399 2.4 12H-0.399998ZM12 -0.399998C5.15167 -0.399998 -0.399998 5.15167 -0.399998 12H2.4C2.4 6.69807 6.69807 2.4 12 2.4V-0.399998ZM24.4 12C24.4 5.15167 18.8483 -0.399998 12 -0.399998V2.4C17.3019 2.4 21.6 6.69807 21.6 12H24.4ZM18.9339 22.2814C22.2288 20.0554 24.4 16.2815 24.4 12H21.6C21.6 15.3124 19.9236 18.2337 17.3665 19.9613L18.9339 22.2814ZM13.9 8.975C13.9 10.2838 12.9459 11.15 12 11.15V13.95C14.6991 13.95 16.7 11.615 16.7 8.975H13.9ZM12 6.8C12.9459 6.8 13.9 7.66616 13.9 8.975H16.7C16.7 6.335 14.6991 4 12 4V6.8ZM10.1 8.975C10.1 7.66616 11.0541 6.8 12 6.8V4C9.30086 4 7.3 6.335 7.3 8.975H10.1ZM12 11.15C11.0541 11.15 10.1 10.2838 10.1 8.975H7.3C7.3 11.615 9.30086 13.95 12 13.95V11.15ZM17.146 17.3178C15.8129 18.6081 14.0004 19.4 12 19.4V22.2C14.756 22.2 17.2591 21.1051 19.0934 19.3296L17.146 17.3178ZM12 15.6C14.1797 15.6 16.0494 16.9415 16.8221 18.8493L19.4173 17.7981C18.2312 14.8697 15.359 12.8 12 12.8V15.6ZM7.17788 18.8493C7.95058 16.9415 9.8203 15.6 12 15.6V12.8C8.64102 12.8 5.7688 14.8697 4.58268 17.7981L7.17788 18.8493ZM12 19.4C9.99963 19.4 8.18709 18.6081 6.85397 17.3178L4.90659 19.3296C6.74088 21.1051 9.24402 22.2 12 22.2V19.4Z" fill="#909090" mask="url(#path-1-inside-1)"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1 +0,0 @@
<svg width="25" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" fill="white"><path fill-rule="evenodd" clip-rule="evenodd" d="M18.15 21.121A10.95 10.95 0 0 1 12 23c-2.476 0-4.762-.818-6.6-2.2A10.983 10.983 0 0 1 1 12C1 5.925 5.925 1 12 1s11 4.925 11 11a10.99 10.99 0 0 1-4.85 9.121ZM12 12.55c1.822 0 3.3-1.6 3.3-3.575C15.3 7.001 13.822 5.4 12 5.4c-1.822 0-3.3 1.6-3.3 3.575 0 1.974 1.478 3.575 3.3 3.575Zm0 8.25a8.77 8.77 0 0 0 6.12-2.476 6.602 6.602 0 0 0-12.24 0A8.771 8.771 0 0 0 12 20.8Z"/></mask><path fill-rule="evenodd" clip-rule="evenodd" d="M18.15 21.121A10.95 10.95 0 0 1 12 23c-2.476 0-4.762-.818-6.6-2.2A10.983 10.983 0 0 1 1 12C1 5.925 5.925 1 12 1s11 4.925 11 11a10.99 10.99 0 0 1-4.85 9.121ZM12 12.55c1.822 0 3.3-1.6 3.3-3.575C15.3 7.001 13.822 5.4 12 5.4c-1.822 0-3.3 1.6-3.3 3.575 0 1.974 1.478 3.575 3.3 3.575Zm0 8.25a8.77 8.77 0 0 0 6.12-2.476 6.602 6.602 0 0 0-12.24 0A8.771 8.771 0 0 0 12 20.8Z" fill="#ff00ff"/><path d="m18.15 21.121.784 1.16-.784-1.16Zm-12.75-.32-.84 1.12.84-1.12Zm12.72-2.477.973 1.006.679-.657-.355-.875-1.297.526Zm-12.24 0-1.297-.526-.355.875.679.657.973-1.006ZM12 24.4c2.566 0 4.954-.781 6.934-2.119l-1.568-2.32A9.55 9.55 0 0 1 12 21.6v2.8Zm-7.44-2.48A12.351 12.351 0 0 0 12 24.4v-2.8a9.551 9.551 0 0 1-5.76-1.919l-1.68 2.24ZM-.4 12c0 4.058 1.95 7.66 4.96 9.92l1.68-2.239A9.583 9.583 0 0 1 2.4 12H-.4ZM12-.4C5.152-.4-.4 5.152-.4 12h2.8A9.6 9.6 0 0 1 12 2.4V-.4ZM24.4 12C24.4 5.152 18.848-.4 12-.4v2.8a9.6 9.6 0 0 1 9.6 9.6h2.8Zm-5.466 10.281c3.295-2.226 5.466-6 5.466-10.281h-2.8a9.59 9.59 0 0 1-4.234 7.961l1.568 2.32ZM13.9 8.975c0 1.309-.954 2.175-1.9 2.175v2.8c2.7 0 4.7-2.335 4.7-4.975h-2.8ZM12 6.8c.946 0 1.9.866 1.9 2.175h2.8C16.7 6.335 14.7 4 12 4v2.8Zm-1.9 2.175c0-1.309.954-2.175 1.9-2.175V4C9.3 4 7.3 6.335 7.3 8.975h2.8ZM12 11.15c-.946 0-1.9-.866-1.9-2.175H7.3c0 2.64 2 4.975 4.7 4.975v-2.8Zm5.146 6.168A7.371 7.371 0 0 1 12 19.4v2.8a10.17 10.17 0 0 0 7.093-2.87l-1.947-2.012ZM12 15.6c2.18 0 4.05 1.342 4.822 3.25l2.595-1.052A8.002 8.002 0 0 0 12 12.8v2.8Zm-4.822 3.25A5.202 5.202 0 0 1 12 15.6v-2.8a8.002 8.002 0 0 0-7.417 4.998l2.595 1.051ZM12 19.4c-2 0-3.813-.792-5.146-2.082L4.907 19.33A10.17 10.17 0 0 0 12 22.2v-2.8Z" fill="#ff00ff" mask="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1333 6.06667C10.1333 8.31262 8.31262 10.1333 6.06667 10.1333C3.82071 10.1333 2 8.31262 2 6.06667C2 3.82071 3.82071 2 6.06667 2C8.31262 2 10.1333 3.82071 10.1333 6.06667ZM10.9992 9.59936C11.7131 8.60443 12.1333 7.38463 12.1333 6.06667C12.1333 2.71614 9.41719 0 6.06667 0C2.71614 0 0 2.71614 0 6.06667C0 9.41719 2.71614 12.1333 6.06667 12.1333C7.38457 12.1333 8.60431 11.7131 9.59922 10.9993C9.62742 11.0369 9.65861 11.0729 9.6928 11.1071L12.2928 13.7071C12.6833 14.0977 13.3165 14.0977 13.707 13.7071C14.0975 13.3166 14.0975 12.6834 13.707 12.2929L11.107 9.69292C11.0728 9.65874 11.0368 9.62756 10.9992 9.59936Z" fill="#909090"/>
</svg>

Before

Width:  |  Height:  |  Size: 785 B

View File

@ -1 +0,0 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.133 6.067a4.067 4.067 0 1 1-8.133 0 4.067 4.067 0 0 1 8.133 0ZM11 9.599A6.067 6.067 0 1 0 9.6 11c.028.038.06.074.094.108l2.6 2.6a1 1 0 1 0 1.414-1.414l-2.6-2.6a1.008 1.008 0 0 0-.108-.094Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 340 B

View File

@ -1,3 +0,0 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7372 8.96458L1.89656 15.8816C0.963879 16.3478 -0.00645849 15.3477 0.449387 14.4353C0.449387 14.4353 2.16481 10.9711 2.63665 10.0637C3.10849 9.15633 3.64864 8.99926 8.6646 8.35098C8.85021 8.32697 9.00215 8.1869 9.00215 7.99982C9.00215 7.81307 8.85021 7.67301 8.6646 7.649C3.64864 7.00071 3.10849 6.84364 2.63665 5.93624C2.16481 5.02918 0.449387 1.56465 0.449387 1.56465C-0.00645849 0.65258 0.963879 -0.347862 1.89656 0.118344L15.7372 7.03573C16.5319 7.43257 16.5319 8.5674 15.7372 8.96458Z" fill="#21262b"/>
</svg>

Before

Width:  |  Height:  |  Size: 623 B

View File

@ -1 +0,0 @@
<svg width="17" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m15.737 8.965-13.84 6.917c-.933.466-1.903-.534-1.448-1.447 0 0 1.716-3.464 2.188-4.371.471-.908 1.012-1.065 6.028-1.713.185-.024.337-.164.337-.351 0-.187-.152-.327-.337-.351-5.016-.648-5.557-.805-6.028-1.713A482.48 482.48 0 0 1 .449 1.565C-.006.653.964-.348 1.897.118l13.84 6.918a1.078 1.078 0 0 1 0 1.929Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 414 B

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.3625 9.2875C19.5625 9.8125 20.075 10.1625 20.6375 10.1625C21.3875 10.1625 22 10.775 22 11.525V12.475C22 13.225 21.3875 13.8375 20.6375 13.8375C20.075 13.8375 19.5625 14.1875 19.3625 14.7125C19.346 14.7538 19.3294 14.7958 19.3128 14.838C19.2538 14.9876 19.1932 15.1413 19.125 15.2875C18.8875 15.8 19 16.4 19.4 16.8C19.9375 17.325 19.9375 18.1875 19.4 18.725L18.725 19.4C18.2 19.9375 17.3375 19.9375 16.8 19.4C16.4125 19 15.8 18.8875 15.2875 19.125C15.1 19.2125 14.9125 19.2875 14.7125 19.3625C14.1875 19.5625 13.8375 20.075 13.8375 20.6375C13.8375 21.3875 13.225 22 12.475 22H11.525C10.775 22 10.1625 21.3875 10.1625 20.6375C10.1625 20.075 9.8125 19.5625 9.2875 19.3625C9.24617 19.346 9.20423 19.3294 9.16195 19.3128C9.01243 19.2538 8.85867 19.1932 8.7125 19.125C8.2 18.8875 7.6 19 7.2 19.4C6.675 19.9375 5.8125 19.9375 5.275 19.4L4.6 18.725C4.0625 18.2 4.0625 17.3375 4.6 16.8C5 16.4125 5.1125 15.8 4.875 15.2875C4.7875 15.1 4.7125 14.9125 4.6375 14.7125C4.4375 14.1875 3.925 13.8375 3.3625 13.8375C2.6125 13.8375 2 13.225 2 12.475V11.525C2 10.775 2.6125 10.1625 3.3625 10.1625C3.925 10.1625 4.4375 9.8125 4.6375 9.2875C4.67694 9.16129 4.72634 9.04005 4.77627 8.91751C4.80546 8.84587 4.83483 8.77379 4.8625 8.7C5.1 8.1875 4.9875 7.5875 4.5875 7.1875C4.05 6.6625 4.05 5.8 4.5875 5.2625L5.275 4.6C5.8 4.0625 6.6625 4.0625 7.2 4.6C7.5875 5 8.2 5.1125 8.7125 4.875C8.9 4.7875 9.0875 4.7 9.2875 4.6375C9.8125 4.4375 10.1625 3.925 10.1625 3.3625C10.1625 2.6125 10.775 2 11.525 2H12.475C13.225 2 13.8375 2.6125 13.8375 3.3625C13.8375 3.9375 14.1875 4.4375 14.7125 4.6375C14.7538 4.65403 14.7958 4.67056 14.838 4.68723C14.9876 4.74617 15.1413 4.80679 15.2875 4.875C15.8 5.1125 16.4 5 16.8 4.6C17.325 4.0625 18.1875 4.0625 18.725 4.6L19.4 5.275C19.9375 5.8 19.9375 6.6625 19.4 7.2C19 7.5875 18.8875 8.2 19.125 8.7125C19.2125 8.9 19.2875 9.0875 19.3625 9.2875ZM12 17C9.2375 17 7 14.7625 7 12C7 9.2375 9.2375 7 12 7C14.7625 7 17 9.2375 17 12C17 14.7625 14.7625 17 12 17Z" fill="#8d97a5"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M19.363 9.287c.2.525.712.875 1.274.875.75 0 1.363.613 1.363 1.363v.95c0 .75-.613 1.363-1.363 1.363-.562 0-1.075.35-1.274.875l-.05.125c-.06.15-.12.303-.188.45A1.338 1.338 0 0 0 19.4 16.8a1.35 1.35 0 0 1 0 1.925l-.675.675a1.35 1.35 0 0 1-1.925 0c-.387-.4-1-.512-1.513-.275a7.905 7.905 0 0 1-.575.238c-.524.2-.874.712-.874 1.274 0 .75-.613 1.363-1.363 1.363h-.95c-.75 0-1.363-.613-1.363-1.363 0-.562-.35-1.075-.875-1.274a23.677 23.677 0 0 0-.125-.05c-.15-.06-.303-.12-.45-.188A1.338 1.338 0 0 0 7.2 19.4a1.35 1.35 0 0 1-1.925 0l-.675-.675a1.35 1.35 0 0 1 0-1.925c.4-.387.513-1 .275-1.513a7.905 7.905 0 0 1-.237-.575 1.369 1.369 0 0 0-1.276-.874c-.75 0-1.362-.613-1.362-1.363v-.95c0-.75.612-1.363 1.362-1.363.563 0 1.075-.35 1.276-.875.039-.126.088-.247.138-.37.03-.071.059-.143.087-.217a1.338 1.338 0 0 0-.276-1.512 1.35 1.35 0 0 1 0-1.925l.688-.663a1.35 1.35 0 0 1 1.925 0c.388.4 1 .513 1.513.275.187-.088.375-.175.575-.237.524-.2.874-.713.874-1.276 0-.75.613-1.362 1.363-1.362h.95c.75 0 1.363.612 1.363 1.362 0 .575.35 1.075.875 1.276l.125.05c.15.058.303.119.45.187A1.338 1.338 0 0 0 16.8 4.6a1.35 1.35 0 0 1 1.925 0l.675.675a1.35 1.35 0 0 1 0 1.925c-.4.388-.512 1-.275 1.513.087.187.163.375.238.575ZM12 17c-2.762 0-5-2.238-5-5s2.238-5 5-5 5 2.238 5 5-2.238 5-5 5Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":1,"name":"Element","id":"element","values":{"variants":{"light":{"base":true,"default":true,"name":"Light","variables":{"background-color-primary":"#fff","background-color-secondary":"#f6f6f6","text-color":"#2E2F32","accent-color":"#03b381","error-color":"#FF4B55","fixed-white":"#fff","room-badge":"#61708b","link-color":"#238cf5"}},"dark":{"dark":true,"default":true,"name":"Dark","variables":{"background-color-primary":"#21262b","background-color-secondary":"#2D3239","text-color":"#fff","accent-color":"#03B381","error-color":"#FF4B55","fixed-white":"#fff","room-badge":"#61708b","link-color":"#238cf5"}}}},"source":{"built-assets":{"element-light":"theme-element-light.0779c91e.css","element-dark":"theme-element-dark.4718033a.css"},"runtime-asset":"theme-element-runtime.8f0a458e.css","derived-variables":["background-color-secondary--darker-7","background-color-primary--darker-10","background-color-secondary--darker-15","background-color-secondary--darker-10","background-color-secondary--darker-40","background-color-secondary--darker-5","background-color-secondary--darker-55","background-color-secondary--darker-35","fixed-white--darker-10","accent-color--darker-5","icon-color=background-color-secondary--darker-40","light-border=background-color-secondary--darker-5","light-text-color=background-color-secondary--darker-55","timeline-time-text-color=background-color-secondary--darker-35","icon-background=background-color-secondary--darker-7","right-panel-text-color=background-color-secondary--darker-35"],"icon":{"icon-url-0":"chevron-down.9a7440b9.svg?primary=icon-color","icon-url-1":"element-logo.86bc8565.svg?primary=accent-color","icon-url-2":"enable-grid.eef43c65.svg?primary=icon-color","icon-url-3":"settings.45b8e09f.svg?primary=icon-color","icon-url-4":"plus.49560f96.svg?primary=icon-color","icon-url-5":"disable-grid.371ceaaa.svg?primary=icon-color","icon-url-6":"search.21e0fd39.svg?primary=icon-color","icon-url-7":"clear.0d180c33.svg?primary=icon-color","icon-url-8":"chevron-left.b8b2c5fc.svg?primary=icon-color","icon-url-9":"clear.0d180c33.svg?primary=background-color-primary","icon-url-10":"chevron-right.885731d1.svg?primary=icon-color","icon-url-11":"chevron-left.b8b2c5fc.svg?primary=icon-color","icon-url-12":"vertical-ellipsis.70ab5d25.svg?primary=icon-color","icon-url-13":"clear.0d180c33.svg?primary=icon-color","icon-url-14":"send.7a090949.svg?primary=background-color-primary","icon-url-15":"paperclip.ec29fd9d.svg?primary=icon-color","icon-url-16":"clear.0d180c33.svg?primary=fixed-white","icon-url-17":"chevron-small.dfd7e618.svg?primary=icon-color","icon-url-18":"room-members.35ed0bf9.svg?primary=icon-color","icon-url-19":"encryption-status.8054183e.svg?primary=icon-color","icon-url-20":"e2ee-normal.bef76bd4.svg?primary=fixed-white","icon-url-21":"e2ee-disabled.8507165d.svg?primary=fixed-white","icon-url-22":"clear.0d180c33.svg?primary=icon-color","icon-url-23":"chevron-thin-left.d111869b.svg?primary=icon-color","icon-url-24":"plus.49560f96.svg?primary=icon-color"}}}

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.154 16.662C13.154 16.158 12.776 15.836 12.272 15.836C11.754 15.836 11.39 16.158 11.39 16.662C11.39 17.152 11.754 17.488 12.272 17.488C12.776 17.502 13.154 17.152 13.154 16.662ZM13.154 12C13.154 11.496 12.776 11.16 12.272 11.16C11.754 11.174 11.39 11.496 11.39 11.986C11.39 12.476 11.754 12.826 12.272 12.826C12.776 12.84 13.154 12.49 13.154 12ZM13.154 7.338C13.154 6.82 12.776 6.498 12.272 6.498C11.754 6.498 11.39 6.834 11.39 7.324C11.39 7.814 11.754 8.164 12.272 8.164C12.776 8.164 13.154 7.828 13.154 7.338Z" fill="#8d97a5"/>
</svg>

Before

Width:  |  Height:  |  Size: 685 B

View File

@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.154 16.662c0-.504-.378-.826-.882-.826-.518 0-.882.322-.882.826 0 .49.364.826.882.826.504.014.882-.336.882-.826Zm0-4.662c0-.504-.378-.84-.882-.84-.518.014-.882.336-.882.826 0 .49.364.84.882.84.504.014.882-.336.882-.826Zm0-4.662c0-.518-.378-.84-.882-.84-.518 0-.882.336-.882.826 0 .49.364.84.882.84.504 0 .882-.336.882-.826Z" fill="#ff00ff"/></svg>

Before

Width:  |  Height:  |  Size: 474 B

12
codestyle.md Normal file
View File

@ -0,0 +1,12 @@
# Code-style
- methods that return a promise should always use async/await
otherwise synchronous errors can get swallowed
you can return a promise without awaiting it though.
- only named exports, no default exports
otherwise it becomes hard to remember what was a default/named export
- should we return promises from storage mutation calls? probably not, as we don't await them anywhere. only read calls should return promises?
- we don't anymore
- don't use these features, as they are not widely enough supported.
- [lookbehind in regular expressions](https://caniuse.com/js-regexp-lookbehind)

83
doc/CSS.md Normal file
View File

@ -0,0 +1,83 @@
https://nio.chat/ looks nice.
We could do top to bottom gradients in default avatars to make them look a bit cooler. Automatically generate them from a single color, e.g. from slightly lighter to slightly darker.
## How to organize the CSS?
Can take ideas/adopt from OOCSS and SMACSS.
### Root
- maybe we should not assume `body` is the root, but rather a `.brawl` class. The root is where we'd set root level css variables, fonts?, etc. Should we scope all css to this root class? That could get painful with just vanilla css. We could use something like https://github.com/domwashburn/postcss-parent-selector to only do this at build time. Other useful plugin for postcss: https://github.com/postcss/postcss-selector-parser
We would still you `rem` for size units though.
### Class names
#### View
- view name?
#### Not quite a View
Some things might not be a view, as they don't have their own view model.
- a spinner, has .spinner for now
- avatar
#### modifier classes
are these modifiers?
- contrast-hi, contrast-mid, contrast-low
- font-large, font-medium, font-small
- large, medium, small (for spinner and avatar)
- hidden: hides the element, can be useful when not wanting to use an if-binding to databind on a css class
- inline: can be applied to any item if it needs to look good in an inline layout
- flex: can be applied to any item if it is placed in a flex container. You'd combine this with some other class to set a `flex` that makes sense, e.g.:
```css
.spinner.flex,
.avatar.flex,
.icon.flex,
button.flex {
flex: 0;
}
```
you could end up with a lot of these though?
well... for flex we don't really need a class, as `flex` doesn't do anything if the parent is not a flex container.
Modifier classes can be useful though. Should we prefix them?
### Theming
do we want as system with HSL or RGBA to define shades and contrasts?
we could define colors as HS and have a separate value for L:
```
/* for dark theme */
--lightness-mod: -1;
--accent-shade: 310, 70%;
/* then at every level */
--lightness: 60%;
/* add/remove (based on dark theme) 20% lightness */
--lightness: calc(var(--lightness) + calc(var(--lightness-mod) * 20%));
--bg-color: hsl(var(-accent-shade), var(--lightness));
```
this makes it easy to derive colors, but if there is no override with rga values, could be limiting.
I guess --fg-color and --bg-color can be those overrides?
what theme color variables do we want?
- accent color
- avatar/name colors
- background color (panels are shades of this?)
Themes are specified as JSON and need javascript to be set. The JSON contains colors in rgb, the theme code will generate css variables containing shades as specified? Well, that could be custom theming, but built-in themes should have full css flexibility.
what hierarchical variables do we want?
- `--fg-color` (we use this instead of color so icons and borders can also take the color, we could use the `currentcolor` constant for this though!)
- `--bg-color` (we use this instead of background so icons and borders can also take the color)
- `--lightness`
- `--size` for things like spinner, avatar

35
doc/FAQ.md Normal file
View File

@ -0,0 +1,35 @@
# FAQ
## What browsers are supported?
Internet Explorer 11, Chrome [1], Firefox [1] (not in a private window), Edge [1], Safari [1] and any mobile versions of these. It will probably also work on any derivatives of these.
1: Because of https://github.com/vector-im/hydrogen-web/issues/230, only [more recent versions](https://caniuse.com/mdn-javascript_operators_optional_chaining) are supported.
TorBrowser ships a crippled IndexedDB implementation and will not work. At some point we should support a memory store as a fallback, but that will still give a sub-par experience with end-to-end encryption.
It used work in pre-webkit Edge, to have it work on Windows Phone, but that support has probably bit-rotted as it isn't tested anymore.
## Is there a way to run the app as a desktop app?
You can install Hydrogen as a PWA using Chrome/Chromium on any platform or Edge on Windows. Gnome Web/Ephiphany also allows to "Install site as web application". There is no Electron build of Hydrogen, and there will likely be none in the near future, as Electron complicates the release process considerably. Once Hydrogen is more mature and feature complete, we might reconsider and use [Tauri](https://tauri.studio) if there are compelling use cases not possible with PWAs. For now though, we want to keep development and releasing fast and nimble ;)
## Is feature X supported?
If you can't find an easy way to locate the feature you are looking for, then the anwser is usually "no, not yet" :) But here are some things people have asked about in the past:
### How does newline work? Shift+Enter has no effect.
That's not yet a feature, as hydrogen just uses a single line text box for message input for now.
## How can I verify my session from Element?
You can only verify by comparing keys manually currently. In Element, go to your own profile in the right panel, click on the Hydrogen device and select Manually Verify by Text. The session key displayed should be the same as in the Hydrogen settings. You can't yet mark your Element session as trusted from Hydrogen.
## I want to host my own Hydrogen, how do I do that?
Published builds can be found at https://github.com/vector-im/hydrogen-web/releases. For building your own, you need to checkout the version you want to build, or master if you want to run bleeding edge, and run `yarn install` and then `yarn build` in a console (and install nodejs >= 15 and yarn if you haven't yet). Now you should find all the files needed to host Hydrogen in the `target/` folder, just copy them all over to your server. As always, don't host your client on the same [origin](https://web.dev/same-origin-policy/#what's-considered-same-origin) as your homeserver.
## I want to embed Hydrogen in my website, how should I do that?
Hydrogen aims to be usable as an SDK, and while it is still early days, you can find some documentation how to do that in [SDK.md](SDK.md).

8
doc/GOAL.md Normal file
View File

@ -0,0 +1,8 @@
goal:
write client that works on lumia 950 phone, so I can use matrix on my phone.
try approach offline to indexeddb. go low-memory, and test the performance of storing every event individually in indexeddb.
try to use little bandwidth, mainly by being an offline application and storing all requested data in indexeddb.
be as functional as possible while offline

11
doc/IMPORT-ISSUES.md Normal file
View File

@ -0,0 +1,11 @@
## How to import common-js dependency using ES6 syntax
---
Until [#6632](https://github.com/vitejs/vite/issues/6632) is fixed, such imports should be done as follows:
```ts
import * as pkg from "off-color";
// @ts-ignore
const offColor = pkg.offColor ?? pkg.default.offColor;
```
This way build, dev server and unit tests should all work.

75
doc/INDEXEDDB.md Normal file
View File

@ -0,0 +1,75 @@
## Promises, async/await and indexedDB
Doesn't indexedDB close your transaction if you don't queue more requests from an idb event handler?
So wouldn't that mean that you can't use promises and async/await when using idb?
It used to be like this, and for IE11 on Win7 (not on Windows 10 strangely enough), it still is like this.
Here we manually flush the promise queue synchronously at the end of an idb event handler.
In modern browsers, indexedDB transactions should only be closed after flushing the microtask queue of the event loop,
which is where promises run.
Keep in mind that indexedDB events, just like any other DOM event, are fired as macro tasks.
Promises queue micro tasks, of which the queue is drained before proceeding to the next macro task.
This also means that if a transaction is completed, you will only receive the event once you are ready to process the next macro tasks.
That doesn't prevent any placed request from throwing TransactionInactiveError though.
## TransactionInactiveError in Safari
Safari doesn't fully follow the rules above, in that if you open a transaction,
you need to "use" (not sure if this means getting a store or actually placing a request) it straight away,
without waiting for any *micro*tasks. See comments about Safari at https://github.com/dfahlander/Dexie.js/issues/317#issue-178349994.
Another failure mode perceived in Hydrogen on Safari is that when the (readonly) prepareTxn in sync wasn't awaited to be completed before opening and using the syncTxn.
I haven't found any documentation online about this at all. Awaiting prepareTxn.complete() fixed the issue below. It's strange though the put does not fail.
## Diagnose of problem
What is happening below is:
- in the sync loop:
- we first open a readonly txn on inboundGroupSessions, which we don't use in the example below
- we then open a readwrite txn on session, ... (does not overlap with first txn)
- first the first incremental sync on a room (!YxKeAxtNcDZDrGgaMF:matrix.org) it seems to work well
- on a second incremental sync for that same room, the first get throws TransactionInactiveError for some reason.
- the put in the second incremental sync somehow did not throw.
So it looks like safari doesn't like (some) transactions still being active while a second one is being openened, even with non-overlapping stores.
For now I haven't awaited every read txn in the app, as this was the only place it fails, but if this pops up again in safari, we might have to do that.
Keep in mind that the `txn ... inactive` logs are only logged when the "complete" or "abort" events are processed,
which happens in a macro task, as opposed to all of our promises, which run in a micro task.
So the transaction is likely to have closed before it appears in the logs.
```
[Log] txn 4504181722375185 active on inboundGroupSessions
[Log] txn 861052256474256 active on session, roomSummary, roomState, roomMembers, timelineEvents, timelineFragments, pendingEvents, userIdentities, groupSessionDecryptions, deviceIdentities, outboundGroupSessions, operations, accountData
[Info] hydrogen_session_5286139994689036.session.put({"key":"sync","value":{"token":"s1572540047_757284957_7660701_602588550_435736037_1567300_101589125_347651623_132704","filterId":"2"}})
[Info] hydrogen_session_5286139994689036.userIdentities.get("@bwindels:matrix.org")
[Log] txn 4504181722375185 inactive
[Log] * applying sync response to room !YxKeAxtNcDZDrGgaMF:matrix.org ...
[Info] hydrogen_session_5286139994689036.roomMembers.put({"roomId":"!YxKeAxtNcDZDrGgaMF:matrix.org","userId":"@bwindels:matrix.org","membership":"join","avatarUrl":"mxc://matrix.org/aerWVfICBMcyFcEyREcivLuI","displayName":"Bruno","key":"!YxKeAxtNcDZDrGgaMF:matrix.org|@bwindels:matrix.org"})
[Info] hydrogen_session_5286139994689036.roomMembers.get("!YxKeAxtNcDZDrGgaMF:matrix.org|@bwindels:matrix.org")
[Info] hydrogen_session_5286139994689036.timelineEvents.add({"fragmentId":0,"eventIndex":2147483658,"roomId":"!YxKeAxtNcDZDrGgaMF:matrix.org","event":{"content":{"body":"haha","msgtype":"m.text"},"origin_server_ts":1601457573756,"sender":"@bwindels:matrix.org","type":"m.room.message","unsigned":{"age":8360},"event_id":"$eD9z73-lCpXBVby5_fKqzRZzMVHiPzKbE_RSZzqRKx0"},"displayName":"Bruno","avatarUrl":"mxc://matrix.org/aerWVfICBMcyFcEyREcivLuI","key":"!YxKeAxtNcDZDrGgaMF:matrix.org|00000000|8000000a","eventIdKey":"!YxKeAxtNcDZDrGgaMF:matrix.org|$eD9z73-lCpXBVby5_fKqzRZzMVHiPzKbE_RSZzqRKx0"})
[Info] hydrogen_session_5286139994689036.roomSummary.put({"roomId":"!YxKeAxtNcDZDrGgaMF:matrix.org","name":"!!!test8!!!!!!","lastMessageBody":"haha","lastMessageTimestamp":1601457573756,"isUnread":true,"encryption":null,"lastDecryptedEventKey":null,"isDirectMessage":false,"membership":"join","inviteCount":0,"joinCount":2,"heroes":null,"hasFetchedMembers":false,"isTrackingMembers":false,"avatarUrl":null,"notificationCount":5,"highlightCount":0,"tags":{"m.lowpriority":{}}})
[Log] txn 861052256474256 inactive
[Info] syncTxn committed!!
... two more unrelated sync responses ...
[Log] starting sync request with since s1572540191_757284957_7660742_602588567_435736063_1567300_101589126_347651632_132704 ...
[Log] txn 8104296957004707 active on inboundGroupSessions
[Log] txn 2233038992157489 active on session, roomSummary, roomState, roomMembers, timelineEvents, timelineFragments, pendingEvents, userIdentities, groupSessionDecryptions, deviceIdentities, outboundGroupSessions, operations, accountData
[Info] hydrogen_session_5286139994689036.session.put({"key":"sync","value":{"token":"s1572540223_757284957_7660782_602588579_435736078_1567300_101589130_347651633_132704","filterId":"2"}})
[Log] * applying sync response to room !YxKeAxtNcDZDrGgaMF:matrix.org ...
[Info] hydrogen_session_5286139994689036.roomMembers.get("!YxKeAxtNcDZDrGgaMF:matrix.org|@bwindels:matrix.org")
[Warning] stopping sync because of error
[Error] StorageError: get("!YxKeAxtNcDZDrGgaMF:matrix.org|@bwindels:matrix.org") failed on txn with stores accountData, deviceIdentities, groupSessionDecryptions, operations, outboundGroupSessions, pendingEvents, roomMembers, roomState, roomSummary, session, timelineEvents, timelineFragments, userIdentities on hydrogen_session_5286139994689036.roomMembers: (name: TransactionInactiveError) (code: 0) Failed to execute 'get' on 'IDBObjectStore': The transaction is inactive or finished.
(anonymous function)
asyncFunctionResume
(anonymous function)
promiseReactionJobWithoutPromise
promiseReactionJob
[Log] newStatus "SyncError"
[Log] txn 8104296957004707 inactive
[Log] txn 2233038992157489 inactive
```

19
doc/QUESTIONS.md Normal file
View File

@ -0,0 +1,19 @@
remaining problems to resolve:
how to store timelime fragments that we don't yet know how they should be sorted wrt the other events and gaps. the case with event permalinks and showing the replied to event when rendering a reply (anything from /context).
either we could put timeline pieces that were the result of /context in something that is not the timeline. Gaps also don't really make sense there ... You can just paginate backwards and forwards. Or maybe still in the timeline but in a different scope not part of the sortKey, scope: live, or scope: piece-1204. While paginating, we could keep the start and end event_id of all the scopes in memory, and set a marker on them to stitch them together?
Hmmm, I can see the usefullness of the concept of timeline set with multiple timelines in it for this. for the live timeline it's less convenient as you're not bothered so much by the stitching up, but for /context pieces that run into the live timeline while paginating it seems more useful... we could have a marker entry that refers to the next or previous scope ... this way we could also use gap entries for /context timelines, just one on either end.
the start and end event_id of a scope, keeping that in memory, how do we make sure this is safe taking transactions into account? our preferred strategy so far has been to read everything from store inside a txn to make sure we don't have any stale caches or races. Would be nice to keep this.
so while paginating, you'd check the event_id of the event against the start/end event_id of every scope to see if stitching is in order, and add marker entries if so. Perhaps marker entries could also be used to stitch up rooms that have changed versioning?
What does all of this mean for using sortKey as an identifier? Will we need to take scope into account as well everywhere?
we'll need to at least contemplate how room state will be handled with all of the above.
how do we deal with the fact that an event can be rendered (and updated) multiple times in the timeline as part of replies.
room state...

14
doc/RELEASE.md Normal file
View File

@ -0,0 +1,14 @@
release:
- bundling css files
- bundling javascript
- run index.html template for release as opposed to develop version?
- make list of all resources needed (images, html page)
- create appcache manifest + service worker
- create tarball + sign
- make gh release with tarball + signature
publish:
- extract tarball
- upload to static website
- overwrite index.html
- overwrite service worker & appcache manifest
- put new version files under /x.x.x

116
doc/SDK.md Normal file
View File

@ -0,0 +1,116 @@
# Hydrogen View SDK
The Hydrogen view SDK allows developers to integrate parts of the Hydrogen application into the UI of their own application. Hydrogen is written with the MVVM pattern, so to construct a view, you'd first construct a view model, which you then pass into the view. For most view models, you will first need a running client.
## Example
The Hydrogen SDK requires some assets to be shipped along with your app for things like downloading attachments, and end-to-end encryption. A convenient way to make this happen is provided by the SDK (importing `hydrogen-view-sdk/paths/vite`) but depends on your build system. Currently, only [vite](https://vitejs.dev/) is supported, so that's what we'll be using in the example below.
You can create a vite project using the following commands:
```sh
# you can pick "vanilla-ts" here for project type if you're not using react or vue
yarn create vite
cd <your-project-name>
yarn
yarn add hydrogen-view-sdk
```
You should see a `index.html` in the project root directory, containing an element with `id="app"`. Add the attribute `class="hydrogen"` to this element, as the CSS we'll include from the SDK assumes for now that the app is rendered in an element with this classname.
If you go into the `src` directory, you should see a `main.ts` file. If you put this code in there, you should see a basic timeline after login and initial sync have finished (might take a while before you see anything on the screen actually).
You'll need to provide the username and password of a user that is already in the [#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) room (or change the room id).
```ts
import {
Platform,
Client,
LoadStatus,
createNavigation,
createRouter,
RoomViewModel,
TimelineView,
viewClassForTile
} from "hydrogen-view-sdk";
import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url';
import workerPath from 'hydrogen-view-sdk/main.js?url';
import olmWasmPath from '@matrix-org/olm/olm.wasm?url';
import olmJsPath from '@matrix-org/olm/olm.js?url';
import olmLegacyJsPath from '@matrix-org/olm/olm_legacy.js?url';
const assetPaths = {
downloadSandbox: downloadSandboxPath,
worker: workerPath,
olm: {
wasm: olmWasmPath,
legacyBundle: olmLegacyJsPath,
wasmBundle: olmJsPath
}
};
import "hydrogen-view-sdk/assets/theme-element-light.css";
// OR import "hydrogen-view-sdk/assets/theme-element-dark.css";
async function main() {
const app = document.querySelector<HTMLDivElement>('#app')!
const config = {};
const platform = new Platform({container: app, assetPaths, config, options: { development: import.meta.env.DEV }});
const navigation = createNavigation();
platform.setNavigation(navigation);
const urlRouter = createRouter({
navigation: navigation,
history: platform.history
});
urlRouter.attach();
const client = new Client(platform);
const loginOptions = await client.queryLogin("matrix.org").result;
client.startWithLogin(loginOptions.password("username", "password"));
await client.loadStatus.waitFor((status: string) => {
return status === LoadStatus.Ready ||
status === LoadStatus.Error ||
status === LoadStatus.LoginFailed;
}).promise;
if (client.loginFailure) {
alert("login failed: " + client.loginFailure);
} else if (client.loadError) {
alert("load failed: " + client.loadError.message);
} else {
const {session} = client;
// looks for room corresponding to #element-dev:matrix.org, assuming it is already joined
const room = session.rooms.get("!bEWtlqtDwCLFIAKAcv:matrix.org");
const vm = new RoomViewModel({
room,
ownUserId: session.userId,
platform,
urlCreator: urlRouter,
navigation,
});
await vm.load();
const view = new TimelineView(vm.timelineViewModel, viewClassForTile);
app.appendChild(view.mount());
}
}
main();
```
## Typescript support
Typescript support is not yet available while we're converting the Hydrogen codebase to Typescript.
In your `src` directory, you'll need to add a `.d.ts` (can be called anything, e.g. `deps.d.ts`)
containing this snippet to make Typescript not complain that `hydrogen-view-sdk` doesn't have types:
```ts
declare module "hydrogen-view-sdk";
```
## API Stability
This library follows semantic versioning; there is no API stability promised as long as the major version is still 0. Once 1.0.0 is released, breaking changes will be released with a change in major versioning.
## Third-party licenses
This package bundles the bs58 package ([license](https://github.com/cryptocoinjs/bs58/blob/master/LICENSE)), and the Inter font ([license](https://github.com/rsms/inter/blob/master/LICENSE.txt)).

238
doc/SENDING.md Normal file
View File

@ -0,0 +1,238 @@
# Remaining stuffs
- don't swallow send errors, they should probably appear in the room error?
- not sure it makes sense to show them where the composer is,
because they might get sent a long time after you enter them in brawl,
so you don't neccessarily have the context of the composer anymore
- local echo
takes care of rate limiting,
and sending events from different rooms in parallel,
NO: txnIds are created inside room. ~~making txnIds? ... it's rooms though that will receive the event in their sync response~~
EventSender:
// used for all kinds of events (state, redaction, ...)
sendEvent(roomId, pendingEvent...Entry?)
how will we do local echo?
a special kind of entry? will they be added to the same list?
how do we store pending events?
OBSOLETE, see PendingEvent below:
separate store with:
roomId
txnId
priority
queueOrder
partialEvent
remoteEventId ? // once we've received a response, but haven't received the event through sync yet. would be nice if you refresh then, that the message doesn't disappear. Actually, we don't need the event id for that, just to only delete it when we receive something down the sync with the same transaction id?
// all the fields that might need to be sent to the server when posting a particular kind of event
PendingEvent
queueOrder //is this high enough to
priority //high priority means it also takes precedence over events sent in other rooms ... but how will that scheduling work?
txnId
type
stateKey
redacts
content
localRelatedId //what's the id? queueOrder? e.g. this would be a local id that this event relates to. We might need an index on it to update the PendingEvent once the related PendingEvent is sent.
blob: a blob that needs to be uploaded and turned into a mxc to put into the content.url field before sending the event
there is also info.thumbnail_url
blobMimeType? Or stored as part of blob?
//blobUploadByteOffset: to support resumable uploads?
so when sending an event, we don't post a whole object, just the content, or a state key and content, or a redacts id.
however, it's somewhat interesting to pretend an event has the same structure before it is sent, then when it came down from the server, so all the logic can reuse the same structure...
we could potentially have a PendingEventEntry, that shares most of its API with EventEntry ... but is there a good reason to do so?
PendingEvent would be a bit less hackish this way
we could have a base class shared between PendingEventEntry and EventEntry to do most of the work, and only have things like getStateKey, getContent, ... in the subclasses?
wrt to pending events in the timeline, their entry would have a special fragmentId, always placing them behind the last "real" event, and the queueOrder could be the entryIndex.
how will local echo work for:
relations
this would depend a lot on how relations work ... I guess the only relevant part is that aggregation can be undo, and redone
redactions
again, depends a lot on how redactions would be implemented. somehow
images (that are uploading)
we could have a createUrl/destroyUrl method? returns local blob url for pending event
and also an optional progress method?
how will the EventSender tell the rooms to start submitting events again when coming online again after being offline for a while?
we'll need to support some states for the UI:
- offline
- queued
- uploading attachment ?
- sending
- sent
offline is an external factor ... we probably need to deal with it throughout the app / matrix level in some way ...
- we could have callback on room for online/offline that is invoked by session, where they can start sending again?
perhaps with a transaction already open on the pending_events store
How could the SendQueue update the timeline? By having an ObservableMap for it's entries in the queue
Room
SendQueue
Timeline
steps of sending
```javascript
//at some point:
// sender is the thing that is shared across rooms to handle rate limiting.
const sendQueue = new SendQueue({roomId, hsApi, sender, storage});
await sendQueue.load(); //loads the queue?
//might need to load members for e2e rooms ...
//events should be encrypted before storing them though ...
// terminology ...?
// task: to let us wait for it to be our turn
// given rate limiting
class Sender {
acquireSlot() {
return new SendSlot();
}
}
// terminology ...?
// task: after waiting for it to be our turn given rate-limiting,
// send the actual thing we want to send.
// this should be used for all rate-limited apis... ?
class SendSlot {
sendContent(content) {
}
sendRedaction() {
}
uploadMedia() {
}
}
class SendQueue {
// when trying to send
enqueueEvent(pendingEvent) {
// store event
// if online and not running send loop
// start sending loop
}
// send loop
// findNextPendingEvent comes from memory or store?
// if different object then in timeline, how to update timeline thingy?
// by entryKey? update it?
_sendLoop() {
while (let pendingEvent = await findNextPendingEvent()) {
pendingEvent.status = QUEUED;
try {
const mediaSlot = await this.sender.acquireSlot();
const mxcUrl = await mediaSlot.uploadMedia(pendingEvent.blob);
pendingEvent.content.url = mxcUrl;
const contentSlot = await this.sender.acquireSlot();
contentSlot.sendContent(pendingEvent.content);
pendingEvent.status = SENDING;
await slot.sendContent(...);
} catch (err) {
//offline
}
pendingEvent.status = SENT;
}
}
resumeSending(online) {
// start loop again when back online
}
// on sync, when received an event with transaction_id
// the first is the transaction_id,
// the second is the storage transaction to modify the pendingevent store if needed
receiveRemoteEcho(txnId, txn) {
}
// returns entries? to be appended to timeline?
// return an ObservableList here? Rather ObservableMap? what ID? queueOrder? that won't be unique over time?
// wrt to relations and redactions, we will also need the list of current
// or we could just do a lookup of the local id to remote once
// it's time to send an event ... perhaps we already have the txn open anyways.
// so we will need to store the event_id returned from /send...
// but by the time it's time to send an event, the one it relates to might already have been
// remove from pendingevents?
// maybe we should have an index on relatedId or something stored in pendingevents and that way
// we can update it once the relatedto event is sent
// ok, so we need an index on relatedId, not the full list for anything apart from timeline display? think so ...
get entriesMap() {
}
}
class Room {
resumeSending(online) {
if (online) {
this.sendQueue.setOnline(online);
}
}
}
```
we were thinking before of having a more lightweight structure to export from timeline, where we only keep a sorted list/set of keys in the collection, and we emit ranges of sorted keys that are either added, updated or removed. we could easily join this with the timeline and values are only stored by the TilesCollection. We do however need to peek into the queue to update local relatedTo ids.
probably best to keep send queue in memory.
so, persistence steps in sending:
- get largest queueOrder + 1 as id/new queueOrder
- the downside of this that when the last event is sent at the same time as adding a new event it would become an update? but the code paths being separate (receiveRemoteEcho and enqueueEvent) probably prevent this.
- persist incoming pending event
- update with remote id if relatedId for pending event
- update once attachment(s) are sent
- send in-memory updates of upload progress through pending event entry
- if the media store supports resumable uploads, we *could* also periodically store how much was uploaded already. But the current REST API can't support this.
- update once sent (we don't remove here until we've receive remote echo)
- store the remote event id so events that will relate to this pending event can get the remote id through getRelateToId()
- remove once remote echo is received
(Pending)EventEntry will need a method getRelateToId() that can return an instance of LocalId or something for unsent events
if we're not rate limited, we'll want to upload attachments in parallel with sending messages before attachee event.
so as long as not rate limited, we'd want several queues to send per room
```
sender (room 1)
---------------------
^ ^
event1 attachment1
^ |
event2-------
```
later on we can make this possible, for now we just upload the attachments right before event.
so, we need to write:
RateLimitedSender
all rate-limited rest api calls go through here so it can coordinate which ones should be prioritized and not
do more requests than needed while rate limited. It will have a list of current requests and initially just go from first to last but later could implement prioritizing the current room, events before attachments, ...
SendQueue (talks to store, had queue logic) for now will live under timeline as you can't send events for rooms you are not watching? could also live under Room so always available if needed
PendingEvent (what the store returns) perhaps doesn't even need a class? can all go in the entry
PendingEventEntry (conforms to Entry API)
can have static helper functions to create given kind of events
PendingEventEntry.stateEvent(type, stateKey, content)
PendingEventEntry.event(type, content, {url: file, "info.thumbnail_url": thumb_file})
PendingEventEntry.redaction(redacts)
PendingEventStore
add()
maxQueueOrder
getAll()
get()
update()
remove()
make sure to handle race between /sync and /send (e.g. /sync with sent event may come in before /send returns)

22
doc/SKINNING.md Normal file
View File

@ -0,0 +1,22 @@
# Replacing javascript files
Any source file can be replaced at build time by mapping the path in a JSON file passed in to the build command, e.g. `yarn build --override-imports customizations.json`. The file should be written like so:
```json
{
"src/platform/web/ui/session/room/timeline/TextMessageView.js": "src/platform/web/ui/session/room/timeline/MyTextMessageView.js"
}
```
The paths are relative to the location of the mapping file, but the mapping file should be in a parent directory of the files you want to replace.
You should see a "replacing x with y" line (twice actually, for the normal and legacy build).
# Injecting CSS
You can override the location of the main css file with the `--override-css <file>` option to the build script. The default is `src/platform/web/ui/css/main.css`, which you probably want to import from your custom css file like so:
```css
@import url('src/platform/web/ui/css/main.css');
/* additions */
```

204
doc/THEMING.md Normal file
View File

@ -0,0 +1,204 @@
# Theming Documentation
## Basic Architecture
A **theme collection** in Hydrogen is represented by a `manifest.json` file and a `theme.css` file.
The manifest specifies variants (eg: dark,light ...) each of which is a **theme** and maps to a single css file in the build output.
Each such theme is produced by changing the values of variables in the base `theme.css` file with those specified in the variant section of the manifest:
![](images/theming-architecture.png)
More in depth explanations can be found in later sections.
## Structure of `manifest.json`
[See theme.ts](../src/platform/types/theme.ts)
## Variables
CSS variables specific to a particular variant are specified in the `variants` section of the manifest:
```json=
"variants": {
"light": {
...
"variables": {
"background-color-primary": "#fff",
"text-color": "#2E2F32",
}
},
"dark": {
...
"variables": {
"background-color-primary": "#21262b",
"text-color": "#fff",
}
}
}
```
These variables will appear in the css file (theme.css):
```css=
body {
background-color: var(--background-color-primary);
color: var(--text-color);
}
```
During the build process, this would result in the creation of two css files (one for each variant) where the variables are substitued with the corresponding values specified in the manifest:
*element-light.css*:
```css=
body {
background-color: #fff;
color: #2E2F32;
}
```
*element-dark.css*:
```css=
body {
background-color: #21262b;
color: #fff;
}
```
## Derived Variables
In addition to simple substitution of variables in the stylesheet, it is also possible to instruct the build system to first produce a new value from the base variable value before the substitution.
Such derived variables have the form `base_css_variable--operation-arg` and can be read as:
apply `operation` to `base_css_variable` with argument `arg`.
Continuing with the previous example, it possible to specify:
```css=
.left-panel {
/* background color should be 20% more darker
than background-color-primary */
background-color: var(--background-color-primary--darker-20);
}
```
Currently supported operations are:
| Operation | Argument | Operates On |
| -------- | -------- | -------- |
| darker | percentage | color |
| lighter | percentage | color |
## Aliases
It is possible give aliases to variables in the `theme.css` file:
```css=
:root {
font-size: 10px;
/* Theme aliases */
--icon-color: var(--background-color-secondary--darker-40);
}
```
It is possible to further derive from these aliased variables:
```css=
div {
background: var(--icon-color--darker-20);
--my-alias: var(--icon-color--darker-20);
/* Derive from aliased variable */
color: var(--my-alias--lighter-15);
}
```
## Colorizing svgs
Along with a change in color-scheme, it may be necessary to change the colors in the svg icons and images.
This can be done by supplying the preferred colors with query parameters:
`my-awesome-logo.svg?primary=base-variable-1&secondary=base-variable-2`
This instructs the build system to colorize the svg with the given primary and secondary colors.
`base-variable-1` and `base-variable-2` are the css-variables specified in the `variables` section of the manifest.
For colorizing svgs, the source svg must use `#ff00ff` as the primary color and `#00ffff` as the secondary color:
| ![](images/svg-icon-example.png) | ![](images/coloring-process.png) |
| :--: |:--: |
| **original source image** | **transformation process** |
## Creating your own theme variant in Hydrogen
If you're looking to change the color-scheme of the existing Element theme, you only need to add your own variant to the existing `manifest.json`.
The steps are fairly simple:
1. Copy over an existing variant to the variants section of the manifest.
2. Change `dark`, `default` and `name` fields.
3. Give new values to each variable in the `variables` section.
4. Build hydrogen.
## Creating your own theme collection in Hydrogen
If a theme variant does not solve your needs, you can create a new theme collection with a different base `theme.css` file.
1. Create a directory for your new theme-collection under `src/platform/web/ui/css/themes/`.
2. Create `manifest.json` and `theme.css` files within the newly created directory.
3. Populate `manifest.json` with the base css variables you wish to use.
4. Write styles in your `theme.css` file using the base variables, derived variables and colorized svg icons.
5. Tell the build system where to find this theme-collection by providing the location of this directory to the `themeBuilder` plugin in `vite.config.js`:
```json=
...
themeBuilder({
themeConfig: {
themes: {
element: "./src/platform/web/ui/css/themes/element",
awesome: "path/to/theme-directory"
},
default: "element",
},
compiledVariables,
}),
...
```
6. Build Hydrogen.
## Changing the default theme
To change the default theme used in Hydrogen, modify the `defaultTheme` field in `config.json` file (which can be found in the build output):
```json=
"defaultTheme": {
"light": theme-id,
"dark": theme-id
}
```
Here *theme-id* is of the form `theme-variant` where `theme` is the key used when specifying the manifest location of the theme collection in `vite.config.js` and `variant` is the key used in variants section of the manifest.
Some examples of theme-ids are `element-dark` and `element-light`.
To find the theme-id of some theme, you can look at the built-asset section of the manifest in the build output.
This default theme will render as "Default" option in the theme-chooser dropdown. If the device preference is for dark theme, the dark default is selected and vice versa.
**You'll need to reload twice so that Hydrogen picks up the config changes!**
# Derived Theme(Collection)
This allows users to theme Hydrogen without the need for rebuilding. Derived theme collections can be thought of as extensions (derivations) of some existing build time theme.
## Creating a derived theme:
Here's how you create a new derived theme:
1. You create a new theme manifest file (eg: theme-awesome.json) and mention which build time theme you're basing your new theme on using the `extends` field. The base css file of the mentioned theme is used for your new theme.
2. You configure the theme manifest as usual by populating the `variants` field with your desired colors.
3. You add your new theme manifest to the list of themes in `config.json`.
Refresh Hydrogen twice (once to refresh cache, and once to load) and the new theme should show up in the theme chooser.
## How does it work?
For every theme collection in hydrogen, the build process emits a runtime css file which like the built theme css file contains variables in the css code. But unlike the theme css file, the runtime css file lacks the definition for these variables:
CSS for the built theme:
```css
:root {
--background-color-primary: #f2f20f;
}
body {
background-color: var(--background-color-primary);
}
```
and the corresponding runtime theme:
```css
/* Notice the lack of definiton for --background-color-primary here! */
body {
background-color: var(--background-color-primary);
}
```
When hydrogen loads a derived theme, it takes the runtime css file of the extended theme and dynamically adds the variable definition based on the values specified in the manifest. Icons are also colored dynamically and injected as variables using Data URIs.

77
doc/TODO.md Normal file
View File

@ -0,0 +1,77 @@
# Minimal thing to get working
- DONE: finish summary store
- DONE: move "sdk" bits over to "matrix" directory
- DONE: add eventemitter
- DONE: make sync work
- DONE: store summaries
- DONE: setup editorconfig
- DONE: setup linting (also in editor)
- DONE: store timeline
- DONE: store state
- DONE: make summary work better (name and joined/inviteCount doesn't seem to work well)
- DONE: timeline doesn't seem to recover it's key well upon loading, the query in load seems to never yield an event in the persister
- DONE: map DOMException to something better
- it's pretty opaque now when something idb related fails. DOMException has these fields:
code: 0
message: "Key already exists in the object store."
name: "ConstraintError"
- DONE: emit events so we can start showing something on the screen maybe?
- DONE: move session._rooms over to Map, so we can iterate over it, ...
- DONE: build a very basic interface with
- DONE: a start/stop sync button
- DONE: a room list sorted alphabetically
- DONE: do some preprocessing on sync response which can then be used by persister, summary, timeline
- DONE: support timeline
- DONE: clicking on a room list, you see messages (userId -> body)
- DONE: style minimal UI
- DONE: implement gap filling and fragments (see FRAGMENTS.md)
- DONE: allow collection items (especially tiles) to self-update
- improve fragmentidcomparer::add
- DONE: better UI
- fix MappedMap update mechanism
- see if in BaseObservableMap we need to change ...params
- DONE: put sync button and status label inside SessionView
- fix some errors:
- find out if `(this._emitCollectionUpdate)(this)` is different than `this._emitCollectionUpdate(this)`
- got "database tried to mutate when not allowed" or something error as well
- find out why when RoomPersister.(\_createGapEntry/\_createEventEntry) we remove .buffer the transaction fails (good), but upon fixing and refreshing is missing a message! syncToken should not be saved, so why isn't this again in the sync response and now the txn does succeed?
- DONE: take access token out of IDB? this way it can be stored in a more secure thing for non-web clients, together wit encryption key for olm sessions ... ? like macos keychain, gnome keyring, ... maybe using https://www.npmjs.com/package/keytar
- DONE: experiment with using just a normal array with 2 numbers for sortkeys, to work in Edge as well.
- DONE: send messages
- DONE: fill gaps with call to /messages
- DONE: build script
- DONE: take dev index.html, run some dom modifications to change script tag with `parse5`.
- DONE: create js bundle, rollup
- DONE: create css bundle, postcss, probably just need postcss-import for now, but good to have more options
- DONE: put all in /target
- have option to run it locally to test
- deploy script
- upload /target to github pages
- DONE: offline available
- both offline mechanisms have (filelist, version) as input for their template:
- create appcache manifest with (index.html, brawl.js, brawl.css) and print version number in it
- create service worker wit file list to cache (at top const files = "%%FILES_ARRAY%%", version = "%%VERSION%%")
- write web manifest
- DONE: delete and clear sessions from picker
- option to close current session and go back to picker
- accept invite
- member list
- e2e encryption
- sync retry strategy
- instead of stopping sync on fetch error, show spinner and status and have auto retry strategy
- create room
- join room
- leave room
- unread rooms, badge count, sort rooms by activity
- DONE: create sync filter
- DONE: lazy loading members
- decide denormalized data in summary vs reading from multiple stores PER room on load
- allow Room/Summary class to be subclassed and store additional data?
- store account data, support read markers

38
doc/TS-MIGRATION.md Normal file
View File

@ -0,0 +1,38 @@
# Typescript style guide
## Use `type` rather than `interface` for named parameters and POJO return values.
`type` and `interface` can be used somewhat interchangeably, but let's use `type` to describe data and `interface` to describe (polymorphic) behaviour.
Good examples of data are option objects to have named parameters, and POJO (plain old javascript objects) without any methods, just fields.
Also see [this playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBACghgJwgO2AeTMAlge2QZygF4oBvAKCiqmTgFsIAuKfYBLZAcwG5LqATCABs4IAPzNkAVzoAjCAl4BfcuVCQoAYQAWWIfwzY8hEvCSpDuAlABkZPlQDGOITgTNW7LstWOR+QjMUYHtqKGcCNilHYDcAChxMK3xmIIsk4wBKewcoFRVyPzgArV19KAgAD2AUfkDEYNDqCM9o2IQEjIJmHT0DLvxsijCw-ClIDsSjAkzeEebjEIYAuE5oEgADABJSKeSAOloGJSgsQh29433nVwQlDbnqfKA)
## Use `type foo = { [key: string]: any }` for types that you intend to fill in later.
For instance, if you have a method such as:
```js
function load(options) {
// ...
}
```
and you intend to type options at some later point, do:
```ts
type Options = { [key: string]: any}
```
This makes it much easier to add the necessary type information at a later time.
## Use `object` or `Record<string, any>` to describe a type that accepts any javascript object.
Sometimes a function or method may genuinely need to accept any object; eg:
```js
function encodeBody(body) {
// ...
}
```
In this scenario:
- Use `object` if you know that you will not access any property
- Use `Record<string, any>` if you need to access some property
Both usages prevent the type from accepting primitives (eg: string, boolean...).
If using `Record`, ensure that you have guards to check that the properties really do exist.

3
doc/UI/index.md Normal file
View File

@ -0,0 +1,3 @@
# Index for UI code
1. [Rendering DOM elements](./render-dom-elements.md)

View File

@ -0,0 +1,47 @@
tldr; Use `tag` from `ui/general/html.js` to quickly create DOM elements.
## Syntax
---
The general syntax is as follows:
```js
tag.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]);
```
**tag_name** can be any one of the following:
```
br, a, ol, ul, li, div, h1, h2, h3, h4, h5, h6,
p, strong, em, span, img, section, main, article, aside,
pre, button, time, input, textarea, label, form, progress, output, video
```
<br />
eg:
Here is an example HTML segment followed with the code to create it in Hydrogen.
```html
<section class="main-section">
<h1>Demo</h1>
<button class="btn_cool">Click me</button>
</section>
```
```js
tag.section({className: "main-section"},[
tag.h1("Demo"),
tag.button({className:"btn_cool"}, "Click me")
]);
```
<br />
**Note:** In views based on `TemplateView`, you will see `t` used instead of `tag`.
`t` is is `TemplateBuilder` object passed to the render function in `TemplateView`.
Although syntactically similar, they are not functionally equivalent.
Primarily `t` **supports** bindings and event handlers while `tag` **does not**.
```js
// The onClick here wont work!!
tag.button({className:"awesome-btn", onClick: () => this.foo()});
render(t, vm){
// The onClick works here.
t.button({className:"awesome-btn", onClick: () => this.foo()});
}
```

206
doc/UI/ui.md Normal file
View File

@ -0,0 +1,206 @@
## IView components
The [interface](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/types.ts) adopted by view components is agnostic of how they are rendered to the DOM. This has several benefits:
- it allows Hydrogen to not ship a [heavy view framework](https://bundlephobia.com/package/react-dom@18.2.0) that may or may not be used by its SDK users, and also keep bundle size of the app down.
- Given the interface is quite simple, is should be easy to integrate this interface into the render lifecycle of other frameworks.
- The main implementations used in Hydrogen are [`ListView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/ListView.ts) (rendering [`ObservableList`](https://github.com/vector-im/hydrogen-web/blob/master/src/observable/list/BaseObservableList.ts)s) and [`TemplateView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/TemplateView.ts) (templating and one-way databinding), each only a few 100 lines of code and tailored towards their specific use-case. They work straight with the DOM API and have no other dependencies.
- a common inteface allows us to mix and match between these different implementations (and gradually shift if need be in the future) with the code.
## Templates
### Template language
Templates use a mini-DSL language in pure javascript to express declarative templates. This is basically a very thin wrapper around `document.createElement`, `document.createTextNode`, `node.setAttribute` and `node.appendChild` to quickly create DOM trees. The general syntax is as follows:
```js
t.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]);
t.tag_name(child_element);
t.tag_name([child_elements]);
```
**tag_name** can be [most HTML or SVG tags](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/html.ts#L102-L110).
eg:
Here is an example HTML segment followed with the code to create it in Hydrogen.
```html
<section class="main-section">
<h1>Demo</h1>
<button class="btn_cool">Click me</button>
</section>
```
```js
t.section({className: "main-section"},[
t.h1("Demo"),
t.button({className:"btn_cool"}, "Click me")
]);
```
All these functions return DOM element nodes, e.g. the result of `document.createElement`.
### TemplateView
`TemplateView` builds on top of templating by adopting the IView component model and adding event handling attributes, sub views and one-way databinding.
In views based on `TemplateView`, you will see a render method with a `t` argument.
`t` is `TemplateBuilder` object passed to the render function in `TemplateView`. It also takes a data object to render and bind to, often called `vm`, short for view model from the MVVM pattern Hydrogen uses.
You either subclass `TemplateView` and override the `render` method:
```js
class MyView extends TemplateView {
render(t, vm) {
return t.div(...);
}
}
```
Or you pass a render function to `InlineTemplateView`:
```js
new InlineTemplateView(vm, (t, vm) => {
return t.div(...);
});
```
**Note:** the render function is only called once to build the initial DOM tree and setup bindings, etc ... Any subsequent updates to the DOM of a component happens through bindings.
#### Event handlers
Any attribute starting with `on` and having a function as a value will be attached as an event listener on the given node. The event handler will be removed during unmounting.
```js
t.button({onClick: evt => {
vm.doSomething(evt.target.value);
}}, "Click me");
```
#### Subviews
`t.view(instance)` will mount the sub view (can be any IView) and return its root node so it can be attached in the DOM tree.
All subviews will be unmounted when the parent view gets unmounted.
```js
t.div({className: "Container"}, t.view(new ChildView(vm.childViewModel)));
```
#### One-way data-binding
A binding couples a part of the DOM to a value on the view model. The view model emits an update when any of its properties change, to which the view can subscribe. When an update is received by the view, it will reevaluate all the bindings, and update the DOM accordingly.
A binding can appear in many places where a static value can usually be used in the template tree.
To create a binding, you pass a function that maps the view value to a static value.
##### Text binding
```js
t.p(["I've got ", vm => vm.counter, " beans"])
```
##### Attribute binding
```js
t.button({disabled: vm => vm.isBusy}, "Submit");
```
##### Class-name binding
```js
t.div({className: {
button: true,
active: vm => vm.isActive
}})
```
##### Subview binding
So far, all the bindings can only change node values within our tree, but don't change the structure of the DOM. A sub view binding allows you to conditionally add a subview based on the result of a binding function.
All sub view bindings return a DOM (element or comment) node and can be directly added to the DOM tree by including them in your template.
###### map
`t.mapView` allows you to choose a view based on the result of the binding function:
```js
t.mapView(vm => vm.count, count => {
return count > 5 ? new LargeView(count) : new SmallView(count);
});
```
Every time the first or binding function returns a different value, the second function is run to create a new view to replace the previous view.
You can also return `null` or `undefined` from the second function to indicate a view should not be rendered. In this case a comment node will be used as a placeholder.
There is also a `t.map` which will create a new template view (with the same value) and you directly provide a render function for it:
```js
t.map(vm => vm.shape, (shape, t, vm) => {
switch (shape) {
case "rect": return t.rect();
case "circle": return t.circle();
}
})
```
###### if
`t.ifView` will render the subview if the binding returns a truthy value:
```js
t.ifView(vm => vm.isActive, vm => new View(vm.someValue));
```
You equally have `t.if`, which creates a `TemplateView` and passes you the `TemplateBuilder`:
```js
t.if(vm => vm.isActive, (t, vm) => t.div("active!"));
```
##### Side-effects
Sometimes you want to imperatively modify your DOM tree based on the value of a binding.
`mapSideEffect` makes this easy to do:
```js
let node = t.div();
t.mapSideEffect(vm => vm.color, (color, oldColor) => node.style.background = color);
return node;
```
**Note:** you shouldn't add any bindings, subviews or event handlers from the side-effect callback,
the safest is to not use the `t` argument at all.
If you do, they will be added every time the callback is run and only cleaned up when the view is unmounted.
#### `tag` vs `t`
If you don't need a view component with data-binding, sub views and event handler attributes, the template language also is available in `ui/general/html.js` without any of these bells and whistles, exported as `tag`. As opposed to static templates with `tag`, you always use
`TemplateView` as an instance of a class, as there is some extra state to keep track (bindings, event handlers and subviews).
Although syntactically similar, `TemplateBuilder` and `tag` are not functionally equivalent.
Primarily `t` **supports** bindings and event handlers while `tag` **does not**. This is because to remove event listeners, we need to keep track of them, and thus we need to keep this state somewhere which
we can't do with a simple function call but we can insite the TemplateView class.
```js
// The onClick here wont work!!
tag.button({className:"awesome-btn", onClick: () => this.foo()});
class MyView extends TemplateView {
render(t, vm){
// The onClick works here.
t.button({className:"awesome-btn", onClick: () => this.foo()});
}
}
```
## ListView
A view component that renders and updates a list of sub views for every item in a `ObservableList`.
```js
const list = new ListView({
list: someObservableList
}, listValue => return new ChildView(listValue))
```
As items are added, removed, moved (change position) and updated, the DOM will be kept in sync.
There is also a `LazyListView` that only renders items in and around the current viewport, with the restriction that all items in the list must be rendered with the same height.
### Sub view updates
Unless the `parentProvidesUpdates` option in the constructor is set to `false`, the ListView will call the `update` method on the child `IView` component when it receives an update event for one of the items in the `ObservableList`.
This way, not every sub view has to have an individual listener on it's view model (a value from the observable list), and all updates go from the observable list to the list view, who then notifies the correct sub view.

90
doc/api.md Normal file
View File

@ -0,0 +1,90 @@
Session
properties:
rooms -> Rooms
# storage
Storage
key...() -> KeyRange
start...Txn() -> Transaction
Transaction
store(name) -> ObjectStore
finish()
rollback()
ObjectStore : QueryTarget
index(name)
Index : QueryTarget
Rooms: EventEmitter, Iterator<RoomSummary>
get(id) -> RoomSummary ?
InternalRoom: EventEmitter
applySync(roomResponse, membership, txn)
- this method updates the room summary
- persists the room summary
- persists room state & timeline with RoomPersister
- updates the OpenRoom if present
applyAndPersistSync(roomResponse, membership, txn) {
this._summary.applySync(roomResponse, membership);
this._summary.persist(txn);
this._roomPersister.persist(roomResponse, membership, txn);
if (this._openRoom) {
this._openRoom.applySync(roomResponse);
}
}
RoomPersister
RoomPersister (persists timeline and room state)
RoomSummary (persists room summary)
RoomSummary : EventEmitter
methods:
async open()
id
name
lastMessage
unreadCount
mentionCount
isEncrypted
isDirectMessage
membership
should this have a custom reducer for custom fields?
events
propChange(fieldName)
OpenRoom : EventEmitter
properties:
timeline
events:
RoomState: EventEmitter
[room_id, event_type, state_key] -> [sort_key, event]
Timeline: EventEmitter
// should have a cache of recently lookup sender members?
// can we disambiguate members like this?
methods:
lastEvents(amount)
firstEvents(amount)
eventsAfter(sortKey, amount)
eventsBefore(sortKey, amount)
events:
eventsApppended
RoomMembers : EventEmitter, Iterator
// no order, but need to be able to get all members somehow, needs to map to a ReactiveMap or something
events:
added(ids, values)
removed(ids, values)
changed(id, fieldName)
RoomMember: EventEmitter
properties:
id
name
powerLevel
membership
avatar
events:
propChange(fieldName)

47
doc/architecture.md Normal file
View File

@ -0,0 +1,47 @@
The matrix layer consists of a `Session`, which represents a logged in user session. It's the root object you can get rooms off. It can persist and load itself from storage, at which point it's ready to be displayed. It doesn't sync it's own though, and you need to create and start a Sync object for updates to be pushed and persisted to the session. `Sync` is the thing (although not the only thing) that mutates the `Session`, with `Session` being unaware of `Sync`.
The matrix layer assumes a transaction-based storage layer, modelled much to how IndexedDB works. The idea is that any logical operation like process sync response, send a message, ... runs completely in a transaction that gets aborted if anything goes wrong. This helps the storage to always be in a consistent state. For this reason you'll often see transactions (txn) being passed in the code. Also, the idea is to not emit any events until readwrite transactions have been committed.
- Reduce the chance that errors (in the event handlers) abort the transaction. You *could* catch & rethrow but it can get messy.
- Try to keep transactions as short-lived as possible, to not block other transactions.
For this reason a `Room` processes a sync response in two phases: `persistSync` & `emitSync`, with the return value of the former being passed into the latter to avoid double processing.
## Timeline, fragments & event indices.
A room in matrix is a DAG (directed, acyclic graph) of events, also known as the timeline. brawl is only aware of fragments of this graph, and can be unaware how these fragments relate to each other until a common event is found while paginating a fragment. After doing an initial sync, you start with one fragment. When looking up an event with the `/context` endpoint (for fetching a replied to message, or navigating to a given event id, e.g. through a permalink), a new, unconnected, fragment is created. Also, when receiving a limited sync response during incremental sync, a new fragment is created. Here, the relationship is clear, so they are immediately linked up at creation. Events in brawl are identified within a room by `[fragment_id, event_index]`. The `event_index` is an unique number within a fragment to sort events in chronological order in the timeline. `fragment_id` cannot be directly compared for sorting (as the relationship may be unknown), but with help of the `FragmentIndex`, one can attempt to sort events by their `FragmentIndex([fragment_id, event_index])`.
A fragment is the following data structure:
```
let fragment := {
roomId: string
id: number
previousId: number?
nextId: number?
previousToken: string?
nextToken: string?
}
```
## Observing the session
`Room`s on the `Session` are exposed as an `ObservableMap` collection, which is like an ordinary `Map` but emits events when it is modified (here when a room is added, removed, or the properties of a room change). `ObservableMap` can have different operators applied to it like `mapValues()`, `filterValues()` each returning a new `ObservableMap`-like object, and also `sortValues()` returning an `ObservableList` (emitting events when a room at an index is added, removed, moved or changes properties).
So for example, the room list, `Room` objects from `Session.rooms` are mapped to a `RoomTileViewModel` and then sorted. This gives us fine-grained events at the end of the collection chain that can be easily and efficiently rendered by the `ListView` component.
On that note, view components are just a simple convention, having these methods:
- `mount()` - prepare to become part of the document and interactive, ensure `root()` returns a valid DOM node.
- `root()` - the room DOM node for the component. Only valid to be called between `mount()` and `unmount()`.
- `update(attributes)` (to be renamed to `setAttributes(attributes)`) - update the attributes for this component. Not all components support all attributes to be updated. For example most components expect a viewModel, but if you want a component with a different view model, you'd just create a new one.
- `unmount()` - tear down after having been removed from the document.
The initial attributes are usually received by the constructor in the first argument. Other arguments are usually freeform, `ListView` accepting a closure to create a child component from a collection value.
Templating and one-way databinding are neccesary improvements, but not assumed by the component contract.
Updates from view models can come in two ways. View models emit a change event, that can be listened to from a view. This usually includes the name of the property that changed. This is the mechanism used to update the room name in the room header of the currently active room for example.
For view models part of an observable collection (and to be rendered by a ListView), updates can also propagate through the collection and delivered by the ListView to the view in question. This avoids every child component in a ListView having to attach a listener to it's viewModel. This is the mechanism to update the room name in a RoomTile in the room list for example.
TODO: specify how the collection based updates work. (not specified yet, we'd need a way to derive a key from a value to emit an update from within a collection, but haven't found a nice way of specifying that in an api)

58
doc/docker.md Normal file
View File

@ -0,0 +1,58 @@
## Warning
Usage of docker is a third-party contribution and not actively tested, used or supported by the main developer(s).
Having said that, you can also use Docker to create a local dev environment or a production deployment.
## Dev environment
In this repository, create a Docker image:
```
docker build -t hydrogen-dev -f Dockerfile-dev .
```
Then start up a container from that image:
```
docker run \
--name hydrogen-dev \
--publish 3000:3000 \
--volume "$PWD":/code \
--interactive \
--tty \
--rm \
hydrogen-dev
```
Then point your browser to `http://localhost:3000`. You can see the server logs in the terminal where you started the container.
To stop the container, simply hit `ctrl+c`.
## Production deployment
### Build or pull image
In this repository, create a Docker image:
```
docker build -t hydrogen .
```
Or, pull the docker image from GitLab:
```
docker pull registry.gitlab.com/jcgruenhage/hydrogen-web
docker tag registry.gitlab.com/jcgruenhage/hydrogen-web hydrogen
```
### Start container image
Then, start up a container from that image:
```
docker run \
--name hydrogen \
--publish 80:80 \
hydrogen
```

View File

@ -0,0 +1,2 @@
err.name: explanation
DataError: parameters to idb request where invalid

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,12 @@
we should automatically fill gaps (capped at a certain (large) amount of events, 5000?) after a limited sync for a room
## E2EE rooms
during these fills (once supported), we should calculate push actions and trigger notifications, as we would otherwise have received this through sync.
we could also trigger notifications when just backfilling on initial sync up to a certain amount of time in the past?
we also need to backfill if we didn't receive any m.room.message in a limited sync for an encrypted room, as it's possible the room summary hasn't seen the last message in the room and is now out of date. this is also true for a non-encrypted room actually, although wrt to the above, here notifications would work well though.
a room should request backfills in needsAfterSyncCompleted and do them in afterSyncCompleted.

View File

@ -0,0 +1,3 @@
use mock view models or even a mock session to render different states of the app in a static html document, where we can somehow easily tweak the css (just browser tools, or do something in the page?) how to persist css after changes?
Also dialogs, forms, ... could be shown on this page.

264
doc/impl-thoughts/E2EE.md Normal file
View File

@ -0,0 +1,264 @@
# Implementing e2e encryption:
## Olm
- implement MemberList as ObservableMap
- make sure we have all members (as we're using lazy loading members), and store these somehow
- keep in mind that the server might not support lazy loading? E.g. we should store in a memberlist all the membership events passed by sync, perhaps with a flag if we already attempted to fetch all. We could also check if the server announces lazy loading support in the version response (I think r0.6.0).
- do we need to update /members on every limited sync response or did we find a way around this?
- I don't think we need to ... we get all state events that were sent during the gap in `room.state`
- I tested this with riot and synapse, and indeed, we get membership events from the gap on a limited sync. This could be clearer in the spec though.
- fields:
- user id
- room id
- membership (invite, join, leave, ban)
- display name
- avatar url
- needs disambiguation in member list? (e.g. display name is not unique)
- device tracking status
- key [room id, user id] so we can easily get who is in a room by looking at [room id, min] -> [room id, max]
should the display name also be part of the key so the list is sorted by name? or have a sorting field of some sort
- index on:
- [user_id, room_id] to see which rooms a user is in, e.g. to recalculate trust on key changes
- [room id, display name] to determine disambiguation?
- for just e2ee without showing the list in the UI, we can do with only some of these things.
- implement creating/loading an olm account
- add libolm as dependency
- store pickled account
- load pickled account
- update pickled account
- add initial one-time keys
- publish keys with /keys/upload
- implement creating/loading an olm session for (userid, deviceid)
- get olm session for [(userid, deviceid), ...] (array so they can all go out in one /keys/claim call?)
- create if not exists
- claim one-time key with /keys/claim
- verify signature on key
- ??? what about inbound/outbound sessions? do they require multiple OlmSession objects?
- doesn't look like it, more like a way to start the session but once started (type=1), they are equivalent?
- for outbound, see https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide#starting-an-olm-session
- for inbound, see: https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide#handling-an-mroomencrypted-event
- so in this case, it would the session would be created as an outbound session.
- store pickled, index by curve25519 identity key?
- get from storage if exists and unpickle
- implement device tracking
- store users?
- needs_update (riot has status with 4 states: not tracked, pending download, downloading, up to date)
- set during sync for users that appear in device_lists.changed
- store devices
- id
- userid
- signing PK
- identity PK
- algorithms
- device name
- verified
- known? riot has this ... what does it mean exactly?
- handle device_lists.changed in /sync response
- call /keys/changed to get updated devices and store them
- handle device_lists.left in /sync response
- we can keep verified devices we don't share a room with anymore around perhaps, but
shouldn't update them ... which we won't do anyway as they won't appear in changed anymore
- when e2e is enabled, start tracking:
- call /keys/query for all members in MemberList
- verify signature on device keys
- store devices
- track which e2ee rooms a user is in? This so we don't need to load the member list when figuring out for which rooms a device changes has an effect. Maybe not yet needed here but we will need it to recalculate room trust. Perhaps we can also reuse the membership store if we have an index on (only) userid so we can ask with one query which rooms a user is in.
- implement maintaining one-time keys on server
- update account with new new keys when /sync responded with device_one_time_keys_count < MAX/2
- upload new one-time keys to /keys/upload
- mark them as published in account
- update picked session in storage
- implement encrypting olm messages
- roughly https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide#encrypting-an-event-with-olm
- packaging as m.room.encrypted event
- implement decrypting olm messages
- roughly https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide#handling-an-mroomencrypted-event
- decrypt with libolm
- verify signature
- check message index, etc to detect replay attacks
- handling wedged olm sessions
- ???
## Megolm
- ??? does every sender in a room have their own megolm session (to send)? I suppose so, yes
- we need to pickle inbound and outbound sessions separately ... are they different entities?
- they are: OutboundGroupSession and InboundGroupSession
- should they be in different stores?
- e.g. we have a store for outbound sessions (to send ourselves) and one for inbound
- NO! the e2e implementation guide says specifically:
"It should store these details as an inbound session, just as it would when receiving them via an m.room_key event."
- wait, we probably have to store the session as BOTH an inbound and outbound session?
- the outbound one so we can keep using it to encrypt
- the inbound one to be able to decrypt our own messages? as we won't send a m.room_key to our own device
- so yes, we'll store our own outbound sessions. Riot doesn't do this and just starts new ones when starting the client,
but keeping this would probably give us better offline support/less network usage as we wouldn't have to create new megolm session most of the time
- and we store the inbound sessions (including the ones derived from our own outbound sessions) to be able to decrypt all messages
- create new megolm session
- create new outbound group session
- get megolm session id and key, put in m.room_key event
- store megolm session
- encrypt using olm and send as m.room.encrypted device message
- receiving new megolm session
- listen for m.room_key device message
- decrypt using olm
- create inbound group session
- store megolm session
- encrypt megolm message
- decrypt megolm message
- rotate megolm session
- ??? does this happen automatically?
- deactive sessions when members leave the room
## SendQueue
we'll need to pass an implementation of EventSender or something to SendQueue that does the actual requests to send a message, one implementation for non-e2ee rooms (upload attachment, send event OR redact, ...) and one for e2ee rooms that send the olm keys, etc ... encrypts the message before sending, reusing as much logic as possible. this will entail multiple sendScheduler.request slots, as we should only do one request per slot, making sure if we'd restart that steps completed in sending are stored so we don't run them again (advancing olm key, ...) or they are safe to rerun. The `E2eeEventSender` or so would then also be the thing that has a dependency on the memberlist for device tracking, which keeps the dependency tree clean (e.g. no setMembers on a class that does both e2ee and non-e2ee). We would also need to be able to encrypt non-megolm events with Olm, like 4S gossiping, etc ...
## Verifying devices
- validate fingerprint
- have a look at SAS?
## Encrypted attachments
- use AES-CTR from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
## Notes
- libolm api docs (also for js api) would be great. Found something that could work:
https://gitlab.matrix.org/matrix-org/olm/-/blob/master/javascript/index.d.ts
## OO Design
e2ee/MemberList
// changes user tracking and returns changed members
// this probably needs to be run after updates to the rooms have been written
// to the txn so that if encryption was enabled in the same sync,
// or maybe not as we probably don't get device updates for a room we just joined/enabled encryption in.
async writeSync(txn)
emitSync(changes)
async addRoom(roomId, userIds, txn)
async addMember(roomId, userId, txn)
async removeMember(roomId, userId, txn)
async getMember(userId, txn)
// where would we use this? to encrypt?
// - does that need to be observable? well, at least updatable
// - to derive room trust from ... but how will this work with central emit point for room updates?
// check observablevalue trust before and after sync and detect change ourselves?
// set flag on room when observablevalue trust value emitted update and then reemit in emitSync?
// ALSO, we need to show trust for all rooms but don't want to have to load all EncryptionUsers and their devices for all e2ee rooms.
// can we build trust incrementally?
// trusted + new unverified device = untrusted
// trusted + some device got verified = ?? //needs a full recheck, but could be ok to do this after verification / cross signing by other party
// trusted + some device/user got unverified = untrusted (not supported yet, but should be possible)
// so sounds possible, but depends on how/if we can build decryption without needing all members
async openMembersForRoom(roomId) : ObservableMap<userId, EncryptionUser>`
// can we easily prevent redundancy between e2ee rooms that share the same member?
e2ee/EncryptionUser
get trackingStatus()
get roomIds()
// how to represent we only keep these in memory for e2ee rooms?
// for non-e2ee we would need to load them from storage, so needing an async method,
// but for e2ee we probably don't want to await a Promise.resolve for every member when encrypting, decrypting, ... ? or would it be that bad?
// should we index by sender key here and assume Device is only used for e2ee? Sounds reasonable ...
`get devices() : ObservableMap<senderKey, Device>`
would be nice if we could expose the devices of a member as an observable list on the member
at the same time, we need to know if any member needs updating devices before sending a message... but this state would actually be kept on the member, so that works.
we do need to aggregate all the trust in a room though for shields... how would trust be added to this?
```js
// do we need the map here?
const roomTrust = memberList.members.map(m => m.trust).reduce((minTrust, trust) => {
if (!minTrust || minTrust.compare(trust) < 0) {
return trust;
}
return minTrust;
});
```
e2ee/Device
// the session we should use to encrypt with, or null if none exists
get outboundSession()
// should this be on device or something more specific to crypto? Although Device is specific to crypto ...
createOutboundSession()
// gets the matching session, or creates one if needed/allowed
async getInboundSessionForMessage()
e2ee/olm/OutboundSession
encrypt(type, content, txn) (same txn should be used that will add the message to pendingEvents, here used to advance ratchet)
e2ee/olm/InboundSession
decrypt(payload, txn)
e2ee/olm/Account
// for everything in crypto, we should have a method to persist the intent
createOTKs(txn)
// ... an another one to upload it, persisting that we have in fact uploaded it
uploadOTKs(txn)
DeviceList
writeSync(txn)
emitSync()
queryPending()
actually, we need two member stores:
- (Member) one for members per room with userid, avatar url, display name, power level, ... (most recent message timestamp)?
- (EncryptionUser) one per unique member id for e2ee, with device tracking status, and e2ee rooms the member is in? If we duplicate this over every room the user is in, we complicate device tracking.
the e2ee rooms an EncryptionUser is in needs to be notified of device (tracking) changes to update its trust shield. The fact that the device list is outdated or that we will need to create a new olm session when sending a message should not emit an event.
requirements:
- Members need to be able to exists without EncryptionUser
- Members need to be able to map to an EncryptionUser (by userId)
- Member needs to have trust property derived from their EncryptionUser, with updates triggered somehow in central point, e.g. by Room.emitSync
- also, how far do we want to take this central update point thing? I guess any update that will cascade in a room (summary) update ... so here adding a device would cascade into the room trust changing, which we want to emit from Room.emitSync.
- hmm, I wonder if it makes sense to do this over member, or rather expose a second ObservableMap on the room for EncryptionUser where we can monitor trust
- PROs separate observablemap:
- don't need to load member list to display shields of user in timeline ... this might be fine though as e2ee rooms tend to be smaller rooms, and this is only for the room that is open.
- CONs separate observablemap:
- more clunky api, would need a join operator on ObservableMap to join the trust and Member into one ObservableMap to be able to display member list.
- See if it is doable to sync e2ee rooms without having all their encryptionUsers and devices in memory:
- Be able to decrypt *without* having all EncryptionUsers of a room and their devices in memory, but rather use indices on storage to load just what we need. Finding a matching inbound olm session is something we need to think how to do best. We'll need to see this one.
- Be able to encrypt may require having all EncryptionUsers of a room and their devices in memory, as we typically only send in the room we are looking at (but not always, so forwarding an event, etc... might then require to "load" some of the machinery for that room, but that should be fine)
- Be able to send EncryptionUser updates *without* having all EncryptionUsers and their devices in memory
other:
- when populating EncryptionUsers for an e2ee room, we'll also populate Members as we need to fetch them from the server anyway.
- Members can also be populated for other reasons (showing member list in non-e2ee room)
we should adjust the session store to become a key value store rather than just a single value, we can split up in:
- syncData (filterId, syncToken, syncCount)
- serverConfig (/versions response)
- serialized olm Account
so we don't have write all of that on every sync to update the sync token
new stores:
room-members
e2ee-users
e2ee-devices
inbound-olm-sessions
outbound-olm-sessions
//tdb:
inbound-megolm-sessions
outbound-megolm-sessions
we should create constants with sets of store names that are needed for certain use cases, like write timeline will require [timeline, fragments, inbound-megolm-sessions] which we can reuse when filling the gap, writing timeline sync, ...
room summary should gain a field tracking if members have been loaded or not?
main things to figure out:
- how to decrypt? what indices do we need? is it reasonable to do this without having all EncryptionUser/devices in memory?
- big part of this is how we can find the matching olm session for an incoming event/create a new olm session
- can we mark some olm sessions as "spent" once they are too old/have used max messages? or do we need to look into all past olm sessions for a senderKey/device?

View File

@ -0,0 +1,88 @@
- DONE: write FragmentIndex
- DONE: adapt SortKey ... naming! :
- FragmentIdIndex (index as in db index)
- compare(idA, idB)
- SortKey
- FragmentId
- EventIndex
- DONE: write fragmentStore
- load all fragments
- add a fragment (live on limited sync, or /context)
- connect two fragments
- update token on fragment (when filling gap or connecting two fragments)
fragments can need connecting when filling a gap or creating a new /context fragment
- DONE: adapt timelineStore
how will fragments be exposed in timeline store?
- all read operations are passed a fragment id
- adapt persister
- DONE: persist fragments in /sync
- DONE: fill gaps / fragment filling
- DONE: load n items before and after key,
- DONE: need to add fragments as we come across boundaries
- DONE: also cache fragments? not for now ...
- DONE: not doing any of the above, just reloading and rebuilding for now
- DONE: adapt Timeline
- DONE: turn ObservableArray into ObservableSortedArray
- upsert already sorted sections
- DONE: upsert single entry
- DONE: adapt TilesCollection & Tile to entry changes
- add live fragment id optimization if we haven't done so already
- lets try to not have to have the fragmentindex in memory if the timeline isn't loaded
- could do this by only loading all fragments into index when filling gaps, backpaginating, ... and on persister load only load the last fragment. This wouldn't even need a FragmentIndex?
# Leftover items
implement SortedArray::setManySorted in a performant manner
implement FragmentIdComparator::add in a performant manner
there is some duplication (also in memory) between SortedArray and TilesCollection. Both keep a sorted list based on fragmentId/eventIndex... TilesCollection doesn't use the index in the event handlers at all. we could allow timeline to export a structure that just emits "these entries are a thing (now)" and not have to go through sorting twice. Timeline would have to keep track of the earliest key so it can use it in loadAtTop, but that should be easy. Hmmm. also, Timeline might want to be in charge of unloading parts of the loaded timeline, and for that it would need to know the order of entries. So maybe not ... we'll see.
check: do /sync events not have a room_id and /messages do???
so a gap is two connected fragments where either the first fragment has a nextToken and/or the second fragment has a previousToken. It can be both, so we can have a gap where you can fill in from the top, from the bottom (like when limited sync) or both.
also, filling gaps and storing /context, how do we find the fragment we could potentially merge with to look for overlapping events?
with /sync this is all fine and dandy, but with /context is there a way where we don't need to look up every event_id in the store to see if it's already there?
we can do a anyOf(event_id) on timelineStore.index("by_index") by sorting the event ids according to IndexedDb.cmp and passing the next value to cursor.continue(nextId).
so we'll need to remove previous/nextEvent on the timeline store and come up with a method to find the first matched event in a list of eventIds.
so we'll need to map all event ids to an event and return the first one that is not null. If we haven't read all events but we know that all the previous ones are null, then we can already return the result.
we can call this findFirstEventIn(roomId, [event ids])
thoughts:
- ranges in timeline store with fragmentId might not make sense anymore as doing queries over multiple fragment ids doesn't make sense anymore ... still makes sense to have them part of SortKey though ...
- we need a test for querytarget::lookup, or make sure it works well ...
# Reading the timeline with fragments
- what format does the persister return newEntries after persisting sync or a gap fill?
- a new fragment can be created during a limited sync
- when doing a /context or /messages call, we could have joined with another fragment
- don't think we need to describe a result spanning multiple fragments here
so:
in case of limited sync, we just say there was a limited sync, this is the fragment that was created for it so we can show a gap in the timeline
in case of a gap fill, we need to return what was changed to the fragment (was it joined with another fragment, what's the new token), and which events were actually added.
we return entries! fragmentboundaryentry(start or end) or evententry. so looks much like the gaps we had before, but now they are not stored in the timeline store, but based on fragments.
- where do we translate from fragments to gap entries? and back? in the timeline object?
that would make sense, that seems to be the only place we need that translation
# SortKey
so, it feels simpler to store fragmentId and eventIndex as fields on the entry instead of an array/arraybuffer in the field sortKey. Currently, the tiles code somewhat relies on having sortKeys but nothing too hard to change.
so, what we could do:
- we create EventKey(fragmentId, eventIndex) that has the nextKey methods.
- we create a class EventEntry that wraps what is stored in the timeline store. This has a reference to the fragmentindex and has an opaque compare method. Tiles delegate to this method. EventEntry could later on also contain methods like MatrixEvent has in the riot js-sdk, e.g. something to safely dig into the event object.

View File

@ -0,0 +1,16 @@
# Local echo
## Remote vs local state for account_data, etc ...
For things like account data, and other requests that might fail, we could persist what we are sending next to the last remote version we have (with a flag for which one is remote and local, part of the key). E.g. for account data the key would be: [type, localOrRemoteFlag]
localOrRemoteFlag would be 1 of 3:
- Remote
- (Local)Unsent
- (Local)Sent
although we only want 1 remote and 1 local value for a given key, perhaps a second field where localOrRemoteFlag is a boolean, and a sent=boolean field as well? We need this to know if we need to retry.
This will allow resending of these requests if needed. Once the request goes through, we remove the local version.
then we can also see what the current value is with or without the pending local changes, and we don't have to wait for remote echo...

View File

@ -0,0 +1,11 @@
LoginView
LoginViewModel
SessionPickerView
SessionPickerViewModel
matrix:
SessionStorage (could be in keychain, ... for now we go with localstorage)
getAll()
Login

View File

@ -0,0 +1,136 @@
# TODO
## Member list
- support migrations in StorageFactory
- migrate all stores from key to key_path
- how to deal with members coming from backfill? do we even need to store them?
# How to store members?
All of this is assuming we'll use lazy loading of members.
Things we need to persist per member
- (user id)
- (room id)
- avatar url
- display name
## Historical members
store name: historical_members
### Without include_redundant_members
To show the correct (historical) display name and avatar next to a message, we need to manage historical members. If we don't set include_redundant_members as a filter, we'll need to track members per fragment, backwards and forwards looking, so we can find out which member to use for redundant members not included in the /messages response. E.g., if we have this timeline:
```
] gap #1 ]
[ messages ]
[ gap #2 ]
[ live messages ]
```
When expanding gap #1, we store the members. After that, we expand gap #2.
How do we know if the members already present are from gap #1 (and thus we need to replace them as gap #2 is later)
or live member changes at the bottom of the timeline (not to be overwritten as gap #2 comes before it).
We can store the fragmentId with a member? This is not enough, as somebody can change their display name in the middle of a fragment. We'd need a forward and backward pointing member per fragment.
```
room_id
fragment_id
forward
avatar_url
display_name
```
It would be good not to duplicate events too much.
We just need these for the extremities though...
I'm assuming `/context` will contain all members, as chronological relation with other chunks can't be assumed by clients? Looking at the riot code, this indeed seems to be the case.
#### Avoiding duplication of members
Ideally, historical_members would fall back on members to not duplicate all the member events. So the forward members for the sync fragment would be taking from the `members` store. Anytime we have a limited sync, this would be put in the backwards look members for the new sync fragment. If we then backpaginate, we can get our members from there. But we also don't want to duplicate the members between forward and backward looking members per fragment, so if they are the same, we only store it as forward. If the fragment is live, it comes from the `members` store.
This would mean that for fragments in the sync island:
- for members that change within a fragment
- we store the old member as backwards member,
- we store the new member as forward if not live fragment or if live in `members`
- when the live fragment changes, the old fragment forward looking members still need to point to the `members`? but those will change over time ... so need to be duplicated at the time of limited sync response? hmmmm
### With include_redundant_members
More data in response
we just set `include_redundant_members` and `/messages` and `/context` contain all their own members, which can be written to the event, and we track a partial member list from /sync, that can later be completed with /joined_members. This is *a lot* simpler.
If we go for this, we might want to think of a migration step to remove include_redundant_members? Well, maybe not before 1.0
IMPORTANT: I'm not sure that with `include_redundant_members` all the member state events will be included in the sync response, we need to test this.
## Member list
store name: members
We need to be able to get a list of all most recent members, and are not interested in historical members. We need it for:
- tracking devices for sending e2ee messages
- showing the member list
- member auto-completion from the composer
Once we decide to start tracking all members (when any of the above cases is triggered for the first time), we load all members with `/members?at=`, and keep updating it with the state and timeline state events of incoming /sync responses. Any member already stored should be replaced. We should have an index on roomId, and on [roomId, userId].
We need historical members (only) for the timeline, so either:
- we store the avatar url and display name on each event
- we need to store all versions of a member (and keep an in-memory cache to not have to read from yet another store while loading the timeline)
## General room state
We won't store `m.room.members` as room state. Any other state events should be stored in a separate store indexed by [roomId, eventType, stateKey].
----
Note that with lazy loading, we don't need all members to show the timeline, as the relevant state is passed in /sync and /messages (not true without include_redundant_members?). This state can be persisted in the members table, and we'll need a flag in room summary whether *all* members have been loaded. We'd insert in two ways:
- appending timeline, replace any members already there
- prepending timeline, don't touch members already there...
this won't work with multiple gaps though, if we have this timeline:
] gap #1 ]
[ messages ]
[ gap #2 ]
[ live messages ]
when expanding gap #1, we store the members. After that, we expand gap #2.
How do we know if the members already present are from gap #1 (and thus we need to replace them as gap #2 is later)
or live member changes at the bottom of the timeline (not to be overwritten as gap #2 comes before it).
We can store the fragmentId with a member? This is not enough, as somebody can change their display name in the middle of a fragment. We'd need a forward and backward pointing member per fragment.
We still have a problem with /context/{eventId} (permalinks) then, but could not store members in this case? As we would store the avatar and display name on the event anyway, we would only have less members in the store when filling permalinks, but if we need all members, we
Should we just bite the bullet and store historical members
# How to track members to add to incoming events
## for /sync
- have most-recently-used cache of *n* members per room
- cache takes members from ... ? persisted members? how do we get most recent members?
## for /messages
- everything will be in the response itself (is that also true without include_redundant_members?)
without include_redundant_members it does look like some members for which events are being returned
will not be included. So when back-paginating, we can take any member we know of with the same fragmentId, or one
that comes after (so we would need to load all membership events for a given userid, and filter them in memory using the fragmentidcomparer)
## for /context/{eventId}
- everything will be in the response itself (is that also true without include_redundant_members?)
I'm guessing include_redundant_members doesn't apply to /context because the client doesn't know
whether it comes before or after some part of the timeline it previously fetched.

View File

@ -0,0 +1,25 @@
# Replying to pending messages
The matrix spec requires clients capable of rich replies (that would be us once replies work) to include fallback (textual in `body` and structured in `formatted_body`) that can be rendered
by clients that do not natively support rich replies (that would be us at the time of writing). The schema for the fallback is as follows:
```
<mx-reply>
<blockquote>
<a href="https://matrix.to/#/!somewhere:example.org/$event:example.org">In reply to</a>
<a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
<br />
<!-- This is where the related event's HTML would be. -->
</blockquote>
</mx-reply>
```
There's a single complication here for pending events: we have `$event:example.org` in the schema (the `In reply to` link), and it must
be present _within the content_, inside `formatted_body`. The issue is that, if we are queuing a reply to a pending event,
we don't know its remote ID. All we know is its transaction ID on our end. If we were to use that while formatting the message,
we'd be sending messages that contain our internal transaction IDs instead of proper matrix event identifiers.
To solve this, we'd need `SendQueue`, whenever it receives a remote echo, to update pending events that are replies with their
`relatedEventId`. This already happens, and the `event_id` field in `m.relates_to` is updated. But we'd need to extend this
to adjust the messages' `formatted_body` with the resolved remote ID, too.
How do we safely do this, without accidentally substituting event IDs into places in the body where they were not intended?

22
doc/impl-thoughts/PUSH.md Normal file
View File

@ -0,0 +1,22 @@
# Push Notifications
- we setup the app on the sygnal server, with an app_id (io.element.hydrogen.web), generating a key pair
- we create a web push subscription, passing the server pub key, and get `endpoint`, `p256dh` and `auth` back. We put `webpush_endpoint` and `auth` in the push data, and use `p256dh` as the push key?
- we call `POST /_matrix/client/r0/pushers/set` on the homeserver with the sygnal instance url. We pass the web push subscription as pusher data.
- the homeserver wants to send out a notification, calling sygnal on `POST /_matrix/push/v1/notify` with for each device the pusher data.
- we encrypt and send with the data in the data for each device in the notification
- this wakes up the service worker
- now we need to find which local session id this notification is for
## Testing/development
- set up local synapse
- set up local sygnal
- write pushkin
- configure "hydrogen" app in sygnal config with a webpush pushkin
- start writing service worker code in hydrogen (we'll need to enable it for local dev)
- try to get a notification through
## Questions
- do we use the `event_id_only` format?
- for e2ee rooms, are we fine with just showing "Bob sent you a message (in room if not DM)", or do we want to sync and show the actual message? perhaps former can be MVP.

View File

@ -0,0 +1,5 @@
# Read receipts
## UI
For the expanding avatars, trimmed at 5 or so, we could use css grid and switch from the right most cell to a cell that covers the whole width when clicking.

View File

@ -0,0 +1,83 @@
# Reconnecting
`HomeServerApi` notifies `Reconnector` of network call failure
`Reconnector` listens for online/offline event
`Reconnector` polls `/versions` with a `RetryDelay` (implemented as ExponentialRetryDelay, also used by SendScheduler if no retry_after_ms is given)
`Reconnector` emits an event when sync and message sending should retry
`Sync` listen to `Reconnector`
`Sync` notifies when the catchup sync has happened
`Reconnector` has state:
- disconnected (and retrying at x seconds from timestamp)
- reconnecting (call /versions, and if successful /sync)
- connected
`Reconnector` has a method to try to connect now
`SessionStatus` can be:
- disconnected (and retrying at x seconds from timestamp)
- reconnecting
- connected (and syncing)
- doing catchup sync
- sending x / y messages
rooms should report how many messages they have queued up, and each time they sent one?
`SendReporter` (passed from `Session` to `Room`, passed down to `SendQueue`), with:
- setPendingEventCount(roomId, count). This should probably use the generic Room updating mechanism, e.g. a pendingMessageCount on Room that is updated. Then session listens for this in `_roomUpdateCallback`.
`Session` listens to `Reconnector` to update it's status, but perhaps we wait to send messages until catchup sync is done
# TODO
- DONE: finish (Base)ObservableValue
- put in own file
- add waitFor (won't this leak if the promise never resolves?)
- decide whether we want to inherit (no?)
- DONE: cleanup Reconnector with recent changes, move generic code, make imports work
- DONE: add SyncStatus as ObservableValue of enum in Sync
- DONE: cleanup SessionContainer
- DONE: move all imports to non-default
- DONE: remove #ifdef
- DONE: move EventEmitter to utils
- DONE: move all lower-cased files
- DONE: change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing
- DONE: adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer
- DONE: show load progress in LoginView/SessionPickView and do away with loading screen
- DONE: rename SessionsStore to SessionInfoStorage
- make sure we've renamed all \*State enums and fields to \*Status
- add pendingMessageCount prop to SendQueue and Room, aggregate this in Session
- DONE: add completedFirstSync to Sync, so we can check if the catchup or initial sync is still in progress
- DONE: update SyncStatusViewModel to use reconnector.connectionStatus, sync.completedFirstSync, session.syncToken (is initial sync?) and session.pendingMessageCount to show these messages:
- DONE: disconnected, retrying in x seconds. [try now].
- DONE: reconnecting...
- DONE: doing catchup sync
- syncing, sending x messages
- DONE: syncing
perhaps we will want to put this as an ObservableValue on the SessionContainer ?
NO: When connected, syncing and not sending anything, just hide the thing for now? although when you send messages it will just pop in and out all the time.
- see if it makes sense for SendScheduler to use the same RetryDelay as Reconnector
- DONE: finally adjust all file names to their class names? e.g. camel case
- see if we want more dependency injection
- for classes from outside sdk
- for internal sdk classes? probably not yet
thought: do we want to retry a request a couple of times when we can't reach the server before handing it over to the reconnector? Not that some requests may succeed while others may fail, like when matrix.org is really slow, some requests may timeout and others may not. Although starting a service like sync while it is still succeeding should be mostly fine. Perhaps we can pass a canRetry flag to the HomeServerApi that if we get a ConnectionError, we will retry. Only when the flag is not set, we'd call the Reconnector. The downside of this is that if 2 parts are doing requests, 1 retries and 1 does not, and the both requests fail, the other part of the code would still be retrying when the reconnector already kicked in. The HomeServerApi should perhaps tell the retryer if it should give up if a non-retrying request already caused the reconnector to kick in?
CatchupSync should also use timeout 0, in case there is nothing to report we spend 30s with a catchup spinner. Riot-web sync also says something about using a 0 timeout until there are no more to_device messages as they are queued up by the server and not all returned at once if there are a lot? This is needed for crypto to be aware of all to_device messages.
We should have a persisted observable value on Sync `syncCount` that just increments with every sync. This way would have other parts of the app, like account data, observe this and take action if something hasn't synced down within a number of syncs. E.g. account data could assume local changes that got sent to the server got subsequently overwritten by another client if the remote echo didn't arrive within 5 syncs, and we could attempt conflict resolution or give up. We could also show a warning that there is a problem with the server if our own messages don't come down the server in x syncs. We'd need to store the current syncCount with pieces of pending data like account data and pendingEvents.
Are overflows of this number a problem to take into account? Don't think so, because Number.MAX_SAFE_INTEGER is 9007199254740991, so if you sync on average once a second (which you won't, as you're offline often) it would take Number.MAX_SAFE_INTEGER/(3600*24*365) = 285616414.72415626 years to overflow.

View File

@ -0,0 +1,269 @@
Relations and redactions
events that refer to another event will need support in the SyncWriter, Timeline and SendQueue I think.
SyncWriter will need to resolve the related remote id to a [fragmentId, eventIndex] and persist that on the event that relates to some other. Same for SendQueue? If unknown remote id, not much to do. However, once the remote id comes in, how do we handle it correctly? We might need a index on m.relates_to/event_id? I'd rather avoid that if possible, as that becomes useless once we have the target event of the relationship (we store the relations on the target event (see "One fetch" below) and have the target event id on the relation so can go both ways). I'm not sure this index will be completely useless actually. For edits, we'll want to be able to list all edits. For reactions, we'll want to fetch the authors and timestamps. For replies, we want to render the origin event and not use the fallback text? It is true though that only a minority of the events will have a related_to event id, so I wonder if it is faster to put it in a different store? Perhaps a prototype can clarify ...
`event_relations` store could be this:
{
sourceEventId:
targetEventId:
rel_type:
roomId:
}
`{"key": "!bEWtlqtDwCLFIAKAcv:matrix.org|$apmyieZOI5vm4DzjEFzjbRiZW9oeQQR21adM6A6eRwM|m.annotation|m.reaction|$jSisozR3is5XUuDZXD5cyaVMOQ5_BtFS3jKfcP89MOM"}`
or actually stored like `roomId|targetEventId|rel_type|sourceEventId`. How can we get the last edit? They are sorted by origin_server_ts IIRC? Should this be part of the key? Solved: we store the event id of a replacement on the target event
We should look into what part of the relationships will be present on the event once it is received from the server (e.g. m.replace might be evident, but not all the reaction events?). If not, we could add a object store with missing relation targets.
The timeline can take incoming events from both the SendQueue and SyncWriter, and see if their related to fragmentId/eventIndex is in view, and then update it?
alternatively, SyncWriter/SendQueue could have a section with updatedEntries apart from newEntries?
SendQueue will need to pass the non-sent state (redactions & relations) about an event that has it's remote echo received to the SyncWriter so it doesn't flash while redactions and relations for it still have to be synced.
Also, related ids should be processed recursively. If event 3 is a redaction of event 2, a reaction to event 1, all 3 entries should be considered as updated.
As a UI for reactions, we could show (👍 14 + 1) where the + 1 is our own local echo (perhaps style it pulsating and/or in grey?). Clicking it again would just show 14 and when the remote echo comes in it would turn into 15.
## One fetch for timeline reading
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
For replies (or references in general?), we do need to load the referred-to event in a second read. For reactions and edits, they will already be stored on the target event.
## Example events from the wild
### Reaction
```json
{
"content": {
"m.relates_to": {
"event_id": "$apmyieZOI5vm4DzjEFzjbRiZW9oeQQR21adM6A6eRwM",
"key": "👍️",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1621284357314,
"sender": "@charly:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 64140856
},
"event_id": "$jSisozR3is5XUuDZXD5cyaVMOQ5_BtFS3jKfcP89MOM",
"room_id": "!bEWtlqtDwCLFIAKAcv:matrix.org"
}
```
### Edit
```json
{
"content": {
"body": " * ...",
"m.new_content": {
"body": "...",
"msgtype": "m.text"
},
"m.relates_to": {
"event_id": "$OXL0yk18y-VG3DuTybVh9j9cvdjjnnzWbBKY-QPXJ-0",
"rel_type": "m.replace"
},
"msgtype": "m.text"
},
"origin_server_ts": 1621264902371,
"room_id": "!bEWtlqtDwCLFIAKAcv:matrix.org",
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 83636544
},
"event_id": "$Z7sFSKWtLTFoMMabkPFe0PSKWpkakjWUkYQeBU8IHVc",
"user_id": "@alice:matrix.org",
"age": 83636544
}
```
### Reply
```json
{
"content": {
"body": "...",
"format": "org.matrix.custom.html",
"formatted_body": "...",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$rGD9iQ93UmopkkagJ0tW_FHATa8IrvABg9cM_tNUvu4"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1621242338597,
"room_id": "!bEWtlqtDwCLFIAKAcv:matrix.org",
"sender": "@bob:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 106408661,
"m.relations": {
"m.annotation": {
"chunk": [
{
"type": "m.reaction",
"key": "👍️",
"count": 1
}
]
}
}
},
"event_id": "$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_q4",
"user_id": "@bob:matrix.org",
"age": 106408661
}
```
### Remaining spec issues
- m.in_reply_to vs rel_type
- reactions in unsigned can't be deduplicated
- how to sort edits? for now we went with origin_server_ts
- do we say anything about events of a different type replacing an event?
- do we specify that replies should be to the original event, not the edit?
## What to store denormalized on the event itself?
```json
{
"reactions": {
"👍": {"count": 3, "me": true, "firstTimestamp": 2323989},
"👋": {"count": 1, "me": false, "firstTimestamp": 2323989}
},
"replacingEvent": {
"event_id": "$abc",
"origin_server_ts": ?,
"content": {}
}
}
```
we only need the m.new_content and event id of the replacing event, even timestamp we can load the event for on hover?
store the replacing event along the original event because we need to keep the original event along somewhere, but for displaying purposes, we'd use the content of the replacingEvent. Should we just store the content of the replacing event? Or even just the `m.new_content`? Could make sense, but perhaps also store the new timestamp along. How about whem somebody than the sender edits?
# Aggregation
what do we do with the aggregated timestamps? do we store them? if so, where?
when we hover reactions, we want to show the authors, rather than the timestamp, so we'll need to call /relations for that anyway. so no need to store the timestamp?
`/relations` is in fact a bit the server-side version of our `event_relations` store
## Dealing with gappy syncs
Doesn't look like synapse currently tells us which target events have outdates relations after a gappy sync. MSC 2675 proposes `stale_events`, but inspecting network traffic, that doesn't seem to be implemented right now.
So, if we locally would need to determine if relations are outdated, we could look if any of the fragments between an event and the last synced event have pagination tokens. Although we have no way to clear this "flag" if we were to fetch the relations after this.
As an initial cut it is probably fine if reactions and edits are outdated unless you scroll up all the way to an event (and hence back-fill), as this is what we'll always do (apart from permalinks).
### Permalinks
So once we do support permalinks, how do we solve this? Element solves this by not storing `/context` and the `/messages` requests around, hence it is always fresh.
We could store the live fragment id in events when we refresh their `/relations`, and if it is not the current live fragment id, you're outdated.
To accurately display anything not in the live fragment, we either need to:
- backfill until there are no more gaps between the event fragment and the live fragment.
- -- there is no way to know how many events this would load.
- ++ that we know which gaps we've already filled
- ++ we need to do this for e2ee rooms anyway
- ++ we need to implement this anyway for non-gappy sync
- ++ we can only do this as an initial cut, especially as we don't support permalinks yet
- Refetch the `/context` and `/messages` for what is on the screen and reconcile.
- ++ we know how much we'll fetch
- -- we need to fetch everything again if we have one small gap
- we store the current live fragment when doing this, so can know:
- if we need to refetch / if there is a gap
- how many gaps we need to fill
- could we fall back to this strategy if the first one takes too long/many events?
- we could pick a heuristic to pick either strategy (like time between syncs or try for x events and if the gap is not closed, give up)?
- Refetch /aggregations for every event
- ++ we don't get the events (we dont need? edits?)
- --- need to do it for every event
- use `stale_events` if we actually implement it one day
- this can work well with the first strategy, we'd store a "relationsStale" flag on the event, and refetch /relations immediately or if scrolled into view.
# API
## Reactions
```js
const reaction = eventEntry.react("👍");
room.sendEvent("m.reaction", reaction);
```
```js
// this is an ObservableMap mapping the key to the count (or rather SortedArray?)
// probably fine to just use a SortedArray to sorts by count, then key
// actually, maybe better to do ObservableMap and store first timestamp so we can support https://github.com/vector-im/element-web/issues/9698 outside of SDK.
const reactions = eventEntry.reactions.sortValues((r1, r2) => r1.count - r2.count);
new ListView({list: reactions}, reaction => new ReactionView(reaction, room));
// reaction has:
reaction.key
reaction.hasMyReaction // how do we get this from the bundled events?
reaction.count
reaction.firstTimestamp
room.sendEvent("m.reaction", reaction.react());
// this won't work as we don't have the event id:
// room.sendRedaction(reaction.redact());
```
## Edits
```js
const replacement = eventEntry.replace({});
room.sendEvent(eventEntry.eventType, replacement);
```
## Replies
```js
const reply = eventEntry.reply({});
room.sendEvent("m.room.message", reply);
```
## Redactions
```js
const redaction = eventEntry.redact();
room.sendRedaction(redaction);
```
All off these reaction and edit entries should probably not be live, and at some point in the future if we need them to be live for some use case, we can add an additional api to make them live with an explicit release mechanism?
```js
// there is no api to get the reactions by sender though, so perhaps we need to load them all and then find our own?
const reactions = await eventEntry.getReactionEntries("👍");
const reaction = reactions.find(r => r.sender = ownUserId);
room.sendRedaction(reaction.redact());
```
```js
const edits = await eventEntry.getEdits();
room.sendRedaction(edits[1].redact());
```
```js
const lastEdit = await eventEntry.getLastEdit();
room.sendRedaction(lastEdit.redact());
```

View File

@ -0,0 +1,17 @@
If we were to render replies in a smart way (instead of relying on the fallback), we would
need to manually find entries that are pointed to be `in_reply_to`. Consulting the timeline
code, it seems appropriate to add a `_replyingTo` field to a `BaseEventEntry` (much like we
have `_pendingAnnotations` and `pendingRedactions`). We can then:
* use `TilesCollection`'s `_findTileIdx` to find the tile of the message being replied to,
and put a reference to its tile into the new tile being created (?).
* It doesn't seem appropriate to add an additional argument to TileCreator, but we may
want to re-use tiles instead of creating duplicate ones. Otherwise, of course, `tileCreator`
can create more than one tile from an entry's `_replyingTo` field.
* Resolve `_replyingTo` much like we resolve `redactingEntry` in timeline: search by `relatedTxnId`
and `relatedEventId` if our entry is a reply (we can add an `isReply` flag there).
* This works fine for local entries, which are loaded via an `AsyncMappedList`, but what
about remote entries? They are not loaded asynchronously, and the fact that they are
not a derived collection is used throughout `Timeline`.
* Entries that don't have replies that are loadeded (but that are replies) probably need
to be tracked somehow?
* Then, on timeline add, check new IDs and update corresponding entries

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