mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-11 01:40:42 +00:00
Compare commits
177 Commits
t3chguy/fi
...
t3chguy/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99553210be | ||
|
|
668450325c | ||
|
|
b4aa375619 | ||
|
|
be92e64458 | ||
|
|
4d0c740ad2 | ||
|
|
438d07bc75 | ||
|
|
b4d343053d | ||
|
|
88d507f4f9 | ||
|
|
4ffc77dfef | ||
|
|
e4d138f4d1 | ||
|
|
746c20bdba | ||
|
|
cd2bd81268 | ||
|
|
561868f12a | ||
|
|
d1f0cbea27 | ||
|
|
e80ab738ce | ||
|
|
d60768daec | ||
|
|
a7ace95eac | ||
|
|
cdb65233b0 | ||
|
|
83186c0403 | ||
|
|
8776371ed4 | ||
|
|
5f9637b14e | ||
|
|
c924f59f41 | ||
|
|
c6134b1849 | ||
|
|
818ec7529c | ||
|
|
7c7cbe655a | ||
|
|
d379103352 | ||
|
|
844a318eb2 | ||
|
|
205f0f9f30 | ||
|
|
703ad190f8 | ||
|
|
1237fbf74c | ||
|
|
137b20b784 | ||
|
|
5de07e091f | ||
|
|
f3237fb0b0 | ||
|
|
e41b476be3 | ||
|
|
4506988ad6 | ||
|
|
734e698c7e | ||
|
|
ba9f962570 | ||
|
|
1dca6b60c3 | ||
|
|
4e47b75f0f | ||
|
|
55634304b0 | ||
|
|
eb2ff67e7b | ||
|
|
c5d04b3649 | ||
|
|
0845c515c4 | ||
|
|
76416ec7b9 | ||
|
|
e3aee58a6d | ||
|
|
ea158ed63f | ||
|
|
38c0bf3b62 | ||
|
|
6821a35444 | ||
|
|
9f9cd6ff08 | ||
|
|
f8b3be682a | ||
|
|
ab5ff85bff | ||
|
|
aa9851dc4e | ||
|
|
437b5241b9 | ||
|
|
42943f3ffc | ||
|
|
2fd4e474cb | ||
|
|
cf1eddf0ea | ||
|
|
cee4a754d7 | ||
|
|
72354161e2 | ||
|
|
fa1d2c7dfb | ||
|
|
42c0c19556 | ||
|
|
d3a0925e42 | ||
|
|
351a31c9ee | ||
|
|
2614cab64b | ||
|
|
15db387425 | ||
|
|
53cc1c78b1 | ||
|
|
be6528de26 | ||
|
|
087f1bc948 | ||
|
|
4360f5a63e | ||
|
|
65a87f1a53 | ||
|
|
b0c04834b6 | ||
|
|
b61d5b4475 | ||
|
|
6c3817fa34 | ||
|
|
e151d9ab53 | ||
|
|
f15b188297 | ||
|
|
ea16928d35 | ||
|
|
4651bf4eb4 | ||
|
|
5442d4050d | ||
|
|
695cc1b28b | ||
|
|
c94e3f00ed | ||
|
|
79e0661be3 | ||
|
|
6cf0809064 | ||
|
|
a0eb94704e | ||
|
|
e1e0d37478 | ||
|
|
9039e70990 | ||
|
|
596f297094 | ||
|
|
4f92927a3d | ||
|
|
b77218e65a | ||
|
|
c21e9f6383 | ||
|
|
4f8a8a2543 | ||
|
|
d615e69a86 | ||
|
|
db1d440e3b | ||
|
|
24fafe2588 | ||
|
|
1e06e3083f | ||
|
|
8e9f3a786c | ||
|
|
95f134a7c5 | ||
|
|
50f5c15027 | ||
|
|
65b43d8dc7 | ||
|
|
8dad4f5f10 | ||
|
|
a3a436ad64 | ||
|
|
ffb69d4c47 | ||
|
|
369ae1208e | ||
|
|
050dae7a9f | ||
|
|
a9cd567fc9 | ||
|
|
82073aea3e | ||
|
|
9cf7b8b6f6 | ||
|
|
fa5bd19368 | ||
|
|
fb72597fcf | ||
|
|
3ce0d78961 | ||
|
|
361636a3d1 | ||
|
|
0b49a7724b | ||
|
|
2db2cf2b1b | ||
|
|
c38505598f | ||
|
|
7e21f12678 | ||
|
|
4f43c57a96 | ||
|
|
60d3b59076 | ||
|
|
5a7956fd90 | ||
|
|
e193f57ac7 | ||
|
|
30f964447f | ||
|
|
cb9eccebed | ||
|
|
a0ae97c2e3 | ||
|
|
fd62e04bc0 | ||
|
|
e6c0b8d8e5 | ||
|
|
67e78a56a6 | ||
|
|
153d0c36b8 | ||
|
|
18bfe3cb43 | ||
|
|
61ba9b48f9 | ||
|
|
5e02a42ca0 | ||
|
|
89622749e2 | ||
|
|
eced524029 | ||
|
|
c4263b3330 | ||
|
|
68f4a7d4b0 | ||
|
|
7df5028c51 | ||
|
|
4de49c76d0 | ||
|
|
165cbcda89 | ||
|
|
c11bfaf07e | ||
|
|
98babd140e | ||
|
|
575a726361 | ||
|
|
ed2440f8cf | ||
|
|
59592f84fb | ||
|
|
ab690372d2 | ||
|
|
571028f124 | ||
|
|
36391187da | ||
|
|
35f2f5055a | ||
|
|
e2c974f53b | ||
|
|
74172969c1 | ||
|
|
6db392025c | ||
|
|
a61de47bb6 | ||
|
|
bcd5c838e8 | ||
|
|
482b81b0ed | ||
|
|
095c613b69 | ||
|
|
5eed034389 | ||
|
|
7a3bc83a55 | ||
|
|
706f4c6e20 | ||
|
|
f43bb0a9f7 | ||
|
|
64f0bb5460 | ||
|
|
e651330ff1 | ||
|
|
6a6a362a8d | ||
|
|
35046bb7b2 | ||
|
|
a10f2e1258 | ||
|
|
a0d7805484 | ||
|
|
ae77dac66d | ||
|
|
5f1f018a30 | ||
|
|
f2da5fcf2c | ||
|
|
9cce1e060a | ||
|
|
58e5b3959d | ||
|
|
84b6bf9789 | ||
|
|
8ba3f53d2c | ||
|
|
36571edb95 | ||
|
|
3a159beb21 | ||
|
|
6ebe844fc0 | ||
|
|
7359c9bf93 | ||
|
|
9b988f70de | ||
|
|
27d4e1d990 | ||
|
|
f7132ff139 | ||
|
|
b98289c660 | ||
|
|
85dbaaa5ff | ||
|
|
29c176b521 |
@@ -88,6 +88,7 @@ module.exports = {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
3
.github/cfp_headers
vendored
3
.github/cfp_headers
vendored
@@ -11,3 +11,6 @@
|
||||
|
||||
/apple-app-site-association
|
||||
Content-Type: application/json
|
||||
|
||||
/.well-known/assetlinks.json
|
||||
Content-Type: application/json
|
||||
|
||||
3
.github/labels.yml
vendored
3
.github/labels.yml
vendored
@@ -261,3 +261,6 @@
|
||||
color: "ededed"
|
||||
- name: "Z-t3chguy"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test-Disabled"
|
||||
description: "The flaking test has been disabled"
|
||||
color: "ededed"
|
||||
|
||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -37,6 +37,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
# Workaround for yarn install timeouts, especially on Windows
|
||||
- run: yarn config set network-timeout 300000
|
||||
|
||||
3
.github/workflows/build_develop.yml
vendored
3
.github/workflows/build_develop.yml
vendored
@@ -26,6 +26,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
@@ -98,7 +99,7 @@ jobs:
|
||||
running-workflow-name: "Build & Deploy develop.element.io"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare).)*$
|
||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$
|
||||
|
||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
||||
# as the expires after 24h and requires auth to download.
|
||||
|
||||
22
.github/workflows/dockerhub.yaml
vendored
22
.github/workflows/dockerhub.yaml
vendored
@@ -7,6 +7,9 @@ on:
|
||||
# This job can take a while, and we have usage limits, so just publish develop only twice a day
|
||||
- cron: "0 7/12 * * *"
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
|
||||
permissions:
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
jobs:
|
||||
buildx:
|
||||
name: Docker Buildx
|
||||
@@ -26,6 +29,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3
|
||||
|
||||
- name: Prepare
|
||||
if: matrix.prepare
|
||||
run: ${{ matrix.prepare }}
|
||||
@@ -39,7 +45,7 @@ jobs:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3
|
||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -58,7 +64,8 @@ jobs:
|
||||
${{ matrix.flavor }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -66,6 +73,17 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Sign the images with GitHub OIDC Token
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
images=""
|
||||
for tag in ${TAGS}; do
|
||||
images+="${tag}@${DIGEST} "
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Update repo description
|
||||
if: matrix.variant == 'vanilla'
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||
|
||||
1
.github/workflows/end-to-end-tests.yaml
vendored
1
.github/workflows/end-to-end-tests.yaml
vendored
@@ -22,6 +22,7 @@ jobs:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
with:
|
||||
element-web-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -20,7 +20,6 @@ jobs:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
include-changes: matrix-react-sdk
|
||||
gpg-fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
asset-path: dist/*.tar.gz
|
||||
expected-asset-count: 3
|
||||
@@ -31,11 +30,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify element-desktop repo that element-web release has completed to re-trigger release-drafter
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
uses: benc-uk/workflow-dispatch@25b02cc069be46d637e8fe2f1e8484008e9e9609 # v1
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: element-hq/element-desktop
|
||||
event-type: upstream-release-notify
|
||||
workflow: release-drafter.yml
|
||||
repo: element-hq/element-desktop
|
||||
ref: staging
|
||||
# Required when using the `repo` option. Either a PAT or a token generated from the GitHub app or CLI
|
||||
token: "${{ secrets.ELEMENT_BOT_TOKEN }}"
|
||||
|
||||
check:
|
||||
name: Post release checks
|
||||
|
||||
5
.github/workflows/static_analysis.yaml
vendored
5
.github/workflows/static_analysis.yaml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
@@ -43,6 +44,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
@@ -60,6 +62,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
# Needs branch matching as it inherits .stylelintrc.js from matrix-react-sdk
|
||||
- name: Install Dependencies
|
||||
@@ -77,6 +80,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
@@ -94,6 +98,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/layered.sh"
|
||||
|
||||
1
.github/workflows/tests.yaml
vendored
1
.github/workflows/tests.yaml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
|
||||
2
.github/workflows/triage-labelled.yml
vendored
2
.github/workflows/triage-labelled.yml
vendored
@@ -148,7 +148,7 @@ jobs:
|
||||
contains(github.event.issue.labels.*.name, 'A-Element-R')
|
||||
steps:
|
||||
- id: add_to_project
|
||||
uses: actions/add-to-project@v1.0.1
|
||||
uses: actions/add-to-project@v1.0.2
|
||||
with:
|
||||
project-url: ${{ env.PROJECT_URL }}
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
3
.github/workflows/update-jitsi.yml
vendored
3
.github/workflows/update-jitsi.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -21,7 +22,7 @@ jobs:
|
||||
run: "yarn update:jitsi"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@c55203cfde3e5c11a452d352b4393e68b85b4533 # v6
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/jitsi-update
|
||||
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -1,3 +1,77 @@
|
||||
Changes in [1.11.69](https://github.com/element-hq/element-web/releases/tag/v1.11.69) (2024-06-18)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Change avatar setting component to use a menu ([#12585](https://github.com/matrix-org/matrix-react-sdk/pull/12585)). Contributed by @dbkr.
|
||||
* New user profile UI in User Settings ([#12548](https://github.com/matrix-org/matrix-react-sdk/pull/12548)). Contributed by @dbkr.
|
||||
* MSC4108 support OIDC QR code login ([#12370](https://github.com/matrix-org/matrix-react-sdk/pull/12370)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix image upload preview size ([#12612](https://github.com/matrix-org/matrix-react-sdk/pull/12612)). Contributed by @RiotRobot.
|
||||
* Fix screen sharing in recent Chrome (https://github.com/matrix-org/matrix-js-sdk/pull/4243).
|
||||
* Fix roving tab index crash `compareDocumentPosition` ([#12594](https://github.com/matrix-org/matrix-react-sdk/pull/12594)). Contributed by @t3chguy.
|
||||
* Keep dialog glass border on narrow screens ([#12591](https://github.com/matrix-org/matrix-react-sdk/pull/12591)). Contributed by @dbkr.
|
||||
* Add missing a11y label to dismiss onboarding button in room list ([#12587](https://github.com/matrix-org/matrix-react-sdk/pull/12587)). Contributed by @t3chguy.
|
||||
* Add hover / active state on avatar setting upload button ([#12590](https://github.com/matrix-org/matrix-react-sdk/pull/12590)). Contributed by @dbkr.
|
||||
* Fix EditInPlace button styles ([#12589](https://github.com/matrix-org/matrix-react-sdk/pull/12589)). Contributed by @dbkr.
|
||||
* Fix incorrect assumptions about required fields in /search response ([#12575](https://github.com/matrix-org/matrix-react-sdk/pull/12575)). Contributed by @t3chguy.
|
||||
* Fix display of no avatar in avatar setting controls ([#12558](https://github.com/matrix-org/matrix-react-sdk/pull/12558)). Contributed by @dbkr.
|
||||
* Element-R: pass pickleKey in as raw key for indexeddb encryption ([#12543](https://github.com/matrix-org/matrix-react-sdk/pull/12543)). Contributed by @richvdh.
|
||||
|
||||
|
||||
|
||||
Changes in [1.11.68](https://github.com/element-hq/element-web/releases/tag/v1.11.68) (2024-06-04)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Tooltip: Improve accessibility for context menus ([#12462](https://github.com/matrix-org/matrix-react-sdk/pull/12462)). Contributed by @florianduros.
|
||||
* Tooltip: Improve accessibility of space panel ([#12525](https://github.com/matrix-org/matrix-react-sdk/pull/12525)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Close the release announcement when a dialog is opened ([#12559](https://github.com/matrix-org/matrix-react-sdk/pull/12559)). Contributed by @florianduros.
|
||||
* Tooltip: close field tooltip when ESC is pressed ([#12553](https://github.com/matrix-org/matrix-react-sdk/pull/12553)). Contributed by @florianduros.
|
||||
* Fix tabbedview breakpoint width ([#12556](https://github.com/matrix-org/matrix-react-sdk/pull/12556)). Contributed by @dbkr.
|
||||
* Fix E2E icon display in room header ([#12545](https://github.com/matrix-org/matrix-react-sdk/pull/12545)). Contributed by @florianduros.
|
||||
* Tooltip: Improve placement for space settings ([#12541](https://github.com/matrix-org/matrix-react-sdk/pull/12541)). Contributed by @florianduros.
|
||||
* Fix deformed avatar in a call in a narrow timeline ([#12538](https://github.com/matrix-org/matrix-react-sdk/pull/12538)). Contributed by @florianduros.
|
||||
* Shown own sent state indicator even when showReadReceipts is disabled ([#12540](https://github.com/matrix-org/matrix-react-sdk/pull/12540)). Contributed by @t3chguy.
|
||||
* Ensure we do not fire the verification mismatch modal multiple times ([#12526](https://github.com/matrix-org/matrix-react-sdk/pull/12526)). Contributed by @t3chguy.
|
||||
* Fix avatar in chat export ([#12537](https://github.com/matrix-org/matrix-react-sdk/pull/12537)). Contributed by @florianduros.
|
||||
* Use `*` for italics as it doesn't break when used mid-word ([#12523](https://github.com/matrix-org/matrix-react-sdk/pull/12523)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.11.67](https://github.com/element-hq/element-web/releases/tag/v1.11.67) (2024-05-22)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Tooltip: Improve the accessibility of the composer and the rich text editor ([#12459](https://github.com/matrix-org/matrix-react-sdk/pull/12459)). Contributed by @florianduros.
|
||||
* Allow explicit configuration of OIDC dynamic registration metadata ([#12514](https://github.com/matrix-org/matrix-react-sdk/pull/12514)). Contributed by @t3chguy.
|
||||
* Tooltip: improve accessibility for messages ([#12487](https://github.com/matrix-org/matrix-react-sdk/pull/12487)). Contributed by @florianduros.
|
||||
* Collapse UserSettings tabs to just icons on narrow screens ([#12505](https://github.com/matrix-org/matrix-react-sdk/pull/12505)). Contributed by @dbkr.
|
||||
* Add room topic to right panel room info ([#12503](https://github.com/matrix-org/matrix-react-sdk/pull/12503)). Contributed by @t3chguy.
|
||||
* OIDC: pass `id_token` via `id_token_hint` on Manage Account interaction ([#12499](https://github.com/matrix-org/matrix-react-sdk/pull/12499)). Contributed by @t3chguy.
|
||||
* Tooltip: improve accessibility in room ([#12493](https://github.com/matrix-org/matrix-react-sdk/pull/12493)). Contributed by @florianduros.
|
||||
* Tooltip: improve accessibility for call and voice messages ([#12489](https://github.com/matrix-org/matrix-react-sdk/pull/12489)). Contributed by @florianduros.
|
||||
* Move the active tab in user settings to the dialog title ([#12481](https://github.com/matrix-org/matrix-react-sdk/pull/12481)). Contributed by @dbkr.
|
||||
* Tooltip: improve accessibility of spaces ([#12497](https://github.com/matrix-org/matrix-react-sdk/pull/12497)). Contributed by @florianduros.
|
||||
* Tooltip: improve accessibility of the right panel ([#12490](https://github.com/matrix-org/matrix-react-sdk/pull/12490)). Contributed by @florianduros.
|
||||
* MSC3575 (Sliding Sync) add well-known proxy support ([#12307](https://github.com/matrix-org/matrix-react-sdk/pull/12307)). Contributed by @EdGeraghty.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Reuse single PlaybackWorker between Playback instances ([#12520](https://github.com/matrix-org/matrix-react-sdk/pull/12520)). Contributed by @t3chguy.
|
||||
* Fix well-known lookup for sliding sync labs check ([#12519](https://github.com/matrix-org/matrix-react-sdk/pull/12519)). Contributed by @t3chguy.
|
||||
* Fix `element-desktop-ssoid being` included in OIDC Authorization call ([#12495](https://github.com/matrix-org/matrix-react-sdk/pull/12495)). Contributed by @t3chguy.
|
||||
* Fix beta notifications reconciliation for intentional mentions push rules ([#12510](https://github.com/matrix-org/matrix-react-sdk/pull/12510)). Contributed by @t3chguy.
|
||||
* fix avatar stretched on 1:1 call ([#12494](https://github.com/matrix-org/matrix-react-sdk/pull/12494)). Contributed by @I-lander.
|
||||
* Check native sliding sync support against an unstable feature flag ([#12498](https://github.com/matrix-org/matrix-react-sdk/pull/12498)). Contributed by @turt2live.
|
||||
* Use OPTIONS for sliding sync detection poke ([#12492](https://github.com/matrix-org/matrix-react-sdk/pull/12492)). Contributed by @turt2live.
|
||||
* TAC: hide tooltip when the release announcement is displayed ([#12472](https://github.com/matrix-org/matrix-react-sdk/pull/12472)). Contributed by @florianduros.
|
||||
|
||||
|
||||
|
||||
Changes in [1.11.66](https://github.com/element-hq/element-web/releases/tag/v1.11.66) (2024-05-07)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -17,17 +17,17 @@ module.exports = {
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-export-default-from",
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-transform-numeric-separator",
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-object-rest-spread",
|
||||
"@babel/plugin-transform-optional-chaining",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator",
|
||||
|
||||
// transform logical assignment (??=, ||=, &&=). preset-env doesn't
|
||||
// normally bother with these (presumably because all the target
|
||||
// browsers support it natively), but they make our webpack version (or
|
||||
// something downstream of babel, at least) fall over.
|
||||
"@babel/plugin-proposal-logical-assignment-operators",
|
||||
"@babel/plugin-transform-logical-assignment-operators",
|
||||
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
|
||||
@@ -225,6 +225,12 @@ Unless otherwise specified, the following applies to all code:
|
||||
}
|
||||
```
|
||||
|
||||
37. Avoid functions whose fundamental behaviour varies with different parameter types.
|
||||
Multiple return types are fine, but if the function's behaviour is going to change significantly,
|
||||
have two separate functions. For example, `SDKConfig.get()` with a string param which returns the
|
||||
type according to the param given is ok, but `SDKConfig.get()` with no args returning the whole
|
||||
config object would not be: this should just be a separate function.
|
||||
|
||||
## React
|
||||
|
||||
Inheriting all the rules of TypeScript, the following additionally apply:
|
||||
|
||||
@@ -250,17 +250,60 @@ When Element is deployed alongside a homeserver with SSO-only login, some option
|
||||
user can be sent to in order to log them out of that system too, making logout symmetric between Element and the SSO system.
|
||||
2. `sso_redirect_options`: Options to define how to handle unauthenticated users. If the object contains `"immediate": true`, then
|
||||
all unauthenticated users will be automatically redirected to the SSO system to start their login. If instead you'd only like to
|
||||
have users which land on the welcome page to be redirected, use `"on_welcome_page": true`. As an example:
|
||||
have users which land on the welcome page to be redirected, use `"on_welcome_page": true`. Additionally, there is an option to
|
||||
redirect anyone landing on the login page, by using `"on_login_page": true`. As an example:
|
||||
```json
|
||||
{
|
||||
"sso_redirect_options": {
|
||||
"immediate": false,
|
||||
"on_welcome_page": true
|
||||
"on_welcome_page": true,
|
||||
"on_login_page": true
|
||||
}
|
||||
}
|
||||
```
|
||||
It is most common to use the `immediate` flag instead of `on_welcome_page`.
|
||||
|
||||
## Native OIDC
|
||||
|
||||
Native OIDC support is currently in labs and is subject to change.
|
||||
|
||||
Static OIDC Client IDs are preferred and can be specified under `oidc_static_clients` as a mapping from `issuer` to configuration object containing `client_id`.
|
||||
Issuer must have a trailing forward slash. As an example:
|
||||
|
||||
```json
|
||||
{
|
||||
"oidc_static_clients": {
|
||||
"https://auth.example.com/": {
|
||||
"client_id": "example-client-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If a matching static client is not found, the app will attempt to dynamically register a client using metadata specified under `oidc_metadata`.
|
||||
The app has sane defaults for the metadata properties below but on stricter policy identity providers they may not pass muster, e.g. `contacts` may be required.
|
||||
The following subproperties are available:
|
||||
|
||||
1. `client_uri`: This is the base URI for the OIDC client registration, typically `logo_uri`, `tos_uri`, and `policy_uri` must be either on the same domain or a subdomain of this URI.
|
||||
2. `logo_uri`: Optional URI for the client logo.
|
||||
3. `tos_uri`: Optional URI for the client's terms of service.
|
||||
4. `policy_uri`: Optional URI for the client's privacy policy.
|
||||
5. `contacts`: Optional list of contact emails for the client.
|
||||
|
||||
As an example:
|
||||
|
||||
```json
|
||||
{
|
||||
"oidc_metadata": {
|
||||
"client_uri": "https://example.com",
|
||||
"logo_uri": "https://example.com/logo.png",
|
||||
"tos_uri": "https://example.com/tos",
|
||||
"policy_uri": "https://example.com/policy",
|
||||
"contacts": ["support@example.com"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## VoIP / Jitsi calls
|
||||
|
||||
Currently, Element uses Jitsi to offer conference calls in rooms, with an experimental Element Call implementation in the works.
|
||||
|
||||
23
docs/labs.md
23
docs/labs.md
@@ -110,29 +110,6 @@ This is useful while we experiment with encryption and to make calling compatibl
|
||||
|
||||
Enables rendering of MD / HTML in room topics.
|
||||
|
||||
## Use the Rust cryptography implementation (`feature_rust_crypto`) [In Development]
|
||||
|
||||
Configures Element to use a new cryptography implementation based on the [matrix-rust-sdk](https://github.com/matrix-org/matrix-rust-sdk).
|
||||
|
||||
This setting is (currently) _sticky_ to a user's session: it only takes effect when the user logs in to a new session. Likewise, even after disabling the setting in `config.json`, the Rust implementation will remain in use until users log out.
|
||||
|
||||
This configuration value is now set to `true` by default. This means that without any additional configuration
|
||||
every new login will use the new cryptography implementation.
|
||||
|
||||
For administrators looking to transition existing users to the new stack, the `RustCrypto.staged_rollout_percent` configuration is available.
|
||||
This configuration allows for a phased migration of users, represented as an integer percentage (0 to 100). By default, this value is set to `0`,
|
||||
which means no existing users will be migrated to the new stack. If you wish to migrate all users, you can adjust this value to `100`.
|
||||
|
||||
This configuration should be placed under the `setting_defaults` section as shown:
|
||||
|
||||
```
|
||||
"setting_defaults": {
|
||||
"RustCrypto.staged_rollout_percent": 20
|
||||
},
|
||||
```
|
||||
|
||||
By adjusting the `RustCrypto.staged_rollout_percent` value, you can control the migration process according to your deployment strategy.
|
||||
|
||||
## New room header & details (`feature_new_room_decoration_ui`) [In Development]
|
||||
|
||||
Refactors visually the room header and room sidebar
|
||||
|
||||
@@ -45,6 +45,6 @@
|
||||
"privacy_policy_url": "https://element.io/cookie-policy",
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
|
||||
"setting_defaults": {
|
||||
"RustCrypto.staged_rollout_percent": 30
|
||||
"RustCrypto.staged_rollout_percent": 60
|
||||
}
|
||||
}
|
||||
|
||||
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.66",
|
||||
"version": "1.11.69",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -71,9 +71,7 @@
|
||||
"@types/react": "17.0.80"
|
||||
},
|
||||
"dependencies": {
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@matrix-org/react-sdk-module-api": "^2.3.0",
|
||||
"gfm.css": "^1.1.2",
|
||||
"jsrsasign": "^11.0.0",
|
||||
"katex": "^0.16.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -90,14 +88,14 @@
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.20.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-class-properties": "^7.12.1",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.20.7",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.12.7",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
@@ -133,9 +131,10 @@
|
||||
"@types/react-transition-group": "^4.4.9",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/sdp-transform": "^2.4.9",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/tar-js": "^0.3.5",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
@@ -147,18 +146,18 @@
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"cronstrue": "^2.41.0",
|
||||
"css-loader": "^7.0.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
"dotenv": "^16.0.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-deprecate": "0.8.4",
|
||||
"eslint-plugin-deprecate": "0.8.5",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"file-loader": "^6.0.0",
|
||||
@@ -182,15 +181,14 @@
|
||||
"postcss-loader": "8.1.0",
|
||||
"postcss-mixins": "^10.0.0",
|
||||
"postcss-nested": "^6.0.0",
|
||||
"postcss-preset-env": "^9.3.0",
|
||||
"postcss-preset-env": "^9.5.14",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.2.5",
|
||||
"prettier": "3.3.2",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^5.0.0",
|
||||
"semver": "^7.5.2",
|
||||
"setimmediate": "^1.0.5",
|
||||
"string-replace-loader": "3",
|
||||
"style-loader": "4",
|
||||
"stylelint": "^16.1.0",
|
||||
@@ -199,7 +197,7 @@
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typescript": "5.4.5",
|
||||
"typescript": "5.5.2",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
|
||||
@@ -38,7 +38,7 @@ module.exports.pitch = function pitch(request) {
|
||||
return cb(err);
|
||||
}
|
||||
if (entries[0]) {
|
||||
return cb(null, `module.exports = __webpack_public_path__ + ${JSON.stringify(entries[0].files[0])};`);
|
||||
return cb(null, `module.exports = __webpack_public_path__ + ${JSON.stringify([...entries[0].files][0])};`);
|
||||
}
|
||||
return cb(null, null);
|
||||
});
|
||||
|
||||
62
res/.well-known/assetlinks.json
Normal file
62
res/.well-known/assetlinks.json
Normal file
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "im.vector.app.debug",
|
||||
"sha256_cert_fingerprints": [
|
||||
"B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "im.vector.app.nightly",
|
||||
"sha256_cert_fingerprints": [
|
||||
"CA:D3:85:16:84:3A:05:CC:EB:00:AB:7B:D3:80:0F:01:BA:8F:E0:4B:38:86:F3:97:D8:F7:9A:1B:C4:54:E4:0F"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "im.vector.app",
|
||||
"sha256_cert_fingerprints": [
|
||||
"F3:FF:38:D2:E5:A6:38:84:86:4A:4E:0D:45:C5:3B:19:8E:7E:39:C0:50:5B:D9:63:F5:55:D6:53:2D:EA:BF:5F"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "io.element.android.x.debug",
|
||||
"sha256_cert_fingerprints": [
|
||||
"B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "io.element.android.x.nightly",
|
||||
"sha256_cert_fingerprints": [
|
||||
"CA:D3:85:16:84:3A:05:CC:EB:00:AB:7B:D3:80:0F:01:BA:8F:E0:4B:38:86:F3:97:D8:F7:9A:1B:C4:54:E4:0F"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "io.element.android.x",
|
||||
"sha256_cert_fingerprints": [
|
||||
"C6:DB:9B:9C:8C:BD:D6:5D:16:E8:EC:8C:8B:91:C8:31:B9:EF:C9:5C:BF:98:AE:41:F6:A9:D8:35:15:1A:7E:16"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
3
res/jitsi_external_api.min.js
vendored
3
res/jitsi_external_api.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -194,9 +194,9 @@ export default class Favicon {
|
||||
}
|
||||
|
||||
private setIcon(canvas: HTMLCanvasElement): void {
|
||||
setImmediate(() => {
|
||||
setTimeout(() => {
|
||||
this.setIconSrc(canvas.toDataURL("image/png"));
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private setIconSrc(url: string): void {
|
||||
|
||||
184
src/serviceworker/index.ts
Normal file
184
src/serviceworker/index.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { idbLoad } from "matrix-react-sdk/src/utils/StorageAccess";
|
||||
import { ACCESS_TOKEN_IV, tryDecryptToken } from "matrix-react-sdk/src/utils/tokens/tokens";
|
||||
import { buildAndEncodePickleKey } from "matrix-react-sdk/src/utils/tokens/pickling";
|
||||
|
||||
const serverSupportMap: {
|
||||
[serverUrl: string]: {
|
||||
supportsAuthedMedia: boolean;
|
||||
cacheExpiryTimeMs: number;
|
||||
};
|
||||
} = {};
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
// We skipWaiting() to update the service worker more frequently, particularly in development environments.
|
||||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
||||
event.waitUntil(skipWaiting());
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
// We force all clients to be under our control, immediately. This could be old tabs.
|
||||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
||||
event.waitUntil(clients.claim());
|
||||
});
|
||||
|
||||
// @ts-expect-error - the service worker types conflict with the DOM types available through TypeScript. Many hours
|
||||
// have been spent trying to convince the type system that there's no actual conflict, but it has yet to work. Instead
|
||||
// of trying to make it do the thing, we force-cast to something close enough where we can (and ignore errors otherwise).
|
||||
self.addEventListener("fetch", (event: FetchEvent) => {
|
||||
// This is the authenticated media (MSC3916) check, proxying what was unauthenticated to the authenticated variants.
|
||||
|
||||
if (event.request.method !== "GET") {
|
||||
return; // not important to us
|
||||
}
|
||||
|
||||
// Note: ideally we'd keep the request headers etc, but in practice we can't even see those details.
|
||||
// See https://stackoverflow.com/a/59152482
|
||||
let url = event.request.url;
|
||||
|
||||
// We only intercept v3 download and thumbnail requests as presumably everything else is deliberate.
|
||||
// For example, `/_matrix/media/unstable` or `/_matrix/media/v3/preview_url` are something well within
|
||||
// the control of the application, and appear to be choices made at a higher level than us.
|
||||
if (!url.includes("/_matrix/media/v3/download") && !url.includes("/_matrix/media/v3/thumbnail")) {
|
||||
return; // not a URL we care about
|
||||
}
|
||||
|
||||
// We need to call respondWith synchronously, otherwise we may never execute properly. This means
|
||||
// later on we need to proxy the request through if it turns out the server doesn't support authentication.
|
||||
event.respondWith(
|
||||
(async (): Promise<Response> => {
|
||||
let accessToken: string | undefined;
|
||||
try {
|
||||
// Figure out which homeserver we're communicating with
|
||||
const csApi = url.substring(0, url.indexOf("/_matrix/media/v3"));
|
||||
|
||||
// Add jitter to reduce request spam, particularly to `/versions` on initial page load
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), Math.random() * 10));
|
||||
|
||||
// Locate our access token, and populate the fetchConfig with the authentication header.
|
||||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
||||
const client = await self.clients.get(event.clientId);
|
||||
accessToken = await getAccessToken(client);
|
||||
|
||||
// Update or populate the server support map using a (usually) authenticated `/versions` call.
|
||||
await tryUpdateServerSupportMap(csApi, accessToken);
|
||||
|
||||
// If we have server support (and a means of authentication), rewrite the URL to use MSC3916 endpoints.
|
||||
if (serverSupportMap[csApi].supportsAuthedMedia && accessToken) {
|
||||
url = url.replace(/\/media\/v3\/(.*)\//, "/client/v1/media/$1/");
|
||||
} // else by default we make no changes
|
||||
} catch (err) {
|
||||
console.error("SW: Error in request rewrite.", err);
|
||||
}
|
||||
|
||||
// Add authentication and send the request. We add authentication even if MSC3916 endpoints aren't
|
||||
// being used to ensure patches like this work:
|
||||
// https://github.com/matrix-org/synapse/commit/2390b66bf0ec3ff5ffb0c7333f3c9b239eeb92bb
|
||||
return fetch(url, fetchConfigForToken(accessToken));
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
async function tryUpdateServerSupportMap(clientApiUrl: string, accessToken?: string): Promise<void> {
|
||||
// only update if we don't know about it, or if the data is stale
|
||||
if (serverSupportMap[clientApiUrl]?.cacheExpiryTimeMs > new Date().getTime()) {
|
||||
return; // up to date
|
||||
}
|
||||
|
||||
const config = fetchConfigForToken(accessToken);
|
||||
const versions = await (await fetch(`${clientApiUrl}/_matrix/client/versions`, config)).json();
|
||||
|
||||
serverSupportMap[clientApiUrl] = {
|
||||
supportsAuthedMedia: Boolean(versions?.versions?.includes("v1.11")),
|
||||
cacheExpiryTimeMs: new Date().getTime() + 2 * 60 * 60 * 1000, // 2 hours from now
|
||||
};
|
||||
}
|
||||
|
||||
// Ideally we'd use the `Client` interface for `client`, but since it's not available (see 'fetch' listener), we use
|
||||
// unknown for now and force-cast it to something close enough later.
|
||||
async function getAccessToken(client: unknown): Promise<string | undefined> {
|
||||
// Access tokens are encrypted at rest, so while we can grab the "access token", we'll need to do work to get the
|
||||
// real thing.
|
||||
const encryptedAccessToken = await idbLoad("account", "mx_access_token");
|
||||
|
||||
// We need to extract a user ID and device ID from localstorage, which means calling WebPlatform for the
|
||||
// read operation. Service workers can't access localstorage.
|
||||
const { userId, deviceId } = await askClientForUserIdParams(client);
|
||||
|
||||
// ... and this is why we need the user ID and device ID: they're index keys for the pickle key table.
|
||||
const pickleKeyData = await idbLoad("pickleKey", [userId, deviceId]);
|
||||
if (pickleKeyData && (!pickleKeyData.encrypted || !pickleKeyData.iv || !pickleKeyData.cryptoKey)) {
|
||||
console.error("SW: Invalid pickle key loaded - ignoring");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Finally, try decrypting the thing and return that. This may fail, but that's okay.
|
||||
try {
|
||||
const pickleKey = await buildAndEncodePickleKey(pickleKeyData, userId, deviceId);
|
||||
return tryDecryptToken(pickleKey, encryptedAccessToken, ACCESS_TOKEN_IV);
|
||||
} catch (e) {
|
||||
console.error("SW: Error decrypting access token.", e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Ideally we'd use the `Client` interface for `client`, but since it's not available (see 'fetch' listener), we use
|
||||
// unknown for now and force-cast it to something close enough inside the function.
|
||||
async function askClientForUserIdParams(client: unknown): Promise<{ userId: string; deviceId: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Dev note: this uses postMessage, which is a highly insecure channel. postMessage is typically visible to other
|
||||
// tabs, windows, browser extensions, etc, making it far from ideal for sharing sensitive information. This is
|
||||
// why our service worker calculates/decrypts the access token manually: we don't want the user's access token
|
||||
// to be available to (potentially) malicious listeners. We do require some information for that decryption to
|
||||
// work though, and request that in the least sensitive way possible.
|
||||
//
|
||||
// We could also potentially use some version of TLS to encrypt postMessage, though that feels way more involved
|
||||
// than just reading IndexedDB ourselves.
|
||||
|
||||
// Avoid stalling the tab in case something goes wrong.
|
||||
const timeoutId = setTimeout(() => reject(new Error("timeout in postMessage")), 1000);
|
||||
|
||||
// We don't need particularly good randomness here - we just use this to generate a request ID, so we know
|
||||
// which postMessage reply is for our active request.
|
||||
const responseKey = Math.random().toString(36);
|
||||
|
||||
// Add the listener first, just in case the tab is *really* fast.
|
||||
const listener = (event: MessageEvent): void => {
|
||||
if (event.data?.responseKey !== responseKey) return; // not for us
|
||||
clearTimeout(timeoutId); // do this as soon as possible, avoiding a race between resolve and reject.
|
||||
resolve(event.data); // "unblock" the remainder of the thread, if that were such a thing in JavaScript.
|
||||
self.removeEventListener("message", listener); // cleanup, since we're not going to do anything else.
|
||||
};
|
||||
self.addEventListener("message", listener);
|
||||
|
||||
// Ask the tab for the information we need. This is handled by WebPlatform.
|
||||
(client as Window).postMessage({ responseKey, type: "userinfo" });
|
||||
});
|
||||
}
|
||||
|
||||
function fetchConfigForToken(accessToken?: string): RequestInit | undefined {
|
||||
if (!accessToken) {
|
||||
return undefined; // no headers/config to specify
|
||||
}
|
||||
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -89,9 +89,14 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref<MatrixCha
|
||||
// XXX: This path matching is a bit brittle, but better to do it early instead of in the app code.
|
||||
const isWelcomeOrLanding =
|
||||
window.location.hash === "#/welcome" || window.location.hash === "#" || window.location.hash === "";
|
||||
const isLoginPage = window.location.hash === "#/login";
|
||||
|
||||
if (!autoRedirect && ssoRedirects.on_welcome_page && isWelcomeOrLanding) {
|
||||
autoRedirect = true;
|
||||
}
|
||||
if (!autoRedirect && ssoRedirects.on_login_page && isLoginPage) {
|
||||
autoRedirect = true;
|
||||
}
|
||||
if (!hasPossibleToken && !isReturningFromSso && autoRedirect) {
|
||||
logger.log("Bypassing app load to redirect to SSO");
|
||||
const tempCli = createClient({
|
||||
|
||||
@@ -81,31 +81,6 @@
|
||||
<img src="<%= require('matrix-react-sdk/res/img/format/quote.svg').default %>" aria-hidden alt="" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
|
||||
<img src="<%= require('matrix-react-sdk/res/img/format/strikethrough.svg').default %>" aria-hidden alt="" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
|
||||
|
||||
<audio id="messageAudio">
|
||||
<source src="media/message.ogg" type="audio/ogg" />
|
||||
<source src="media/message.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="ringAudio" loop>
|
||||
<source src="media/ring.ogg" type="audio/ogg" />
|
||||
<source src="media/ring.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="ringbackAudio" loop>
|
||||
<source src="media/ringback.ogg" type="audio/ogg" />
|
||||
<source src="media/ringback.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="callendAudio">
|
||||
<source src="media/callend.ogg" type="audio/ogg" />
|
||||
<source src="media/callend.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="busyAudio">
|
||||
<source src="media/busy.ogg" type="audio/ogg" />
|
||||
<source src="media/busy.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="errorAudio">
|
||||
<source src="media/error.ogg" type="audio/ogg" />
|
||||
<source src="media/error.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
<audio id="remoteAudio"></audio>
|
||||
<!-- let CSS themes pass constants to the app -->
|
||||
<div id="mx_theme_accentColor"></div><div id="mx_theme_secondaryAccentColor"></div><div id="mx_theme_tertiaryAccentColor"></div>
|
||||
|
||||
|
||||
@@ -19,19 +19,14 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { extractErrorMessageFromError } from "matrix-react-sdk/src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
// These are things that can run before the skin loads - be careful not to reference the react-sdk though.
|
||||
import { parseQsFromFragment } from "./url_utils";
|
||||
import "./modernizr";
|
||||
|
||||
// Make setImmediate available in bundle
|
||||
import "setimmediate";
|
||||
|
||||
// Require common CSS here; this will make webpack process it into bundle.css.
|
||||
// Our own CSS (which is themed) is imported via separate webpack entry points
|
||||
// in webpack.config.js
|
||||
require("gfm.css/gfm.css");
|
||||
require("katex/dist/katex.css");
|
||||
|
||||
/**
|
||||
@@ -60,8 +55,8 @@ function checkBrowserFeatures(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Custom checks atop Modernizr because it doesn't have ES2018/ES2019 checks
|
||||
// in it for some features we depend on.
|
||||
// Custom checks atop Modernizr because it doesn't have checks in it for
|
||||
// some features we depend on.
|
||||
// Modernizr requires rules to be lowercase with no punctuation.
|
||||
// ES2018: http://262.ecma-international.org/9.0/#sec-promise.prototype.finally
|
||||
window.Modernizr.addTest("promiseprototypefinally", () => typeof window.Promise?.prototype?.finally === "function");
|
||||
@@ -74,6 +69,13 @@ function checkBrowserFeatures(): boolean {
|
||||
);
|
||||
// ES2019: http://262.ecma-international.org/10.0/#sec-object.fromentries
|
||||
window.Modernizr.addTest("objectfromentries", () => typeof window.Object?.fromEntries === "function");
|
||||
// ES2024: https://tc39.es/ecma262/2024/#sec-get-regexp.prototype.unicodesets
|
||||
window.Modernizr.addTest(
|
||||
"regexpunicodesets",
|
||||
() => window.RegExp?.prototype && "unicodeSets" in window.RegExp.prototype,
|
||||
);
|
||||
// ES2024: https://402.ecma-international.org/9.0/#sec-intl.segmenter
|
||||
window.Modernizr.addTest("intlsegmenter", () => typeof window.Intl?.Segmenter === "function");
|
||||
|
||||
const featureList = Object.keys(window.Modernizr) as Array<keyof ModernizrStatic>;
|
||||
|
||||
@@ -109,7 +111,6 @@ async function start(): Promise<void> {
|
||||
rageshakePromise,
|
||||
setupLogStorage,
|
||||
preparePlatform,
|
||||
loadOlm,
|
||||
loadConfig,
|
||||
loadLanguage,
|
||||
loadTheme,
|
||||
@@ -118,6 +119,7 @@ async function start(): Promise<void> {
|
||||
showError,
|
||||
showIncompatibleBrowser,
|
||||
_t,
|
||||
extractErrorMessageFromError,
|
||||
} = await import(
|
||||
/* webpackChunkName: "init" */
|
||||
/* webpackPreload: true */
|
||||
@@ -147,7 +149,6 @@ async function start(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const loadOlmPromise = loadOlm();
|
||||
// set the platform for react sdk
|
||||
preparePlatform();
|
||||
// load config requires the platform to be ready
|
||||
@@ -180,7 +181,7 @@ async function start(): Promise<void> {
|
||||
// error handling begins here
|
||||
// ##########################
|
||||
if (!acceptBrowser) {
|
||||
await new Promise<void>((resolve) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
logger.error("Browser is missing required features.");
|
||||
// take to a different landing page to AWOOOOOGA at the user
|
||||
showIncompatibleBrowser(() => {
|
||||
@@ -189,7 +190,7 @@ async function start(): Promise<void> {
|
||||
}
|
||||
logger.log("User accepts the compatibility risks.");
|
||||
resolve();
|
||||
});
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -214,7 +215,6 @@ async function start(): Promise<void> {
|
||||
// app load critical path starts here
|
||||
// assert things started successfully
|
||||
// ##################################
|
||||
await loadOlmPromise;
|
||||
await loadModulesPromise;
|
||||
await loadThemePromise;
|
||||
await loadLanguagePromise;
|
||||
|
||||
@@ -17,10 +17,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import olmWasmPath from "@matrix-org/olm/olm.wasm";
|
||||
import Olm from "@matrix-org/olm";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import * as React from "react";
|
||||
import * as languageHandler from "matrix-react-sdk/src/languageHandler";
|
||||
@@ -76,48 +72,6 @@ export async function loadConfig(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export function loadOlm(): Promise<void> {
|
||||
/* Load Olm. We try the WebAssembly version first, and then the legacy,
|
||||
* asm.js version if that fails. For this reason we need to wait for this
|
||||
* to finish before continuing to load the rest of the app. In future
|
||||
* we could somehow pass a promise down to react-sdk and have it wait on
|
||||
* that so olm can be loading in parallel with the rest of the app.
|
||||
*
|
||||
* We also need to tell the Olm js to look for its wasm file at the same
|
||||
* level as index.html. It really should be in the same place as the js,
|
||||
* ie. in the bundle directory, but as far as I can tell this is
|
||||
* completely impossible with webpack. We do, however, use a hashed
|
||||
* filename to avoid caching issues.
|
||||
*/
|
||||
return Olm.init({
|
||||
locateFile: () => olmWasmPath,
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("Using WebAssembly Olm");
|
||||
})
|
||||
.catch((wasmLoadError) => {
|
||||
logger.log("Failed to load Olm: trying legacy version", wasmLoadError);
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = document.createElement("script");
|
||||
s.src = "olm_legacy.js"; // XXX: This should be cache-busted too
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.body.appendChild(s);
|
||||
})
|
||||
.then(() => {
|
||||
// Init window.Olm, ie. the one just loaded by the script tag,
|
||||
// not 'Olm' which is still the failed wasm version.
|
||||
return window.Olm.init();
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("Using legacy Olm");
|
||||
})
|
||||
.catch((legacyLoadError) => {
|
||||
logger.log("Both WebAssembly and asm.js Olm failed!", legacyLoadError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadLanguage(): Promise<void> {
|
||||
const prefLang = SettingsStore.getValue("language", null, /*excludeDefault=*/ true);
|
||||
let langs: string[] = [];
|
||||
@@ -138,7 +92,7 @@ export async function loadLanguage(): Promise<void> {
|
||||
}
|
||||
|
||||
export async function loadTheme(): Promise<void> {
|
||||
setTheme();
|
||||
return setTheme();
|
||||
}
|
||||
|
||||
export async function loadApp(fragParams: {}): Promise<void> {
|
||||
@@ -189,3 +143,5 @@ export async function loadModules(): Promise<void> {
|
||||
}
|
||||
|
||||
export { _t } from "../languageHandler";
|
||||
|
||||
export { extractErrorMessageFromError } from "matrix-react-sdk/src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
@@ -177,17 +177,17 @@ const setupCompleted = (async (): Promise<string | void> => {
|
||||
}
|
||||
}
|
||||
|
||||
await widgetApi!.transport.reply(ev.detail, response);
|
||||
widgetApi!.transport.reply(ev.detail, response);
|
||||
});
|
||||
};
|
||||
|
||||
handleAction(ElementWidgetActions.JoinCall, async ({ audioInput, videoInput }) => {
|
||||
joinConference(audioInput as string | null, videoInput as string | null);
|
||||
void joinConference(audioInput as string | null, videoInput as string | null);
|
||||
});
|
||||
handleAction(ElementWidgetActions.HangupCall, async ({ force }) => {
|
||||
if (force === true) {
|
||||
meetApi?.dispose();
|
||||
notifyHangup();
|
||||
void notifyHangup();
|
||||
meetApi = undefined;
|
||||
closeConference();
|
||||
} else {
|
||||
@@ -297,7 +297,7 @@ function toggleConferenceVisibility(inConference: boolean): void {
|
||||
|
||||
function skipToJitsiSplashScreen(): void {
|
||||
// really just a function alias for self-documenting code
|
||||
joinConference();
|
||||
void joinConference();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -500,8 +500,8 @@ const onVideoConferenceJoined = (): void => {
|
||||
if (widgetApi) {
|
||||
// ignored promise because we don't care if it works
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
widgetApi.setAlwaysOnScreen(true);
|
||||
widgetApi.transport.send(ElementWidgetActions.JoinCall, {});
|
||||
void widgetApi.setAlwaysOnScreen(true);
|
||||
void widgetApi.transport.send(ElementWidgetActions.JoinCall, {});
|
||||
}
|
||||
|
||||
// Video rooms should start in tile mode
|
||||
@@ -509,7 +509,7 @@ const onVideoConferenceJoined = (): void => {
|
||||
};
|
||||
|
||||
const onVideoConferenceLeft = (): void => {
|
||||
notifyHangup();
|
||||
void notifyHangup();
|
||||
meetApi = undefined;
|
||||
};
|
||||
|
||||
@@ -517,7 +517,7 @@ const onErrorOccurred = ({ error }: Parameters<ExternalAPIEventCallbacks["errorO
|
||||
if (error.isFatal) {
|
||||
// We got disconnected. Since Jitsi Meet might send us back to the
|
||||
// prejoin screen, we're forced to act as if we hung up entirely.
|
||||
notifyHangup(error.message);
|
||||
void notifyHangup(error.message);
|
||||
meetApi = undefined;
|
||||
closeConference();
|
||||
}
|
||||
@@ -525,7 +525,7 @@ const onErrorOccurred = ({ error }: Parameters<ExternalAPIEventCallbacks["errorO
|
||||
|
||||
const onAudioMuteStatusChanged = ({ muted }: AudioMuteStatusChangedEvent): void => {
|
||||
const action = muted ? ElementWidgetActions.MuteAudio : ElementWidgetActions.UnmuteAudio;
|
||||
widgetApi?.transport.send(action, {});
|
||||
void widgetApi?.transport.send(action, {});
|
||||
};
|
||||
|
||||
const onVideoMuteStatusChanged = ({ muted }: VideoMuteStatusChangedEvent): void => {
|
||||
@@ -535,15 +535,15 @@ const onVideoMuteStatusChanged = ({ muted }: VideoMuteStatusChangedEvent): void
|
||||
// otherwise the React SDK will mistakenly think the user turned off
|
||||
// their video by hand
|
||||
setTimeout(() => {
|
||||
if (meetApi) widgetApi?.transport.send(ElementWidgetActions.MuteVideo, {});
|
||||
if (meetApi) void widgetApi?.transport.send(ElementWidgetActions.MuteVideo, {});
|
||||
}, 200);
|
||||
} else {
|
||||
widgetApi?.transport.send(ElementWidgetActions.UnmuteVideo, {});
|
||||
void widgetApi?.transport.send(ElementWidgetActions.UnmuteVideo, {});
|
||||
}
|
||||
};
|
||||
|
||||
const updateParticipants = (): void => {
|
||||
widgetApi?.transport.send(ElementWidgetActions.CallParticipants, {
|
||||
void widgetApi?.transport.send(ElementWidgetActions.CallParticipants, {
|
||||
participants: meetApi?.getParticipantsInfo(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -120,4 +120,4 @@ async function initPage(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
initPage();
|
||||
void initPage();
|
||||
|
||||
@@ -57,8 +57,6 @@ interface SquirrelUpdate {
|
||||
updateURL: string;
|
||||
}
|
||||
|
||||
const LEGACY_PROTOCOL = "element";
|
||||
const OIDC_PROTOCOL = "io.element.desktop";
|
||||
const SSO_ID_KEY = "element-desktop-ssoid";
|
||||
|
||||
const isMac = navigator.platform.toUpperCase().includes("MAC");
|
||||
@@ -169,15 +167,14 @@ export default class ElectronPlatform extends VectorBasePlatform {
|
||||
});
|
||||
});
|
||||
|
||||
window.electron.on("openDesktopCapturerSourcePicker", () => {
|
||||
window.electron.on("openDesktopCapturerSourcePicker", async () => {
|
||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||
finished.then(([source]) => {
|
||||
// getDisplayMedia promise does not return if no dummy is passed here as source
|
||||
this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" });
|
||||
});
|
||||
const [source] = await finished;
|
||||
// getDisplayMedia promise does not return if no dummy is passed here as source
|
||||
await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" });
|
||||
});
|
||||
|
||||
this.ipc.call("startSSOFlow", this.ssoID);
|
||||
void this.ipc.call("startSSOFlow", this.ssoID);
|
||||
|
||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
}
|
||||
@@ -197,7 +194,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
|
||||
),
|
||||
initial: getInitialLetter(r.name),
|
||||
}));
|
||||
this.ipc.call("breadcrumbs", rooms);
|
||||
void this.ipc.call("breadcrumbs", rooms);
|
||||
};
|
||||
|
||||
private onUpdateDownloaded = async (ev: Event, { releaseNotes, releaseName }: SquirrelUpdate): Promise<void> => {
|
||||
@@ -263,7 +260,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
|
||||
const handler = notification.onclick as Function;
|
||||
notification.onclick = (): void => {
|
||||
handler?.();
|
||||
this.ipc.call("focusWindow");
|
||||
void this.ipc.call("focusWindow");
|
||||
};
|
||||
|
||||
return notification;
|
||||
@@ -381,7 +378,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
|
||||
|
||||
public getSSOCallbackUrl(fragmentAfterLogin?: string): URL {
|
||||
const url = super.getSSOCallbackUrl(fragmentAfterLogin);
|
||||
url.protocol = LEGACY_PROTOCOL;
|
||||
url.protocol = "element";
|
||||
url.searchParams.set(SSO_ID_KEY, this.ssoID);
|
||||
return url;
|
||||
}
|
||||
@@ -401,7 +398,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
|
||||
}
|
||||
|
||||
public navigateForwardBack(back: boolean): void {
|
||||
this.ipc.call(back ? "navigateBack" : "navigateForward");
|
||||
void this.ipc.call(back ? "navigateBack" : "navigateForward");
|
||||
}
|
||||
|
||||
public overrideBrowserShortcuts(): boolean {
|
||||
@@ -446,13 +443,16 @@ export default class ElectronPlatform extends VectorBasePlatform {
|
||||
return (SdkConfig.get() as unknown as Record<string, string>)["web_base_url"] ?? "https://app.element.io";
|
||||
}
|
||||
|
||||
public get defaultOidcClientUri(): string {
|
||||
// Default to element.io as our scheme `io.element.desktop` is within its scope on default MAS policies
|
||||
return "https://element.io";
|
||||
}
|
||||
|
||||
public async getOidcClientMetadata(): Promise<OidcRegistrationClientMetadata> {
|
||||
const baseMetadata = await super.getOidcClientMetadata();
|
||||
return {
|
||||
...baseMetadata,
|
||||
applicationType: "native",
|
||||
// XXX: This should be overridable in config
|
||||
clientUri: "https://element.io",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
|
||||
*/
|
||||
public getOidcCallbackUrl(): URL {
|
||||
const url = super.getOidcCallbackUrl();
|
||||
url.protocol = OIDC_PROTOCOL;
|
||||
url.protocol = "io.element.desktop";
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017-2020 New Vector Ltd
|
||||
Copyright 2017-2020, 2024 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -44,9 +44,41 @@ export default class WebPlatform extends VectorBasePlatform {
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
// Register service worker if available on this platform
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("sw.js");
|
||||
|
||||
// Register the service worker in the background
|
||||
this.tryRegisterServiceWorker().catch((e) => console.error("Error registering/updating service worker:", e));
|
||||
}
|
||||
|
||||
private async tryRegisterServiceWorker(): Promise<void> {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return; // not available on this platform - don't try to register the service worker
|
||||
}
|
||||
|
||||
// sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts`
|
||||
const registration = await navigator.serviceWorker.register("sw.js");
|
||||
if (!registration) {
|
||||
// Registration didn't work for some reason - assume failed and ignore.
|
||||
// This typically happens in Jest.
|
||||
return;
|
||||
}
|
||||
|
||||
await registration.update();
|
||||
navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage.bind(this));
|
||||
}
|
||||
|
||||
private onServiceWorkerPostMessage(event: MessageEvent): void {
|
||||
try {
|
||||
if (event.data?.["type"] === "userinfo" && event.data?.["responseKey"]) {
|
||||
const userId = localStorage.getItem("mx_user_id");
|
||||
const deviceId = localStorage.getItem("mx_device_id");
|
||||
event.source!.postMessage({
|
||||
responseKey: event.data["responseKey"],
|
||||
userId,
|
||||
deviceId,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error responding to service worker: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +113,10 @@ export default class WebPlatform extends VectorBasePlatform {
|
||||
// annoyingly, the latest spec says this returns a
|
||||
// promise, but this is only supported in Chrome 46
|
||||
// and Firefox 47, so adapt the callback API.
|
||||
return new Promise(function (resolve) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
window.Notification.requestPermission((result) => {
|
||||
resolve(result);
|
||||
});
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +148,7 @@ export default class WebPlatform extends VectorBasePlatform {
|
||||
// Ideally, loading an old copy would be impossible with the
|
||||
// cache-control: nocache HTTP header set, but Firefox doesn't always obey it :/
|
||||
console.log("startUpdater, current version is " + getNormalizedAppVersion(WebPlatform.VERSION));
|
||||
this.pollForUpdate((version: string, newVersion: string) => {
|
||||
void this.pollForUpdate((version: string, newVersion: string) => {
|
||||
const query = parseQs(location);
|
||||
if (query.updated) {
|
||||
console.log("Update reloaded but still on an old version, stopping");
|
||||
@@ -175,7 +207,7 @@ export default class WebPlatform extends VectorBasePlatform {
|
||||
|
||||
public startUpdateCheck(): void {
|
||||
super.startUpdateCheck();
|
||||
this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
|
||||
void this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
|
||||
dis.dispatch<CheckUpdatesPayload>({
|
||||
action: Action.CheckUpdates,
|
||||
...updateState,
|
||||
|
||||
@@ -35,7 +35,7 @@ export function initRageshake(): Promise<void> {
|
||||
// we manually check persistence for rageshakes ourselves
|
||||
const prom = rageshake.init(/*setUpPersistence=*/ false);
|
||||
prom.then(
|
||||
() => {
|
||||
async () => {
|
||||
logger.log("Initialised rageshake.");
|
||||
logger.log(
|
||||
"To fix line numbers in Chrome: " +
|
||||
@@ -48,7 +48,7 @@ export function initRageshake(): Promise<void> {
|
||||
rageshake.flush();
|
||||
});
|
||||
|
||||
rageshake.cleanup();
|
||||
await rageshake.cleanup();
|
||||
},
|
||||
(err) => {
|
||||
logger.error("Failed to initialise rageshake: " + err);
|
||||
|
||||
@@ -1,692 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* loading.js: test the myriad paths we have for loading the application */
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent, waitFor, RenderResult, waitForElementToBeRemoved } from "@testing-library/react";
|
||||
import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
|
||||
import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";
|
||||
import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat";
|
||||
import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { QueryDict, sleep } from "matrix-js-sdk/src/utils";
|
||||
import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
|
||||
import { ActionPayload } from "matrix-react-sdk/src/dispatcher/payloads";
|
||||
|
||||
import "../jest-mocks";
|
||||
import WebPlatform from "../../src/vector/platform/WebPlatform";
|
||||
import { parseQs, parseQsFromFragment } from "../../src/vector/url_utils";
|
||||
import { cleanLocalstorage, deleteIndexedDB, waitForLoadingSpinner, waitForWelcomeComponent } from "../test-utils";
|
||||
|
||||
const DEFAULT_HS_URL = "http://my_server";
|
||||
const DEFAULT_IS_URL = "http://my_is";
|
||||
|
||||
/** The matrix versions our mock server claims to support */
|
||||
const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"];
|
||||
|
||||
describe("loading:", function () {
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
// an Object simulating the window.location
|
||||
let windowLocation: Location | undefined;
|
||||
|
||||
// the mounted MatrixChat
|
||||
let matrixChat: RenderResult | undefined;
|
||||
|
||||
// a promise which resolves when the MatrixChat calls onTokenLoginCompleted
|
||||
let tokenLoginCompletePromise: Promise<void> | undefined;
|
||||
|
||||
beforeEach(function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
// @ts-ignore
|
||||
window.fetch = httpBackend.fetchFn;
|
||||
|
||||
windowLocation = undefined;
|
||||
matrixChat = undefined;
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
console.log(`${Date.now()}: loading: afterEach`);
|
||||
matrixChat?.unmount();
|
||||
// unmounting should have cleared the MatrixClientPeg
|
||||
expect(MatrixClientPeg.get()).toBe(null);
|
||||
|
||||
// clear the indexeddbs so we can start from a clean slate next time.
|
||||
await Promise.all([deleteIndexedDB("matrix-js-sdk:crypto"), deleteIndexedDB("matrix-js-sdk:riot-web-sync")]);
|
||||
cleanLocalstorage();
|
||||
console.log(`${Date.now()}: loading: afterEach complete`);
|
||||
});
|
||||
|
||||
/* simulate the load process done by index.js
|
||||
*
|
||||
* TODO: it would be nice to factor some of this stuff out of index.js so
|
||||
* that we can test it rather than our own implementation of it.
|
||||
*/
|
||||
function loadApp(
|
||||
opts: {
|
||||
queryString?: string;
|
||||
uriFragment?: string;
|
||||
config?: IConfigOptions;
|
||||
} = {},
|
||||
): void {
|
||||
const queryString = opts.queryString || "";
|
||||
const uriFragment = opts.uriFragment || "";
|
||||
|
||||
windowLocation = {
|
||||
search: queryString,
|
||||
hash: uriFragment,
|
||||
toString: function (): string {
|
||||
return this.search + this.hash;
|
||||
},
|
||||
} as Location;
|
||||
|
||||
function onNewScreen(screen: string): void {
|
||||
console.log(Date.now() + " newscreen " + screen);
|
||||
const hash = "#/" + screen;
|
||||
windowLocation!.hash = hash;
|
||||
console.log(Date.now() + " browser URI now " + windowLocation);
|
||||
}
|
||||
|
||||
// Parse the given window.location and return parameters that can be used when calling
|
||||
// MatrixChat.showScreen(screen, params)
|
||||
function getScreenFromLocation(location: Location): { screen: string; params: QueryDict } {
|
||||
const fragparts = parseQsFromFragment(location);
|
||||
return {
|
||||
screen: fragparts.location.substring(1),
|
||||
params: fragparts.params,
|
||||
};
|
||||
}
|
||||
|
||||
const fragParts = parseQsFromFragment(windowLocation);
|
||||
|
||||
const config = {
|
||||
default_hs_url: DEFAULT_HS_URL,
|
||||
default_is_url: DEFAULT_IS_URL,
|
||||
validated_server_config: {
|
||||
hsUrl: DEFAULT_HS_URL,
|
||||
hsName: "TEST_ENVIRONMENT",
|
||||
hsNameIsDifferent: false, // yes, we lie
|
||||
isUrl: DEFAULT_IS_URL,
|
||||
} as ValidatedServerConfig,
|
||||
embedded_pages: {
|
||||
home_url: "data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==",
|
||||
},
|
||||
features: {
|
||||
feature_rust_crypto: false,
|
||||
},
|
||||
...(opts.config ?? {}),
|
||||
} as IConfigOptions;
|
||||
|
||||
PlatformPeg.set(new WebPlatform());
|
||||
|
||||
const params = parseQs(windowLocation);
|
||||
|
||||
tokenLoginCompletePromise = new Promise<void>((resolve) => {
|
||||
matrixChat = render(
|
||||
<MatrixChat
|
||||
onNewScreen={onNewScreen}
|
||||
config={config!}
|
||||
realQueryParams={params}
|
||||
startingFragmentQueryParams={fragParts.params}
|
||||
enableGuest={true}
|
||||
onTokenLoginCompleted={resolve}
|
||||
initialScreenAfterLogin={getScreenFromLocation(windowLocation!)}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// set an expectation that we will get a call to /sync, then flush
|
||||
// http requests until we do.
|
||||
//
|
||||
// returns a promise resolving to the received request
|
||||
async function expectAndAwaitSync(opts?: { isGuest?: boolean }): Promise<any> {
|
||||
let syncRequest: (typeof MockHttpBackend.prototype.requests)[number] | null = null;
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
unstable_features: {},
|
||||
});
|
||||
const isGuest = opts?.isGuest;
|
||||
if (!isGuest) {
|
||||
// the call to create the LL filter
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "llfid" });
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
}
|
||||
httpBackend
|
||||
.when("GET", "/sync")
|
||||
.check((r) => {
|
||||
syncRequest = r;
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
for (let attempts = 10; attempts > 0; attempts--) {
|
||||
console.log(Date.now() + " waiting for /sync");
|
||||
if (syncRequest) {
|
||||
return syncRequest;
|
||||
}
|
||||
await httpBackend.flush(undefined);
|
||||
}
|
||||
throw new Error("Gave up waiting for /sync");
|
||||
}
|
||||
|
||||
describe("Clean load with no stored credentials:", function () {
|
||||
it("gives a welcome page by default", function () {
|
||||
loadApp();
|
||||
|
||||
return sleep(1)
|
||||
.then(async () => {
|
||||
// at this point, we're trying to do a guest registration;
|
||||
// we expect a spinner
|
||||
await waitForLoadingSpinner();
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/register")
|
||||
.check(function (req) {
|
||||
expect(req.queryParams?.kind).toEqual("guest");
|
||||
})
|
||||
.respond(403, "Guest access is disabled");
|
||||
|
||||
return httpBackend.flush(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
// Wait for another trip around the event loop for the UI to update
|
||||
return waitForWelcomeComponent(matrixChat);
|
||||
})
|
||||
.then(() => {
|
||||
return waitFor(() => expect(windowLocation?.hash).toEqual("#/welcome"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should follow the original link after successful login", function () {
|
||||
loadApp({
|
||||
uriFragment: "#/room/!room:id",
|
||||
});
|
||||
|
||||
// Pass the liveliness checks
|
||||
httpBackend.when("GET", "/versions").respond(200, { versions: SERVER_SUPPORTED_MATRIX_VERSIONS });
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(200, {});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer")
|
||||
.respond(404, { errcode: "M_UNRECOGNIZED", error: "Unrecognized request" });
|
||||
|
||||
return sleep(1)
|
||||
.then(async () => {
|
||||
// at this point, we're trying to do a guest registration;
|
||||
// we expect a spinner
|
||||
await waitForLoadingSpinner();
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/register")
|
||||
.check(function (req) {
|
||||
expect(req.queryParams?.kind).toEqual("guest");
|
||||
})
|
||||
.respond(403, "Guest access is disabled");
|
||||
|
||||
return httpBackend.flush(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
// Wait for another trip around the event loop for the UI to update
|
||||
return sleep(10);
|
||||
})
|
||||
.then(() => {
|
||||
return moveFromWelcomeToLogin(matrixChat);
|
||||
})
|
||||
.then(() => {
|
||||
return completeLogin(matrixChat!);
|
||||
})
|
||||
.then(() => {
|
||||
// once the sync completes, we should have a room view
|
||||
return awaitRoomView(matrixChat);
|
||||
})
|
||||
.then(() => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
expect(windowLocation?.hash).toEqual("#/room/!room:id");
|
||||
|
||||
// and the localstorage should have been updated
|
||||
expect(localStorage.getItem("mx_user_id")).toEqual("@user:id");
|
||||
expect(localStorage.getItem("mx_access_token")).toEqual("access_token");
|
||||
expect(localStorage.getItem("mx_hs_url")).toEqual(DEFAULT_HS_URL);
|
||||
expect(localStorage.getItem("mx_is_url")).toEqual(DEFAULT_IS_URL);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("should not register as a guest when using a #/login link", function () {
|
||||
loadApp({
|
||||
uriFragment: "#/login",
|
||||
});
|
||||
|
||||
// Pass the liveliness checks
|
||||
httpBackend.when("GET", "/versions").respond(200, { versions: SERVER_SUPPORTED_MATRIX_VERSIONS });
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(200, {});
|
||||
|
||||
return awaitLoginComponent(matrixChat)
|
||||
.then(async () => {
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading..."));
|
||||
// we expect a single <Login> component
|
||||
await screen.findByRole("main");
|
||||
screen.getAllByText("Sign in");
|
||||
|
||||
// the only outstanding request should be a GET /login
|
||||
// (in particular there should be no /register request for
|
||||
// guest registration).
|
||||
const allowedRequests = ["/_matrix/client/v3/login", "/versions", "/_matrix/identity/v2"];
|
||||
for (const req of httpBackend.requests) {
|
||||
if (req.method === "GET" && allowedRequests.find((p) => req.path.endsWith(p))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected HTTP request to ${req}`);
|
||||
}
|
||||
return completeLogin(matrixChat!);
|
||||
})
|
||||
.then(() => {
|
||||
expect(matrixChat?.container.querySelector(".mx_HomePage")).toBeTruthy();
|
||||
expect(windowLocation?.hash).toEqual("#/home");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient rehydrated from stored credentials:", function () {
|
||||
beforeEach(async function () {
|
||||
localStorage.setItem("mx_hs_url", "http://localhost");
|
||||
localStorage.setItem("mx_is_url", "http://localhost");
|
||||
localStorage.setItem("mx_access_token", "access_token");
|
||||
localStorage.setItem("mx_user_id", "@me:localhost");
|
||||
localStorage.setItem("mx_device_id", "QWERTYUIOP");
|
||||
localStorage.setItem("mx_last_room_id", "!last_room:id");
|
||||
|
||||
// Create a crypto store as well to satisfy storage consistency checks
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto");
|
||||
await cryptoStore.startup();
|
||||
});
|
||||
|
||||
it("shows the last known room by default", function () {
|
||||
loadApp();
|
||||
|
||||
return awaitLoggedIn(matrixChat!)
|
||||
.then(() => {
|
||||
// we are logged in - let the sync complete
|
||||
return expectAndAwaitSync();
|
||||
})
|
||||
.then(() => {
|
||||
// once the sync completes, we should have a room view
|
||||
return awaitRoomView(matrixChat);
|
||||
})
|
||||
.then(() => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
expect(windowLocation?.hash).toEqual("#/room/!last_room:id");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a home page by default if we have no joined rooms", function () {
|
||||
localStorage.removeItem("mx_last_room_id");
|
||||
|
||||
loadApp();
|
||||
|
||||
return awaitLoggedIn(matrixChat!)
|
||||
.then(() => {
|
||||
// we are logged in - let the sync complete
|
||||
return expectAndAwaitSync();
|
||||
})
|
||||
.then(() => {
|
||||
// once the sync completes, we should have a home page
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
expect(matrixChat?.container.querySelector(".mx_HomePage")).toBeTruthy();
|
||||
expect(windowLocation?.hash).toEqual("#/home");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a room view if we followed a room link", function () {
|
||||
loadApp({
|
||||
uriFragment: "#/room/!room:id",
|
||||
});
|
||||
|
||||
return awaitLoggedIn(matrixChat!)
|
||||
.then(() => {
|
||||
// we are logged in - let the sync complete
|
||||
return expectAndAwaitSync();
|
||||
})
|
||||
.then(() => {
|
||||
// once the sync completes, we should have a room view
|
||||
return awaitRoomView(matrixChat);
|
||||
})
|
||||
.then(() => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
expect(windowLocation?.hash).toEqual("#/room/!room:id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/#/login link:", function () {
|
||||
beforeEach(function () {
|
||||
loadApp({
|
||||
uriFragment: "#/login",
|
||||
});
|
||||
|
||||
// give the UI a chance to display
|
||||
return expectAndAwaitSync();
|
||||
});
|
||||
|
||||
it("does not show a login view", async function () {
|
||||
await awaitRoomView(matrixChat);
|
||||
|
||||
await screen.getByRole("tree", { name: "Spaces" });
|
||||
expect(screen.queryAllByText("Sign in")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Guest auto-registration:", function () {
|
||||
it("shows a welcome page by default", function () {
|
||||
loadApp();
|
||||
|
||||
return sleep(1)
|
||||
.then(async () => {
|
||||
// at this point, we're trying to do a guest registration;
|
||||
// we expect a spinner
|
||||
await waitForLoadingSpinner();
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/register")
|
||||
.check(function (req) {
|
||||
expect(req.queryParams?.kind).toEqual("guest");
|
||||
})
|
||||
.respond(200, {
|
||||
user_id: "@guest:localhost",
|
||||
device_id: "QWERTYUIOP",
|
||||
access_token: "secret_token",
|
||||
});
|
||||
|
||||
return httpBackend.flush(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
return awaitLoggedIn(matrixChat!);
|
||||
})
|
||||
.then(() => {
|
||||
// we are logged in - let the sync complete
|
||||
return expectAndAwaitSync({ isGuest: true });
|
||||
})
|
||||
.then(() => {
|
||||
// once the sync completes, we should have a welcome page
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
expect(matrixChat?.container.querySelector(".mx_Welcome")).toBeTruthy();
|
||||
expect(windowLocation?.hash).toEqual("#/welcome");
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default homeserver to register with", function () {
|
||||
loadApp();
|
||||
|
||||
return sleep(1)
|
||||
.then(async () => {
|
||||
// at this point, we're trying to do a guest registration;
|
||||
// we expect a spinner
|
||||
await waitForLoadingSpinner();
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/register")
|
||||
.check(function (req) {
|
||||
expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true);
|
||||
expect(req.queryParams?.kind).toEqual("guest");
|
||||
})
|
||||
.respond(200, {
|
||||
user_id: "@guest:localhost",
|
||||
access_token: "secret_token",
|
||||
});
|
||||
|
||||
return httpBackend.flush(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
return awaitLoggedIn(matrixChat!);
|
||||
})
|
||||
.then(() => {
|
||||
return expectAndAwaitSync({ isGuest: true });
|
||||
})
|
||||
.then((req) => {
|
||||
expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true);
|
||||
|
||||
// once the sync completes, we should have a welcome page
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
expect(matrixChat?.container.querySelector(".mx_Welcome")).toBeTruthy();
|
||||
expect(windowLocation?.hash).toEqual("#/welcome");
|
||||
expect(MatrixClientPeg.safeGet().baseUrl).toEqual(DEFAULT_HS_URL);
|
||||
expect(MatrixClientPeg.safeGet().idBaseUrl).toEqual(DEFAULT_IS_URL);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a room view if we followed a room link", function () {
|
||||
loadApp({
|
||||
uriFragment: "#/room/!room:id",
|
||||
});
|
||||
return sleep(1)
|
||||
.then(async () => {
|
||||
// at this point, we're trying to do a guest registration;
|
||||
// we expect a spinner
|
||||
await waitForLoadingSpinner();
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/register")
|
||||
.check(function (req) {
|
||||
expect(req.queryParams?.kind).toEqual("guest");
|
||||
})
|
||||
.respond(200, {
|
||||
user_id: "@guest:localhost",
|
||||
access_token: "secret_token",
|
||||
});
|
||||
|
||||
return httpBackend.flush(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
return awaitLoggedIn(matrixChat!);
|
||||
})
|
||||
.then(() => {
|
||||
return expectAndAwaitSync({ isGuest: true });
|
||||
})
|
||||
.then(() => {
|
||||
// once the sync completes, we should have a room view
|
||||
return awaitRoomView(matrixChat);
|
||||
})
|
||||
.then(() => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
expect(windowLocation?.hash).toEqual("#/room/!room:id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Login as user", function () {
|
||||
beforeEach(function () {
|
||||
// first we have to load the homepage
|
||||
loadApp();
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/register")
|
||||
.check(function (req) {
|
||||
expect(req.queryParams?.kind).toEqual("guest");
|
||||
})
|
||||
.respond(200, {
|
||||
user_id: "@guest:localhost",
|
||||
access_token: "secret_token",
|
||||
});
|
||||
|
||||
return httpBackend
|
||||
.flush(undefined)
|
||||
.then(() => {
|
||||
return awaitLoggedIn(matrixChat!);
|
||||
})
|
||||
.then(() => {
|
||||
// we got a sync spinner - let the sync complete
|
||||
return expectAndAwaitSync();
|
||||
})
|
||||
.then(async () => {
|
||||
// once the sync completes, we should have a home page
|
||||
await waitFor(() => matrixChat?.container.querySelector(".mx_HomePage"));
|
||||
|
||||
// we simulate a click on the 'login' button by firing off
|
||||
// the relevant dispatch.
|
||||
//
|
||||
// XXX: is it an anti-pattern to access the react-sdk's
|
||||
// dispatcher in this way? Is it better to find the login
|
||||
// button and simulate a click? (we might have to arrange
|
||||
// for it to be shown - it's not always, due to the
|
||||
// collapsing left panel
|
||||
|
||||
dis.dispatch({ action: "start_login" });
|
||||
|
||||
return awaitLoginComponent(matrixChat);
|
||||
});
|
||||
});
|
||||
|
||||
it("should give us a login page", async function () {
|
||||
// we expect a single <Login> component
|
||||
await screen.findByRole("main");
|
||||
screen.getAllByText("Sign in");
|
||||
|
||||
expect(windowLocation?.hash).toEqual("#/login");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Token login:", function () {
|
||||
it("logs in successfully", function () {
|
||||
localStorage.setItem("mx_sso_hs_url", "https://homeserver");
|
||||
localStorage.setItem("mx_sso_is_url", "https://idserver");
|
||||
loadApp({
|
||||
queryString: "?loginToken=secretToken",
|
||||
});
|
||||
|
||||
return sleep(1)
|
||||
.then(async () => {
|
||||
// we expect a spinner while we're logging in
|
||||
await waitForLoadingSpinner();
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/login")
|
||||
.check(function (req) {
|
||||
expect(req.path).toMatch(new RegExp("^https://homeserver/"));
|
||||
expect(req.data.type).toEqual("m.login.token");
|
||||
expect(req.data.token).toEqual("secretToken");
|
||||
})
|
||||
.respond(200, {
|
||||
user_id: "@user:localhost",
|
||||
device_id: "DEVICE_ID",
|
||||
access_token: "access_token",
|
||||
});
|
||||
|
||||
return httpBackend.flush(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
// at this point, MatrixChat should fire onTokenLoginCompleted, which
|
||||
// makes index.js reload the app. We're not going to attempt to
|
||||
// simulate the reload - just check that things are left in the
|
||||
// right state for the reloaded app.
|
||||
|
||||
return tokenLoginCompletePromise;
|
||||
})
|
||||
.then(() => {
|
||||
return expectAndAwaitSync().catch((e) => {
|
||||
throw new Error("Never got /sync after login: did the client start?");
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// check that the localstorage has been set up in such a way that
|
||||
// the reloaded app can pick up where we leave off.
|
||||
expect(localStorage.getItem("mx_user_id")).toEqual("@user:localhost");
|
||||
expect(localStorage.getItem("mx_access_token")).toEqual("access_token");
|
||||
expect(localStorage.getItem("mx_hs_url")).toEqual("https://homeserver");
|
||||
expect(localStorage.getItem("mx_is_url")).toEqual("https://idserver");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// check that we have a Login component, send a 'user:pass' login,
|
||||
// and await the HTTP requests.
|
||||
async function completeLogin(matrixChat: RenderResult): Promise<void> {
|
||||
// When we switch to the login component, it'll hit the login endpoint
|
||||
// for proof of life and to get flows. We'll only give it one option.
|
||||
httpBackend.when("GET", "/login").respond(200, { flows: [{ type: "m.login.password" }] });
|
||||
httpBackend.flush(undefined); // We already would have tried the GET /login request
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/login")
|
||||
.check(function (req) {
|
||||
expect(req.data.type).toEqual("m.login.password");
|
||||
expect(req.data.identifier.type).toEqual("m.id.user");
|
||||
expect(req.data.identifier.user).toEqual("user");
|
||||
expect(req.data.password).toEqual("pass");
|
||||
})
|
||||
.respond(200, {
|
||||
user_id: "@user:id",
|
||||
device_id: "DEVICE_ID",
|
||||
access_token: "access_token",
|
||||
});
|
||||
|
||||
// Give the component some time to finish processing the login flows before continuing.
|
||||
await waitFor(() => expect(matrixChat?.container.querySelector("#mx_LoginForm_username")).toBeTruthy());
|
||||
|
||||
// Enter login details
|
||||
fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_username")!, { target: { value: "user" } });
|
||||
fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_password")!, { target: { value: "pass" } });
|
||||
fireEvent.click(screen.getByText("Sign in", { selector: ".mx_Login_submit" }));
|
||||
|
||||
return httpBackend
|
||||
.flush(undefined)
|
||||
.then(() => {
|
||||
// Wait for another trip around the event loop for the UI to update
|
||||
return sleep(1);
|
||||
})
|
||||
.then(() => {
|
||||
return expectAndAwaitSync().catch((e) => {
|
||||
throw new Error("Never got /sync after login: did the client start?");
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function awaitLoggedIn(matrixChat: RenderResult): Promise<void> {
|
||||
if (matrixChat.container.querySelector(".mx_MatrixChat_wrapper")) return; // already logged in
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const onAction = ({ action }: ActionPayload): void => {
|
||||
if (action !== "on_logged_in") {
|
||||
return;
|
||||
}
|
||||
console.log(Date.now() + ": Received on_logged_in action");
|
||||
dis.unregister(dispatcherRef);
|
||||
resolve(sleep(1));
|
||||
};
|
||||
const dispatcherRef = dis.register(onAction);
|
||||
console.log(Date.now() + ": Waiting for on_logged_in action");
|
||||
});
|
||||
}
|
||||
|
||||
async function awaitRoomView(matrixChat?: RenderResult): Promise<void> {
|
||||
await waitFor(() => matrixChat?.container.querySelector(".mx_RoomView"));
|
||||
}
|
||||
|
||||
async function awaitLoginComponent(matrixChat?: RenderResult): Promise<void> {
|
||||
await waitFor(() => matrixChat?.container.querySelector(".mx_AuthPage"));
|
||||
}
|
||||
|
||||
function moveFromWelcomeToLogin(matrixChat?: RenderResult): Promise<void> {
|
||||
dis.dispatch({ action: "start_login" });
|
||||
return awaitLoginComponent(matrixChat);
|
||||
}
|
||||
@@ -16,40 +16,6 @@ limitations under the License.
|
||||
|
||||
import { RenderResult, screen, waitFor } from "@testing-library/react";
|
||||
|
||||
export function cleanLocalstorage(): void {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
export function deleteIndexedDB(dbName: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!window.indexedDB) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
console.log(`${startTime}: Removing indexeddb instance: ${dbName}`);
|
||||
const req = window.indexedDB.deleteDatabase(dbName);
|
||||
|
||||
req.onblocked = (): void => {
|
||||
console.log(`${Date.now()}: can't yet delete indexeddb ${dbName} because it is open elsewhere`);
|
||||
};
|
||||
|
||||
req.onerror = (ev): void => {
|
||||
reject(new Error(`${Date.now()}: unable to delete indexeddb ${dbName}: ${req.error?.message}`));
|
||||
};
|
||||
|
||||
req.onsuccess = (): void => {
|
||||
const now = Date.now();
|
||||
console.log(`${now}: Removed indexeddb instance: ${dbName} in ${now - startTime} ms`);
|
||||
resolve();
|
||||
};
|
||||
}).catch((e) => {
|
||||
console.error(`${Date.now()}: Error removing indexeddb instance ${dbName}: ${e}`);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
// wait for loading page
|
||||
export async function waitForLoadingSpinner(): Promise<void> {
|
||||
await screen.findByRole("progressbar");
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
"esModuleInterop": true,
|
||||
"module": "es2022",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2016",
|
||||
"target": "es2018",
|
||||
"noUnusedLocals": true,
|
||||
"sourceMap": false,
|
||||
"outDir": "./lib",
|
||||
"declaration": true,
|
||||
"jsx": "react",
|
||||
"lib": ["es2021", "dom", "dom.iterable"],
|
||||
"lib": ["es2022", "dom", "dom.iterable"],
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -153,6 +153,10 @@ module.exports = (env, argv) => {
|
||||
mobileguide: "./src/vector/mobile_guide/index.ts",
|
||||
jitsi: "./src/vector/jitsi/index.ts",
|
||||
usercontent: "./node_modules/matrix-react-sdk/src/usercontent/index.ts",
|
||||
serviceworker: {
|
||||
import: "./src/serviceworker/index.ts",
|
||||
filename: "sw.js", // update WebPlatform if this changes
|
||||
},
|
||||
...(useHMR ? {} : cssThemes),
|
||||
},
|
||||
|
||||
@@ -270,10 +274,6 @@ module.exports = (env, argv) => {
|
||||
// there is no need for webpack to parse them - they can just be
|
||||
// included as-is.
|
||||
/highlight\.js[\\/]lib[\\/]languages/,
|
||||
|
||||
// olm takes ages for webpack to process, and it's already heavily
|
||||
// optimised, so there is little to gain by us uglifying it.
|
||||
/olm[\\/](javascript[\\/])?olm\.js$/,
|
||||
],
|
||||
rules: [
|
||||
useHMR && {
|
||||
@@ -439,20 +439,6 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// the olm library wants to load its own wasm, rather than have webpack do it.
|
||||
// We therefore use the `file-loader` to tell webpack to dump the contents to
|
||||
// a separate file and return the name, and override the default `type` for `.wasm` files
|
||||
// (which is `webassembly/experimental` under webpack 4) to stop webpack trying to interpret
|
||||
// the filename as webassembly. (see also https://github.com/webpack/webpack/issues/6725)
|
||||
test: /olm\.wasm$/,
|
||||
loader: "file-loader",
|
||||
type: "javascript/auto",
|
||||
options: {
|
||||
name: "[name].[hash:7].[ext]",
|
||||
outputPath: ".",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Fix up the name of the opus-recorder worker (react-sdk dependency).
|
||||
// We more or less just want it to be clear it's for opus and not something else.
|
||||
@@ -494,8 +480,11 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
// Same deal as olm.wasm: the decoderWorker wants to load the wasm artifact
|
||||
// itself.
|
||||
// The decoderWorker wants to load its own wasm, rather than have webpack do it.
|
||||
// We therefore use the `file-loader` to tell webpack to dump the contents to
|
||||
// a separate file and return the name, and override the default `type` for `.wasm` files
|
||||
// (which is `webassembly/experimental` under webpack 4) to stop webpack trying to interpret
|
||||
// the filename as webassembly. (see also https://github.com/webpack/webpack/issues/6725)
|
||||
test: /decoderWorker\.min\.wasm$/,
|
||||
loader: "file-loader",
|
||||
type: "javascript/auto",
|
||||
@@ -653,8 +642,8 @@ module.exports = (env, argv) => {
|
||||
|
||||
// This exports our CSS using the splitChunks and loaders above.
|
||||
new MiniCssExtractPlugin({
|
||||
filename: useHMR ? "bundles/[name].css" : "bundles/[hash]/[name].css",
|
||||
chunkFilename: useHMR ? "bundles/[name].css" : "bundles/[hash]/[name].css",
|
||||
filename: useHMR ? "bundles/[name].css" : "bundles/[fullhash]/[name].css",
|
||||
chunkFilename: useHMR ? "bundles/[name].css" : "bundles/[fullhash]/[name].css",
|
||||
ignoreOrder: false, // Enable to remove warnings about conflicting order
|
||||
}),
|
||||
|
||||
@@ -666,7 +655,7 @@ module.exports = (env, argv) => {
|
||||
// HtmlWebpackPlugin will screw up our formatting like the names
|
||||
// of the themes and which chunks we actually care about.
|
||||
inject: false,
|
||||
excludeChunks: ["mobileguide", "usercontent", "jitsi"],
|
||||
excludeChunks: ["mobileguide", "usercontent", "jitsi", "serviceworker"],
|
||||
minify: false,
|
||||
templateParameters: {
|
||||
og_image_url: ogImageUrl,
|
||||
@@ -736,17 +725,16 @@ module.exports = (env, argv) => {
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
"res/apple-app-site-association",
|
||||
{ from: ".well-known/**", context: path.resolve(__dirname, "res") },
|
||||
"res/jitsi_external_api.min.js",
|
||||
"res/jitsi_external_api.min.js.LICENSE.txt",
|
||||
"res/manifest.json",
|
||||
"res/sw.js",
|
||||
"res/welcome.html",
|
||||
{ from: "welcome/**", context: path.resolve(__dirname, "res") },
|
||||
{ from: "themes/**", context: path.resolve(__dirname, "res") },
|
||||
{ from: "vector-icons/**", context: path.resolve(__dirname, "res") },
|
||||
{ from: "decoder-ring/**", context: path.resolve(__dirname, "res") },
|
||||
{ from: "media/**", context: path.resolve(__dirname, "node_modules/matrix-react-sdk/res/") },
|
||||
"node_modules/@matrix-org/olm/olm_legacy.js",
|
||||
{ from: "config.json", noErrorOnMissing: true },
|
||||
"contribute.json",
|
||||
],
|
||||
@@ -770,9 +758,9 @@ module.exports = (env, argv) => {
|
||||
// directory and symlink it into place - this allows users who loaded
|
||||
// an older version of the application to continue to access webpack
|
||||
// chunks even after the app is redeployed.
|
||||
filename: "bundles/[hash]/[name].js",
|
||||
chunkFilename: "bundles/[hash]/[name].js",
|
||||
webassemblyModuleFilename: "bundles/[hash]/[modulehash].wasm",
|
||||
filename: "bundles/[fullhash]/[name].js",
|
||||
chunkFilename: "bundles/[fullhash]/[name].js",
|
||||
webassemblyModuleFilename: "bundles/[fullhash]/[modulehash].wasm",
|
||||
},
|
||||
|
||||
// configuration for the webpack-dev-server
|
||||
|
||||
Reference in New Issue
Block a user