mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-15 02:00:24 +00:00
Compare commits
2 Commits
hs/fix-key
...
robin/reve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d32efb27 | ||
|
|
f05df80b46 |
@@ -7,7 +7,3 @@ test/end-to-end-tests/lib/
|
|||||||
src/component-index.js
|
src/component-index.js
|
||||||
# Auto-generated file
|
# Auto-generated file
|
||||||
src/modules.ts
|
src/modules.ts
|
||||||
src/modules.js
|
|
||||||
# Test result files
|
|
||||||
/playwright/test-results/
|
|
||||||
/playwright/html-report/
|
|
||||||
|
|||||||
26
.eslintrc.js
26
.eslintrc.js
@@ -1,11 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||||
extends: [
|
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||||
"plugin:matrix-org/babel",
|
|
||||||
"plugin:matrix-org/react",
|
|
||||||
"plugin:matrix-org/a11y",
|
|
||||||
"plugin:storybook/recommended",
|
|
||||||
],
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["./tsconfig.json"],
|
project: ["./tsconfig.json"],
|
||||||
},
|
},
|
||||||
@@ -35,10 +30,6 @@ module.exports = {
|
|||||||
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||||
"Use UIStore to access window dimensions instead.",
|
"Use UIStore to access window dimensions instead.",
|
||||||
),
|
),
|
||||||
...buildRestrictedPropertiesOptions(
|
|
||||||
["React.forwardRef", "*.forwardRef", "forwardRef"],
|
|
||||||
"Use ref props instead.",
|
|
||||||
),
|
|
||||||
...buildRestrictedPropertiesOptions(
|
...buildRestrictedPropertiesOptions(
|
||||||
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
||||||
"Use Media helper instead to centralise access for customisation.",
|
"Use Media helper instead to centralise access for customisation.",
|
||||||
@@ -64,11 +55,6 @@ module.exports = {
|
|||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
paths: [
|
paths: [
|
||||||
{
|
|
||||||
name: "react",
|
|
||||||
importNames: ["forwardRef"],
|
|
||||||
message: "Use ref props instead.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "@testing-library/react",
|
name: "@testing-library/react",
|
||||||
message: "Please use jest-matrix-react instead",
|
message: "Please use jest-matrix-react instead",
|
||||||
@@ -214,13 +200,8 @@ module.exports = {
|
|||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
// We're okay with assertion errors when we ask for them
|
// We're okay with assertion errors when we ask for them
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
"@typescript-eslint/no-empty-object-type": [
|
// We do this sometimes to brand interfaces
|
||||||
"error",
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
{
|
|
||||||
// We do this sometimes to brand interfaces
|
|
||||||
allowInterfaces: "with-single-extends",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// temporary override for offending icon require files
|
// temporary override for offending icon require files
|
||||||
@@ -266,7 +247,6 @@ module.exports = {
|
|||||||
// We don't need super strict typing in test utilities
|
// We don't need super strict typing in test utilities
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||||
"@typescript-eslint/no-empty-object-type": "off",
|
|
||||||
|
|
||||||
// Jest/Playwright specific
|
// Jest/Playwright specific
|
||||||
|
|
||||||
|
|||||||
11
.github/CODEOWNERS
vendored
11
.github/CODEOWNERS
vendored
@@ -10,17 +10,14 @@
|
|||||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||||
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||||
/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||||
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
|
||||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
|
||||||
|
|
||||||
# Ignore translations as those will be updated by GHA for Localazy download
|
# Ignore translations as those will be updated by GHA for Localazy download
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||||
# Ignore the synapse & mas plugins as this is updated by GHA for docker image updating
|
# Ignore the synapse plugin as this is updated by GHA for docker image updating
|
||||||
/playwright/testcontainers/synapse.ts
|
/playwright/testcontainers/synapse.ts
|
||||||
/playwright/testcontainers/mas.ts
|
|
||||||
|
|
||||||
|
|||||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
||||||
- [ ] Tests written for new code (and old code if feasible).
|
- [ ] Tests written for new code (and old code if feasible).
|
||||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||||
- [ ] Linter and other CI checks pass.
|
- [ ] Linter and other CI checks pass.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Download release tarball
|
- name: Download release tarball
|
||||||
uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1
|
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
||||||
with:
|
with:
|
||||||
tag: ${{ inputs.tag }}
|
tag: ${{ inputs.tag }}
|
||||||
fileName: element-*.tar.gz*
|
fileName: element-*.tar.gz*
|
||||||
|
|||||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -43,9 +43,9 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
# Disable cache on Windows as it is slower than not caching
|
# Disable cache on Windows as it is slower than not caching
|
||||||
# https://github.com/actions/setup-node/issues/975
|
# https://github.com/actions/setup-node/issues/975
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: webapp-${{ matrix.image }}
|
name: webapp-${{ matrix.image }}
|
||||||
path: webapp
|
path: webapp
|
||||||
|
|||||||
4
.github/workflows/build_debian.yaml
vendored
4
.github/workflows/build_debian.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
VERSION: ${{ github.ref_name }}
|
VERSION: ${{ github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download package
|
- name: Download package
|
||||||
run: |
|
run: |
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
|
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
|
||||||
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
|
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
|
||||||
|
|
||||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: element-web.deb
|
name: element-web.deb
|
||||||
path: element-web.deb
|
path: element-web.deb
|
||||||
|
|||||||
11
.github/workflows/build_develop.yml
vendored
11
.github/workflows/build_develop.yml
vendored
@@ -26,9 +26,9 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
|
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
|
||||||
|
|
||||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
path: dist/develop.tar.gz
|
path: dist/develop.tar.gz
|
||||||
@@ -109,11 +109,10 @@ jobs:
|
|||||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
# 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.
|
# as the expires after 24h and requires auth to download.
|
||||||
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
||||||
# Checksum algorithm specified as per https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
|
||||||
- name: Deploy to R2
|
- name: Deploy to R2
|
||||||
run: |
|
run: |
|
||||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
|
||||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Load GPG key
|
- name: Load GPG key
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
154
.github/workflows/docker.yaml
vendored
154
.github/workflows/docker.yaml
vendored
@@ -1,154 +0,0 @@
|
|||||||
name: Docker
|
|
||||||
on:
|
|
||||||
workflow_dispatch: {}
|
|
||||||
push:
|
|
||||||
tags: [v*]
|
|
||||||
pull_request: {}
|
|
||||||
schedule:
|
|
||||||
# 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: {}
|
|
||||||
jobs:
|
|
||||||
buildx:
|
|
||||||
name: Docker Buildx
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }}
|
|
||||||
permissions:
|
|
||||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
|
||||||
packages: write # needed for publishing packages to GHCR
|
|
||||||
env:
|
|
||||||
TEST_TAG: vectorim/element-web:test
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
|
||||||
|
|
||||||
- name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
|
||||||
with:
|
|
||||||
install: true
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and load
|
|
||||||
id: test-build
|
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
load: true
|
|
||||||
|
|
||||||
- name: Test the image
|
|
||||||
env:
|
|
||||||
IMAGEID: ${{ steps.test-build.outputs.imageid }}
|
|
||||||
timeout-minutes: 2
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
|
|
||||||
# Make a fake module to test the image
|
|
||||||
MODULE_PATH="modules/module_name/index.js"
|
|
||||||
mkdir -p $(dirname $MODULE_PATH)
|
|
||||||
echo 'alert("Testing");' > $MODULE_PATH
|
|
||||||
|
|
||||||
# Spin up a container of the image
|
|
||||||
ELEMENT_WEB_PORT=8181
|
|
||||||
CONTAINER_ID=$(
|
|
||||||
docker run \
|
|
||||||
--rm \
|
|
||||||
-e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \
|
|
||||||
-dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \
|
|
||||||
-v $(pwd)/modules:/modules \
|
|
||||||
"$IMAGEID" \
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run some smoke tests
|
|
||||||
wget --retry-connrefused --tries=5 -q --wait=3 --spider "http://localhost:$ELEMENT_WEB_PORT/modules/module_name/index.js"
|
|
||||||
MODULE_0=$(curl "http://localhost:$ELEMENT_WEB_PORT/config.json" | jq -r .modules[0])
|
|
||||||
test "$MODULE_0" = "/${MODULE_PATH}"
|
|
||||||
|
|
||||||
# Check healthcheck
|
|
||||||
until test "$(docker inspect -f {{.State.Health.Status}} $CONTAINER_ID)" == "healthy"; do
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
docker stop "$CONTAINER_ID"
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
vectorim/element-web
|
|
||||||
ghcr.io/element-hq/element-web
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=tag
|
|
||||||
flavor: |
|
|
||||||
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: build-and-push
|
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
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 }}
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
run: |
|
|
||||||
images=""
|
|
||||||
for tag in ${TAGS}; do
|
|
||||||
images+="${tag}@${DIGEST} "
|
|
||||||
done
|
|
||||||
cosign sign --yes ${images}
|
|
||||||
|
|
||||||
- name: Update repo description
|
|
||||||
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
repository: vectorim/element-web
|
|
||||||
|
|
||||||
- name: Repository Dispatch
|
|
||||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
repository: element-hq/element-web-pro
|
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
|
||||||
event-type: image-built
|
|
||||||
# Stable way to determine the :version
|
|
||||||
client-payload: |-
|
|
||||||
{
|
|
||||||
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
|
|
||||||
}
|
|
||||||
79
.github/workflows/dockerhub.yaml
vendored
Normal file
79
.github/workflows/dockerhub.yaml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: Dockerhub
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
push:
|
||||||
|
tags: [v*]
|
||||||
|
schedule:
|
||||||
|
# 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: {}
|
||||||
|
jobs:
|
||||||
|
buildx:
|
||||||
|
name: Docker Buildx
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
environment: dockerhub
|
||||||
|
permissions:
|
||||||
|
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||||
|
|
||||||
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
vectorim/element-web
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
flavor: |
|
||||||
|
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build-and-push
|
||||||
|
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
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
|
||||||
|
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
repository: vectorim/element-web
|
||||||
14
.github/workflows/docs.yml
vendored
14
.github/workflows/docs.yml
vendored
@@ -17,23 +17,23 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Fetch element-desktop
|
- name: Fetch element-desktop
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-desktop
|
repository: element-hq/element-desktop
|
||||||
path: element-desktop
|
path: element-desktop
|
||||||
|
|
||||||
- name: Fetch element-web
|
- name: Fetch element-web
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: element-web
|
path: element-web
|
||||||
|
|
||||||
- name: Fetch matrix-js-sdk
|
- name: Fetch matrix-js-sdk
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
path: matrix-js-sdk
|
path: matrix-js-sdk
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
cache-dependency-path: element-web/yarn.lock
|
cache-dependency-path: element-web/yarn.lock
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
|
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
|
||||||
|
|
||||||
- name: Setup mdBook
|
- name: Setup mdBook
|
||||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
|
uses: peaceiris/actions-mdbook@v2
|
||||||
with:
|
with:
|
||||||
mdbook-version: "0.4.10"
|
mdbook-version: "0.4.10"
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
run: mdbook build
|
run: mdbook build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: ./book
|
path: ./book
|
||||||
|
|
||||||
@@ -104,4 +104,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Download HTML report
|
- name: Download HTML report
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
path: playwright-report
|
path: playwright-report
|
||||||
|
|
||||||
- name: 📤 Deploy to Netlify
|
- name: 📤 Deploy to Netlify
|
||||||
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
uses: matrix-org/netlify-pr-preview@v3
|
||||||
with:
|
with:
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||||
|
|||||||
47
.github/workflows/end-to-end-tests.yaml
vendored
47
.github/workflows/end-to-end-tests.yaml
vendored
@@ -50,11 +50,11 @@ jobs:
|
|||||||
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
path: webapp
|
path: webapp
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Calculate runner variables
|
- name: Calculate runner variables
|
||||||
id: runner-vars
|
id: runner-vars
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
|
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
|
||||||
@@ -129,18 +129,18 @@ jobs:
|
|||||||
- runAllTests: false
|
- runAllTests: false
|
||||||
project: Pinecone
|
project: Pinecone
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
path: webapp
|
path: webapp
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
@@ -154,11 +154,12 @@ jobs:
|
|||||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
uses: actions/cache@v4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/ms-playwright
|
path: |
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}
|
~/.cache/ms-playwright
|
||||||
|
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
@@ -179,35 +180,25 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
|
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
|
||||||
path: blob-report
|
path: blob-report
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
downstream-modules:
|
|
||||||
name: Downstream Playwright tests [element-modules]
|
|
||||||
needs: build
|
|
||||||
if: inputs.skip != true && github.event_name == 'merge_group'
|
|
||||||
uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main
|
|
||||||
with:
|
|
||||||
webapp-artifact: webapp
|
|
||||||
|
|
||||||
complete:
|
complete:
|
||||||
name: end-to-end-tests
|
name: end-to-end-tests
|
||||||
needs:
|
needs: playwright
|
||||||
- playwright
|
|
||||||
- downstream-modules
|
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -219,7 +210,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: all-blob-reports-*
|
pattern: all-blob-reports-*
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
@@ -227,7 +218,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Merge into HTML Report
|
- name: Merge into HTML Report
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports
|
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,./playwright/stale-screenshot-reporter.ts ./all-blob-reports
|
||||||
env:
|
env:
|
||||||
# Only pass creds to the flaky-reporter on main branch runs
|
# Only pass creds to the flaky-reporter on main branch runs
|
||||||
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
|
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
|
||||||
@@ -235,11 +226,11 @@ jobs:
|
|||||||
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
|
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
|
||||||
- name: Upload HTML report
|
- name: Upload HTML report
|
||||||
if: always() && inputs.skip != true
|
if: always() && inputs.skip != true
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: html-report
|
name: html-report
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
- if: needs.playwright.result != 'skipped' && needs.playwright.result != 'success'
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
4
.github/workflows/issue_closed.yml
vendored
4
.github/workflows/issue_closed.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
name: Tidy closed issues
|
name: Tidy closed issues
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
id: main
|
id: main
|
||||||
with:
|
with:
|
||||||
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
|
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
|
||||||
@@ -142,7 +142,7 @@ jobs:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
name: Close duplicate as Not Planned
|
name: Close duplicate as Not Planned
|
||||||
if: steps.main.outputs.closeAsNotPlanned
|
if: steps.main.outputs.closeAsNotPlanned
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/netlify.yaml
vendored
4
.github/workflows/netlify.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
Exercise caution. Use test accounts.
|
Exercise caution. Use test accounts.
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
path: webapp
|
path: webapp
|
||||||
|
|
||||||
- name: 📤 Deploy to Netlify
|
- name: 📤 Deploy to Netlify
|
||||||
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
uses: matrix-org/netlify-pr-preview@v3
|
||||||
with:
|
with:
|
||||||
path: webapp
|
path: webapp
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||||
|
|||||||
2
.github/workflows/pending-reviews.yaml
vendored
2
.github/workflows/pending-reviews.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
|
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
|
||||||
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
|
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
env:
|
env:
|
||||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||||
ROOM_ID: ${{ secrets.ROOM_ID }}
|
ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||||
|
|||||||
13
.github/workflows/playwright-image-updates.yaml
vendored
13
.github/workflows/playwright-image-updates.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Update synapse image
|
- name: Update synapse image
|
||||||
run: |
|
run: |
|
||||||
@@ -21,18 +21,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IMAGE: ghcr.io/element-hq/synapse:develop
|
IMAGE: ghcr.io/element-hq/synapse:develop
|
||||||
|
|
||||||
- name: Update MAS image
|
|
||||||
run: |
|
|
||||||
docker pull "$IMAGE"
|
|
||||||
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
|
|
||||||
DIGEST=${INSPECT#*@}
|
|
||||||
sed -i "s/const TAG.*/const TAG = \"main@$DIGEST\";/" playwright/testcontainers/mas.ts
|
|
||||||
env:
|
|
||||||
IMAGE: ghcr.io/element-hq/matrix-authentication-service:main
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
branch: actions/playwright-image-updates
|
branch: actions/playwright-image-updates
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
name: Check PR base branch
|
name: Check PR base branch
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const baseBranch = context.payload.pull_request.base.ref;
|
const baseBranch = context.payload.pull_request.base.ref;
|
||||||
|
|||||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -19,7 +19,6 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
id-token: write
|
|
||||||
secrets:
|
secrets:
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
@@ -51,7 +50,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
checks: read
|
checks: read
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for docker build
|
- name: Wait for dockerhub
|
||||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|||||||
12
.github/workflows/release_prepare.yml
vendored
12
.github/workflows/release_prepare.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
REPOS: matrix-js-sdk element-web element-desktop
|
REPOS: matrix-js-sdk element-web element-desktop
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Element Desktop
|
- name: Checkout Element Desktop
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
if: inputs.element-desktop
|
if: inputs.element-desktop
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-desktop
|
repository: element-hq/element-desktop
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
- name: Checkout Element Web
|
- name: Checkout Element Web
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
if: inputs.element-web
|
if: inputs.element-web
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
- name: Checkout Matrix JS SDK
|
- name: Checkout Matrix JS SDK
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
if: inputs.matrix-js-sdk
|
if: inputs.matrix-js-sdk
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
repo: matrix-org/matrix-js-sdk
|
repo: matrix-org/matrix-js-sdk
|
||||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
wait-interval: 10
|
wait-interval: 10
|
||||||
check-name: "draft / draft"
|
check-name: draft
|
||||||
allowed-conclusions: success
|
allowed-conclusions: success
|
||||||
|
|
||||||
- name: Wait for element-web draft
|
- name: Wait for element-web draft
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
repo: element-hq/element-web
|
repo: element-hq/element-web
|
||||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
wait-interval: 10
|
wait-interval: 10
|
||||||
check-name: "draft / draft"
|
check-name: draft
|
||||||
allowed-conclusions: success
|
allowed-conclusions: success
|
||||||
|
|
||||||
- name: Wait for element-desktop draft
|
- name: Wait for element-desktop draft
|
||||||
@@ -122,5 +122,5 @@ jobs:
|
|||||||
repo: element-hq/element-desktop
|
repo: element-hq/element-desktop
|
||||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
wait-interval: 10
|
wait-interval: 10
|
||||||
check-name: "draft / draft"
|
check-name: draft
|
||||||
allowed-conclusions: success
|
allowed-conclusions: success
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# Triggers after the shared component tests have finished,
|
|
||||||
# It uploads the received images and diffs to netlify, printing the URLs to the console
|
|
||||||
name: Upload Shared Component Visual Test Diffs
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Shared Component Visual Tests"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
|
||||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
report:
|
|
||||||
if: github.event.workflow_run.conclusion == 'failure'
|
|
||||||
name: Upload Diffs
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
environment: Netlify
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
deployments: write
|
|
||||||
steps:
|
|
||||||
- name: Install tree
|
|
||||||
run: "sudo apt-get install -y tree"
|
|
||||||
|
|
||||||
- name: Download Diffs
|
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
name: received-images
|
|
||||||
path: received-images
|
|
||||||
|
|
||||||
- name: Generate Index
|
|
||||||
run: "cd received-images && tree -L 1 --noreport -H '' -o index.html ."
|
|
||||||
|
|
||||||
- name: 📤 Deploy to Netlify
|
|
||||||
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
|
||||||
with:
|
|
||||||
path: received-images
|
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
|
||||||
branch: ${{ github.event.workflow_run.head_branch }}
|
|
||||||
revision: ${{ github.event.workflow_run.head_sha }}
|
|
||||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site_id: ${{ vars.NETLIFY_SITE_ID }}
|
|
||||||
desc: Shared Component Visual Diffs
|
|
||||||
deployment_env: SharedComponentDiffs
|
|
||||||
prefix: "diffs-"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
name: Shared Component Visual Tests
|
|
||||||
on:
|
|
||||||
pull_request: {}
|
|
||||||
merge_group:
|
|
||||||
types: [checks_requested]
|
|
||||||
push:
|
|
||||||
branches: [develop, master]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions: {} # No permissions required
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testStorybook:
|
|
||||||
name: "Run Visual Tests"
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
issues: read
|
|
||||||
pull-requests: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
repository: element-hq/element-web
|
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
cache: "yarn"
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Get installed Playwright version
|
|
||||||
id: playwright
|
|
||||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: ~/.cache/ms-playwright
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
|
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: "yarn playwright install --with-deps --only-shell"
|
|
||||||
|
|
||||||
- name: Build Element Web resources
|
|
||||||
# Needed to prepare language files
|
|
||||||
run: "yarn build:res"
|
|
||||||
|
|
||||||
- name: Build storybook dependencies
|
|
||||||
# When the first test is ran, it will fail because the dependencies are not yet built.
|
|
||||||
# This step is to ensure that the dependencies are built before running the tests.
|
|
||||||
run: "yarn test:storybook:ci"
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Run Visual tests
|
|
||||||
run: "yarn test:storybook:ci"
|
|
||||||
|
|
||||||
- name: Upload received images & diffs
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: received-images
|
|
||||||
path: playwright/shared-component-received
|
|
||||||
24
.github/workflows/static_analysis.yaml
vendored
24
.github/workflows/static_analysis.yaml
vendored
@@ -23,9 +23,9 @@ jobs:
|
|||||||
name: "Typescript Syntax Check"
|
name: "Typescript Syntax Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -51,14 +51,12 @@ jobs:
|
|||||||
error|invalid_json
|
error|invalid_json
|
||||||
error|misconfigured
|
error|misconfigured
|
||||||
welcome_to_element
|
welcome_to_element
|
||||||
devtools|settings|elementCallUrl
|
|
||||||
labs|sliding_sync_description
|
|
||||||
|
|
||||||
rethemendex_lint:
|
rethemendex_lint:
|
||||||
name: "Rethemendex Check"
|
name: "Rethemendex Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- run: ./res/css/rethemendex.sh
|
- run: ./res/css/rethemendex.sh
|
||||||
|
|
||||||
@@ -68,9 +66,9 @@ jobs:
|
|||||||
name: "ESLint"
|
name: "ESLint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -86,9 +84,9 @@ jobs:
|
|||||||
name: "Style Lint"
|
name: "Style Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -104,9 +102,9 @@ jobs:
|
|||||||
name: "Workflow Lint"
|
name: "Workflow Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -122,9 +120,9 @@ jobs:
|
|||||||
name: "Analyse Dead Code"
|
name: "Analyse Dead Code"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|||||||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@@ -39,12 +39,12 @@ jobs:
|
|||||||
runner: [1, 2]
|
runner: [1, 2]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||||
|
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||||
|
|
||||||
- name: Jest Cache
|
- name: Jest Cache
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: /tmp/jest_cache
|
path: /tmp/jest_cache
|
||||||
key: ${{ hashFiles('**/yarn.lock') }}
|
key: ${{ hashFiles('**/yarn.lock') }}
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
if: env.ENABLE_COVERAGE == 'true'
|
if: env.ENABLE_COVERAGE == 'true'
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.runner }}
|
name: coverage-${{ matrix.runner }}
|
||||||
path: |
|
path: |
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Skip SonarCloud in merge queue
|
- name: Skip SonarCloud in merge queue
|
||||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||||
uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
|
uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|||||||
5
.github/workflows/triage-assigned.yml
vendored
5
.github/workflows/triage-assigned.yml
vendored
@@ -11,11 +11,10 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
contains(github.event.issue.assignees.*.login, 't3chguy') ||
|
contains(github.event.issue.assignees.*.login, 't3chguy') ||
|
||||||
contains(github.event.issue.assignees.*.login, 'florianduros') ||
|
contains(github.event.issue.assignees.*.login, 'andybalaam') ||
|
||||||
contains(github.event.issue.assignees.*.login, 'dbkr') ||
|
|
||||||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/67
|
project-url: https://github.com/orgs/element-hq/projects/67
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/triage-incoming.yml
vendored
2
.github/workflows/triage-incoming.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
automate-project-columns:
|
automate-project-columns:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/120
|
project-url: https://github.com/orgs/element-hq/projects/120
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
28
.github/workflows/triage-labelled.yml
vendored
28
.github/workflows/triage-labelled.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
|
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')
|
contains(github.event.issue.labels.*.name, 'A-Element-Call')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'good first issue') ||
|
contains(github.event.issue.labels.*.name, 'good first issue') ||
|
||||||
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
|
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
||||||
steps:
|
steps:
|
||||||
- id: add_to_project
|
- id: add_to_project
|
||||||
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
uses: actions/add-to-project@v1.0.2
|
||||||
with:
|
with:
|
||||||
project-url: ${{ env.PROJECT_URL }}
|
project-url: ${{ env.PROJECT_URL }}
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
||||||
steps:
|
steps:
|
||||||
- id: add_to_project
|
- id: add_to_project
|
||||||
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
uses: actions/add-to-project@v1.0.2
|
||||||
with:
|
with:
|
||||||
project-url: ${{ env.PROJECT_URL }}
|
project-url: ${{ env.PROJECT_URL }}
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A11y'))
|
contains(github.event.issue.labels.*.name, 'A11y'))
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/18
|
project-url: https://github.com/orgs/element-hq/projects/18
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'X-Needs-Product')
|
contains(github.event.issue.labels.*.name, 'X-Needs-Product')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/28
|
project-url: https://github.com/orgs/element-hq/projects/28
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
|
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/48
|
project-url: https://github.com/orgs/element-hq/projects/48
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -145,20 +145,20 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'Team: VoIP')
|
contains(github.event.issue.labels.*.name, 'Team: VoIP')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/41
|
project-url: https://github.com/orgs/element-hq/projects/41
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
crypto:
|
verticals_feature:
|
||||||
name: Add labelled issues to Crypto project
|
name: Add labelled issues to Verticals Feature project
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'Team: Crypto')
|
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/76
|
project-url: https://github.com/orgs/element-hq/projects/57
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
tech_debt:
|
tech_debt:
|
||||||
@@ -172,7 +172,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Testing') ||
|
contains(github.event.issue.labels.*.name, 'A-Testing') ||
|
||||||
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/101
|
project-url: https://github.com/orgs/element-hq/projects/101
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
name: Move PRs asking for design review to the design board
|
name: Move PRs asking for design review to the design board
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: find_team_members
|
id: find_team_members
|
||||||
with:
|
with:
|
||||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
TEAM: "design"
|
TEAM: "design"
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: add_to_project
|
id: add_to_project
|
||||||
if: steps.any_matching_reviewers.outputs.match == 'true'
|
if: steps.any_matching_reviewers.outputs.match == 'true'
|
||||||
with:
|
with:
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
name: Move PRs asking for design review to the design board
|
name: Move PRs asking for design review to the design board
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: find_team_members
|
id: find_team_members
|
||||||
with:
|
with:
|
||||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
TEAM: "product"
|
TEAM: "product"
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: add_to_project
|
id: add_to_project
|
||||||
if: steps.any_matching_reviewers.outputs.match == 'true'
|
if: steps.any_matching_reviewers.outputs.match == 'true'
|
||||||
with:
|
with:
|
||||||
|
|||||||
21
.github/workflows/triage-stale-flaky-tests.yml
vendored
Normal file
21
.github/workflows/triage-stale-flaky-tests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Close stale flaky issues
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
close:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
only-labels: "Z-Flaky-Test"
|
||||||
|
days-before-stale: 14
|
||||||
|
days-before-close: 0
|
||||||
|
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
||||||
|
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
||||||
|
operations-per-run: 100
|
||||||
29
.github/workflows/triage-stale.yml
vendored
29
.github/workflows/triage-stale.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: Close stale issues & PRs
|
|
||||||
on:
|
|
||||||
workflow_dispatch: {}
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *"
|
|
||||||
permissions: {}
|
|
||||||
jobs:
|
|
||||||
close:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
actions: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
|
||||||
with:
|
|
||||||
operations-per-run: 100
|
|
||||||
|
|
||||||
# Flaky test issue closing
|
|
||||||
any-of-issue-labels: "Z-Flaky-Test-Chrome,Z-Flaky-Test-Firefox,Z-Flaky-Test-Webkit"
|
|
||||||
days-before-issue-stale: 14
|
|
||||||
days-before-issue-close: 0
|
|
||||||
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
|
||||||
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
|
||||||
|
|
||||||
# Stale PR closing
|
|
||||||
days-before-pr-stale: 180
|
|
||||||
days-before-pr-close: 0
|
|
||||||
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
|
|
||||||
2
.github/workflows/triage-unlabelled.yml
vendored
2
.github/workflows/triage-unlabelled.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
|
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
|
||||||
contains(github.event.issue.labels.*.name, 'Z-Labs')
|
contains(github.event.issue.labels.*.name, 'Z-Labs')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.removeLabel({
|
github.rest.issues.removeLabel({
|
||||||
|
|||||||
6
.github/workflows/update-jitsi.yml
vendored
6
.github/workflows/update-jitsi.yml
vendored
@@ -9,9 +9,9 @@ jobs:
|
|||||||
update:
|
update:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
run: "yarn update:jitsi"
|
run: "yarn update:jitsi"
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
branch: actions/jitsi-update
|
branch: actions/jitsi-update
|
||||||
|
|||||||
9
.github/workflows/update-topics.yaml
vendored
9
.github/workflows/update-topics.yaml
vendored
@@ -22,11 +22,11 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
environment: Matrix
|
environment: Matrix
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
- uses: actions/github-script@v7
|
||||||
env:
|
env:
|
||||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||||
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||||
PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org"
|
PUBLIC_ROOM_ID: "!YTvKGNlinIzlkMTVRl:matrix.org"
|
||||||
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
|
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
|
||||||
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
|
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
|
||||||
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
|
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
|
||||||
@@ -81,11 +81,6 @@ jobs:
|
|||||||
d.body = d.body.replace(regex, releaseTopic);
|
d.body = d.body.replace(regex, releaseTopic);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (data["m.topic"]) {
|
|
||||||
data["m.topic"].forEach(d => {
|
|
||||||
d.body = d.body.replace(regex, releaseTopic);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res = await fetch(apiUrl, {
|
res = await fetch(apiUrl, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -25,12 +25,9 @@ electron/pub
|
|||||||
.env
|
.env
|
||||||
/coverage
|
/coverage
|
||||||
# Auto-generated file
|
# Auto-generated file
|
||||||
/src/modules.js
|
/src/modules.ts
|
||||||
/build_config.yaml
|
/build_config.yaml
|
||||||
/book
|
/book
|
||||||
/index.html
|
/index.html
|
||||||
# version file and tarball created by `npm pack` / `yarn pack`
|
# version file and tarball created by `npm pack` / `yarn pack`
|
||||||
/git-revision.txt
|
/git-revision.txt
|
||||||
|
|
||||||
*storybook.log
|
|
||||||
storybook-static
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ electron/pub
|
|||||||
/coverage
|
/coverage
|
||||||
# Auto-generated file
|
# Auto-generated file
|
||||||
/src/modules.ts
|
/src/modules.ts
|
||||||
/src/modules.js
|
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
/build_config.yaml
|
/build_config.yaml
|
||||||
# Raises an error because it contains a template var breaking the script tag
|
# Raises an error because it contains a template var breaking the script tag
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { create } from "storybook/theming";
|
|
||||||
|
|
||||||
export default create({
|
|
||||||
base: "light",
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
textColor: "#1b1d22",
|
|
||||||
colorSecondary: "#111111",
|
|
||||||
|
|
||||||
// UI
|
|
||||||
appBg: "#ffffff",
|
|
||||||
appContentBg: "#ffffff",
|
|
||||||
|
|
||||||
// Toolbar
|
|
||||||
barBg: "#ffffff",
|
|
||||||
|
|
||||||
brandTitle: "Element Web",
|
|
||||||
brandUrl: "https://github.com/element-hq/element-web",
|
|
||||||
brandImage: "https://element.io/images/logo-ele-secondary.svg",
|
|
||||||
brandTarget: "_self",
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Addon, types, useGlobals } from "storybook/manager-api";
|
|
||||||
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
|
|
||||||
import React from "react";
|
|
||||||
import { GlobeIcon } from "@storybook/icons";
|
|
||||||
|
|
||||||
// We can't import `shared/i18n.tsx` directly here.
|
|
||||||
// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
|
|
||||||
import json from "../webapp/i18n/languages.json";
|
|
||||||
const languages = Object.keys(json).filter((lang) => lang !== "default");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the title of a language in the user's locale.
|
|
||||||
*/
|
|
||||||
function languageTitle(language: string): string {
|
|
||||||
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const languageAddon: Addon = {
|
|
||||||
title: "Language Selector",
|
|
||||||
type: types.TOOL,
|
|
||||||
render: ({ active }) => {
|
|
||||||
const [globals, updateGlobals] = useGlobals();
|
|
||||||
const selectedLanguage = globals.language || "en";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WithTooltip
|
|
||||||
placement="top"
|
|
||||||
trigger="click"
|
|
||||||
closeOnOutsideClick
|
|
||||||
tooltip={({ onHide }) => {
|
|
||||||
return (
|
|
||||||
<TooltipLinkList
|
|
||||||
links={languages.map((language) => ({
|
|
||||||
id: language,
|
|
||||||
title: languageTitle(language),
|
|
||||||
active: selectedLanguage === language,
|
|
||||||
onClick: async () => {
|
|
||||||
// Update the global state with the selected language
|
|
||||||
updateGlobals({ language });
|
|
||||||
onHide();
|
|
||||||
},
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton title="Language">
|
|
||||||
<GlobeIcon />
|
|
||||||
{languageTitle(selectedLanguage)}
|
|
||||||
</IconButton>
|
|
||||||
</WithTooltip>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { StorybookConfig } from "@storybook/react-vite";
|
|
||||||
import path from "node:path";
|
|
||||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
||||||
import { mergeConfig } from "vite";
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
||||||
staticDirs: ["../webapp"],
|
|
||||||
addons: ["@storybook/addon-docs", "@storybook/addon-designs"],
|
|
||||||
framework: "@storybook/react-vite",
|
|
||||||
core: {
|
|
||||||
disableTelemetry: true,
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
reactDocgen: "react-docgen-typescript",
|
|
||||||
},
|
|
||||||
async viteFinal(config) {
|
|
||||||
return mergeConfig(config, {
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
// Alias used by i18n.tsx
|
|
||||||
$webapp: path.resolve("webapp"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Needed for counterpart to work
|
|
||||||
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
|
||||||
server: {
|
|
||||||
allowedHosts: ["localhost", ".docker.internal"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { addons } from "storybook/manager-api";
|
|
||||||
import ElementTheme from "./ElementTheme";
|
|
||||||
import { languageAddon } from "./languageAddon";
|
|
||||||
|
|
||||||
addons.setConfig({
|
|
||||||
theme: ElementTheme,
|
|
||||||
});
|
|
||||||
|
|
||||||
addons.register("elementhq/language", () => addons.add("language", languageAddon));
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.docs-story {
|
|
||||||
background: var(--cpd-color-bg-canvas-default);
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
|
|
||||||
import { addons } from "storybook/preview-api";
|
|
||||||
|
|
||||||
import "../res/css/shared.pcss";
|
|
||||||
import "./preview.css";
|
|
||||||
import React, { useLayoutEffect } from "react";
|
|
||||||
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
|
|
||||||
import { setLanguage } from "../src/shared-components/utils/i18n";
|
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
|
||||||
|
|
||||||
export const globalTypes = {
|
|
||||||
theme: {
|
|
||||||
name: "Theme",
|
|
||||||
description: "Global theme for components",
|
|
||||||
toolbar: {
|
|
||||||
icon: "circlehollow",
|
|
||||||
title: "Theme",
|
|
||||||
items: [
|
|
||||||
{ title: "System", value: "system", icon: "browser" },
|
|
||||||
{ title: "Light", value: "light", icon: "sun" },
|
|
||||||
{ title: "Light (high contrast)", value: "light-hc", icon: "sun" },
|
|
||||||
{ title: "Dark", value: "dark", icon: "moon" },
|
|
||||||
{ title: "Dark (high contrast)", value: "dark-hc", icon: "moon" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
language: {
|
|
||||||
name: "Language",
|
|
||||||
description: "Global language for components",
|
|
||||||
},
|
|
||||||
initialGlobals: {
|
|
||||||
theme: "system",
|
|
||||||
language: "en",
|
|
||||||
},
|
|
||||||
} satisfies ArgTypes;
|
|
||||||
|
|
||||||
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
|
|
||||||
|
|
||||||
const ThemeSwitcher: React.FC<{
|
|
||||||
theme: string;
|
|
||||||
}> = ({ theme }) => {
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
document.documentElement.classList.remove(...allThemesClasses);
|
|
||||||
if (theme !== "system") {
|
|
||||||
document.documentElement.classList.add(`cpd-theme-${theme}`);
|
|
||||||
}
|
|
||||||
return () => document.documentElement.classList.remove(...allThemesClasses);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withThemeProvider: Decorator = (Story, context) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ThemeSwitcher theme={context.globals.theme} />
|
|
||||||
<Story />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const LanguageSwitcher: React.FC<{
|
|
||||||
language: string;
|
|
||||||
}> = ({ language }) => {
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const changeLanguage = async (language: string) => {
|
|
||||||
await setLanguage(language);
|
|
||||||
// Force the component to re-render to apply the new language
|
|
||||||
addons.getChannel().emit(FORCE_RE_RENDER);
|
|
||||||
};
|
|
||||||
changeLanguage(language);
|
|
||||||
}, [language]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const withLanguageProvider: Decorator = (Story, context) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LanguageSwitcher language={context.globals.language} />
|
|
||||||
<Story />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const withTooltipProvider: Decorator = (Story) => {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Story />
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
tags: ["autodocs"],
|
|
||||||
decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
|
|
||||||
parameters: {
|
|
||||||
options: {
|
|
||||||
storySort: {
|
|
||||||
method: "alphabetical",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default preview;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { waitForPageReady } from "@storybook/test-runner";
|
|
||||||
import { toMatchImageSnapshot } from "jest-image-snapshot";
|
|
||||||
|
|
||||||
const customSnapshotsDir = `${process.cwd()}/playwright/shared-component-snapshots/`;
|
|
||||||
const customReceivedDir = `${process.cwd()}/playwright/shared-component-received/`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import('@storybook/test-runner').TestRunnerConfig}
|
|
||||||
*/
|
|
||||||
const config = {
|
|
||||||
setup(page) {
|
|
||||||
expect.extend({ toMatchImageSnapshot });
|
|
||||||
},
|
|
||||||
async postVisit(page, context) {
|
|
||||||
await waitForPageReady(page);
|
|
||||||
|
|
||||||
// If you want to take screenshot of multiple browsers, use
|
|
||||||
// page.context().browser().browserType().name() to get the browser name to prefix the file name
|
|
||||||
const image = await page.screenshot();
|
|
||||||
expect(image).toMatchImageSnapshot({
|
|
||||||
customSnapshotsDir,
|
|
||||||
customSnapshotIdentifier: `${context.id}-${process.platform}`,
|
|
||||||
storeReceivedOnFailure: true,
|
|
||||||
customReceivedDir,
|
|
||||||
customDiffDir: customReceivedDir,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -33,15 +33,19 @@ module.exports = {
|
|||||||
"import-notation": null,
|
"import-notation": null,
|
||||||
"value-keyword-case": null,
|
"value-keyword-case": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
|
"declaration-block-no-duplicate-properties": [
|
||||||
|
true,
|
||||||
|
// useful for fallbacks
|
||||||
|
{ ignore: ["consecutive-duplicates-with-different-values"] },
|
||||||
|
],
|
||||||
"shorthand-property-no-redundant-values": null,
|
"shorthand-property-no-redundant-values": null,
|
||||||
"property-no-vendor-prefix": null,
|
"property-no-vendor-prefix": null,
|
||||||
|
"value-no-vendor-prefix": null,
|
||||||
"selector-no-vendor-prefix": null,
|
"selector-no-vendor-prefix": null,
|
||||||
"media-feature-name-no-vendor-prefix": null,
|
"media-feature-name-no-vendor-prefix": null,
|
||||||
"number-max-precision": null,
|
"number-max-precision": null,
|
||||||
"no-invalid-double-slash-comments": true,
|
"no-invalid-double-slash-comments": true,
|
||||||
"media-feature-range-notation": null,
|
"media-feature-range-notation": null,
|
||||||
"declaration-property-value-no-unknown": null,
|
|
||||||
"declaration-property-value-keyword-no-deprecated": null,
|
|
||||||
"csstools/value-no-unknown-custom-properties": [
|
"csstools/value-no-unknown-custom-properties": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
@@ -70,13 +74,5 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"property-no-deprecated": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
ignoreProperties: ["-webkit-box-orient", "word-wrap"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"nesting-selector-no-missing-scoping-root": null,
|
|
||||||
"no-invalid-position-declaration": null,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,3 @@ include:
|
|||||||
|
|
||||||
* Thom Cleary (https://github.com/thomcatdotrocks)
|
* Thom Cleary (https://github.com/thomcatdotrocks)
|
||||||
Small update for tarball deployment
|
Small update for tarball deployment
|
||||||
|
|
||||||
* Alexander (https://github.com/ioalexander)
|
|
||||||
Save image on CTRL + S shortcut
|
|
||||||
|
|||||||
459
CHANGELOG.md
459
CHANGELOG.md
@@ -1,462 +1,3 @@
|
|||||||
Changes in [1.11.110](https://github.com/element-hq/element-web/releases/tag/v1.11.110) (2025-08-27)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Hide recovery key when re-entering it while creating or changing it ([#30499](https://github.com/element-hq/element-web/pull/30499)). Contributed by @andybalaam.
|
|
||||||
* Add `?no_universal_links=true` to OIDC url so EX doesn't try to handle it ([#29439](https://github.com/element-hq/element-web/pull/29439)). Contributed by @t3chguy.
|
|
||||||
* Show a blue lock for unencrypted rooms and hide the grey shield for encrypted rooms ([#30440](https://github.com/element-hq/element-web/pull/30440)). Contributed by @langleyd.
|
|
||||||
* Add support for Module API 1.4 ([#30185](https://github.com/element-hq/element-web/pull/30185)). Contributed by @t3chguy.
|
|
||||||
* MVVM - Introduce some helpers for snapshot management ([#30398](https://github.com/element-hq/element-web/pull/30398)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* A11y: move focus to right panel when opened ([#30553](https://github.com/element-hq/element-web/pull/30553)). Contributed by @florianduros.
|
|
||||||
* Fix e2e warning icon should be white ([#30539](https://github.com/element-hq/element-web/pull/30539)). Contributed by @florianduros.
|
|
||||||
* Remove NoOneHere disabled reason. ([#30524](https://github.com/element-hq/element-web/pull/30524)). Contributed by @toger5.
|
|
||||||
* Fix downloading files with authenticated media API ([#30520](https://github.com/element-hq/element-web/pull/30520)). Contributed by @t3chguy.
|
|
||||||
* Fix call permissions check confusion around element call ([#30521](https://github.com/element-hq/element-web/pull/30521)). Contributed by @t3chguy.
|
|
||||||
* Fix line wrap around emoji verification ([#30523](https://github.com/element-hq/element-web/pull/30523)). Contributed by @t3chguy.
|
|
||||||
* Don't highlight redacted events ([#30519](https://github.com/element-hq/element-web/pull/30519)). Contributed by @t3chguy.
|
|
||||||
* Fix matrix.to links not being handled in the app ([#30522](https://github.com/element-hq/element-web/pull/30522)). Contributed by @t3chguy.
|
|
||||||
* Fix issue of new room list taking up the full width ([#30459](https://github.com/element-hq/element-web/pull/30459)). Contributed by @langleyd.
|
|
||||||
* Fix widget persistence in React development mode ([#30509](https://github.com/element-hq/element-web/pull/30509)). Contributed by @robintown.
|
|
||||||
* Fix widget initialization in React development mode ([#30463](https://github.com/element-hq/element-web/pull/30463)). Contributed by @robintown.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.109](https://github.com/element-hq/element-web/releases/tag/v1.11.109) (2025-08-11)
|
|
||||||
====================================================================================================
|
|
||||||
This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [Backport staging] Allow /upgraderoom command without developer mode enabled ([#30529](https://github.com/element-hq/element-web/pull/30529)). Contributed by @RiotRobot.
|
|
||||||
* [Backport staging] Support for creator/owner power level ([#30526](https://github.com/element-hq/element-web/pull/30526)). Contributed by @RiotRobot.
|
|
||||||
* New room list: change icon and label of menu item for to start a DM ([#30470](https://github.com/element-hq/element-web/pull/30470)). Contributed by @florianduros.
|
|
||||||
* Implement the member list with virtuoso ([#29869](https://github.com/element-hq/element-web/pull/29869)). Contributed by @langleyd.
|
|
||||||
* Add labs option for history sharing on invite ([#30313](https://github.com/element-hq/element-web/pull/30313)). Contributed by @richvdh.
|
|
||||||
* Bump wysiwyg to 2.39.0 adding support for pasting rich text content in the Rich Text Edtior ([#30421](https://github.com/element-hq/element-web/pull/30421)). Contributed by @langleyd.
|
|
||||||
* Support `EventShieldReason.MISMATCHED_SENDER` ([#30403](https://github.com/element-hq/element-web/pull/30403)). Contributed by @richvdh.
|
|
||||||
* Change unencrypted and public pills to blue ([#30399](https://github.com/element-hq/element-web/pull/30399)). Contributed by @florianduros.
|
|
||||||
* Change color of public room icon ([#30390](https://github.com/element-hq/element-web/pull/30390)). Contributed by @florianduros.
|
|
||||||
* Script for updating storybook screenshots ([#30340](https://github.com/element-hq/element-web/pull/30340)). Contributed by @dbkr.
|
|
||||||
* Add toggle to hide empty state in devtools ([#30352](https://github.com/element-hq/element-web/pull/30352)). Contributed by @toger5.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Use userId to filter users in non-federated rooms when showing the InviteDialog ([#30537](https://github.com/element-hq/element-web/pull/30537)). Contributed by @RiotRobot.
|
|
||||||
* [Backport staging] Catch error when encountering invalid m.room.pinned\_events event ([#30536](https://github.com/element-hq/element-web/pull/30536)). Contributed by @RiotRobot.
|
|
||||||
* Update for compatibility with v12 rooms ([#30452](https://github.com/element-hq/element-web/pull/30452)). Contributed by @dbkr.
|
|
||||||
* New room list: fix tooltip on presence ([#30474](https://github.com/element-hq/element-web/pull/30474)). Contributed by @florianduros.
|
|
||||||
* New room list: add tooltip for presence and room status ([#30472](https://github.com/element-hq/element-web/pull/30472)). Contributed by @florianduros.
|
|
||||||
* Fix: Clicking on an item in the member list causes it to scroll to the top rather than show the profile view ([#30455](https://github.com/element-hq/element-web/pull/30455)). Contributed by @langleyd.
|
|
||||||
* Put the 'decrypting' tooltip back ([#30446](https://github.com/element-hq/element-web/pull/30446)). Contributed by @dbkr.
|
|
||||||
* Use server name explicitly for via. ([#30362](https://github.com/element-hq/element-web/pull/30362)). Contributed by @Half-Shot.
|
|
||||||
* fix: replace hardcoded string in poll history dialog ([#30402](https://github.com/element-hq/element-web/pull/30402)). Contributed by @florianduros.
|
|
||||||
* fix: replace hardcoded string on qr code back button ([#30401](https://github.com/element-hq/element-web/pull/30401)). Contributed by @florianduros.
|
|
||||||
* Fix color of icon button with outline ([#30361](https://github.com/element-hq/element-web/pull/30361)). Contributed by @florianduros.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
|
|
||||||
====================================================================================================
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Fix downloaded attachments not being decrypted ([#30434](https://github.com/element-hq/element-web/pull/30434)). Contributed by @RiotRobot.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.107](https://github.com/element-hq/element-web/releases/tag/v1.11.107) (2025-07-29)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Message preview should show tooltip with the full message on hover ([#30265](https://github.com/element-hq/element-web/pull/30265)). Contributed by @MidhunSureshR.
|
|
||||||
* Support rendering notification badges on platforms that do their own icon overlays ([#30315](https://github.com/element-hq/element-web/pull/30315)). Contributed by @Half-Shot.
|
|
||||||
* Add SubscriptionViewModel base class ([#30297](https://github.com/element-hq/element-web/pull/30297)). Contributed by @dbkr.
|
|
||||||
* Enhancement: Save image on CTRL+S ([#30330](https://github.com/element-hq/element-web/pull/30330)). Contributed by @ioalexander.
|
|
||||||
* Add quote functionality to MessageContextMenu (#29893) ([#30323](https://github.com/element-hq/element-web/pull/30323)). Contributed by @AlirezaMrtz.
|
|
||||||
* Initial structure for shared component views ([#30216](https://github.com/element-hq/element-web/pull/30216)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Fix e2e shield being invisible in white mode for encrypted room ([#30411](https://github.com/element-hq/element-web/pull/30411)). Contributed by @RiotRobot.
|
|
||||||
* Force ED titlebar color for new room list ([#30332](https://github.com/element-hq/element-web/pull/30332)). Contributed by @florianduros.
|
|
||||||
* Add a background color to left panel for macos titlebar in element desktop ([#30328](https://github.com/element-hq/element-web/pull/30328)). Contributed by @florianduros.
|
|
||||||
* Fix: Prevent page refresh on Enter key in right panel member search ([#30312](https://github.com/element-hq/element-web/pull/30312)). Contributed by @AlirezaMrtz.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.106](https://github.com/element-hq/element-web/releases/tag/v1.11.106) (2025-07-15)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [Backport staging] Fix e2e icon colour ([#30304](https://github.com/element-hq/element-web/pull/30304)). Contributed by @RiotRobot.
|
|
||||||
* Add support for module message hint `allowDownloadingMedia` ([#30252](https://github.com/element-hq/element-web/pull/30252)). Contributed by @Half-Shot.
|
|
||||||
* Update the mobile\_guide page to the new design and link out to Element X by default. ([#30172](https://github.com/element-hq/element-web/pull/30172)). Contributed by @pixlwave.
|
|
||||||
* Filter settings exported when rageshaking ([#30236](https://github.com/element-hq/element-web/pull/30236)). Contributed by @Half-Shot.
|
|
||||||
* Allow Element Call to learn the room name ([#30213](https://github.com/element-hq/element-web/pull/30213)). Contributed by @robintown.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Fix missing image download button ([#30322](https://github.com/element-hq/element-web/pull/30322)). Contributed by @RiotRobot.
|
|
||||||
* Fix transparent verification checkmark in dark mode ([#30235](https://github.com/element-hq/element-web/pull/30235)). Contributed by @Banbuii.
|
|
||||||
* Fix logic in DeviceListener ([#30230](https://github.com/element-hq/element-web/pull/30230)). Contributed by @uhoreg.
|
|
||||||
* Disable file drag-and-drop if insufficient permissions ([#30186](https://github.com/element-hq/element-web/pull/30186)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* New room list: add context menu to room list item ([#29952](https://github.com/element-hq/element-web/pull/29952)). Contributed by @florianduros.
|
|
||||||
* Support for custom message components via Module API ([#30074](https://github.com/element-hq/element-web/pull/30074)). Contributed by @Half-Shot.
|
|
||||||
* Prompt users to set up recovery ([#30075](https://github.com/element-hq/element-web/pull/30075)). Contributed by @uhoreg.
|
|
||||||
* Update `IconButton` colors ([#30124](https://github.com/element-hq/element-web/pull/30124)). Contributed by @florianduros.
|
|
||||||
* New room list: filter list can be collapsed ([#29992](https://github.com/element-hq/element-web/pull/29992)). Contributed by @florianduros.
|
|
||||||
* Show `EmptyRoomListView` when low priority filter matches zero rooms ([#30122](https://github.com/element-hq/element-web/pull/30122)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix untranslatable string "People" in notifications beta ([#30165](https://github.com/element-hq/element-web/pull/30165)). Contributed by @t3chguy.
|
|
||||||
* Force verification even after logging in via delegate ([#30141](https://github.com/element-hq/element-web/pull/30141)). Contributed by @andybalaam.
|
|
||||||
* Hide add integrations button based on UIComponent.AddIntegrations ([#30140](https://github.com/element-hq/element-web/pull/30140)). Contributed by @t3chguy.
|
|
||||||
* Use nav for new room list and label sections ([#30134](https://github.com/element-hq/element-web/pull/30134)). Contributed by @dbkr.
|
|
||||||
* Spacestore should emit event after rebuilding home space ([#30132](https://github.com/element-hq/element-web/pull/30132)). Contributed by @MidhunSureshR.
|
|
||||||
* Handle m.room.pinned\_events being invalid ([#30129](https://github.com/element-hq/element-web/pull/30129)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Update the mobile\_guide page to the new design. ([#30006](https://github.com/element-hq/element-web/pull/30006)). Contributed by @pixlwave.
|
|
||||||
* Provide a devtool for manually verifying other devices ([#30094](https://github.com/element-hq/element-web/pull/30094)). Contributed by @andybalaam.
|
|
||||||
* Implement MSC4155: Invite filtering ([#29603](https://github.com/element-hq/element-web/pull/29603)). Contributed by @Half-Shot.
|
|
||||||
* Add low priority avatar decoration to room tile ([#30065](https://github.com/element-hq/element-web/pull/30065)). Contributed by @MidhunSureshR.
|
|
||||||
* Add ability to prevent window content being captured by other apps (Desktop) ([#30098](https://github.com/element-hq/element-web/pull/30098)). Contributed by @t3chguy.
|
|
||||||
* New room list: move message preview in user settings ([#30023](https://github.com/element-hq/element-web/pull/30023)). Contributed by @florianduros.
|
|
||||||
* New room list: change room options icon ([#30029](https://github.com/element-hq/element-web/pull/30029)). Contributed by @florianduros.
|
|
||||||
* RoomListStore: Sort low priority rooms to the bottom of the list ([#30070](https://github.com/element-hq/element-web/pull/30070)). Contributed by @MidhunSureshR.
|
|
||||||
* Add low priority filter pill to the room list UI ([#30060](https://github.com/element-hq/element-web/pull/30060)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: remove color gradient in space panel ([#29721](https://github.com/element-hq/element-web/pull/29721)). Contributed by @florianduros.
|
|
||||||
* /share?msg=foo endpoint using forward message dialog ([#29874](https://github.com/element-hq/element-web/pull/29874)). Contributed by @ara4n.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Do not send empty auth when setting up cross-signing keys ([#29914](https://github.com/element-hq/element-web/pull/29914)). Contributed by @gnieto.
|
|
||||||
* Settings: flip local video feed by default ([#29501](https://github.com/element-hq/element-web/pull/29501)). Contributed by @jbtrystram.
|
|
||||||
* AccessSecretStorageDialog: various fixes ([#30093](https://github.com/element-hq/element-web/pull/30093)). Contributed by @richvdh.
|
|
||||||
* AccessSecretStorageDialog: fix inability to enter recovery key ([#30090](https://github.com/element-hq/element-web/pull/30090)). Contributed by @richvdh.
|
|
||||||
* Fix failure to upload thumbnail causing image to send as file ([#30086](https://github.com/element-hq/element-web/pull/30086)). Contributed by @t3chguy.
|
|
||||||
* Low priority menu item should be a toggle ([#30071](https://github.com/element-hq/element-web/pull/30071)). Contributed by @MidhunSureshR.
|
|
||||||
* Add sanity checks to prevent users from ignoring themselves ([#30079](https://github.com/element-hq/element-web/pull/30079)). Contributed by @MidhunSureshR.
|
|
||||||
* Fix issue with duplicate images ([#30073](https://github.com/element-hq/element-web/pull/30073)). Contributed by @fatlewis.
|
|
||||||
* Handle errors returned from Seshat ([#30083](https://github.com/element-hq/element-web/pull/30083)). Contributed by @richvdh.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10)
|
|
||||||
====================================================================================================
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
+ Check the sender of an event matches owner of session, preventing sender spoofing by homeserver owners.
|
|
||||||
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
|
|
||||||
|
|
||||||
Changes in [1.11.102](https://github.com/element-hq/element-web/releases/tag/v1.11.102) (2025-06-03)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* EW: Modernize the recovery key input modal ([#29819](https://github.com/element-hq/element-web/pull/29819)). Contributed by @uhoreg.
|
|
||||||
* New room list: move secondary filters into primary filters ([#29972](https://github.com/element-hq/element-web/pull/29972)). Contributed by @florianduros.
|
|
||||||
* Prompt the user when key storage is unexpectedly off ([#29912](https://github.com/element-hq/element-web/pull/29912)). Contributed by @andybalaam.
|
|
||||||
* New room list: move sort menu in room list header ([#29983](https://github.com/element-hq/element-web/pull/29983)). Contributed by @florianduros.
|
|
||||||
* New room list: rework spacing of room list item ([#29965](https://github.com/element-hq/element-web/pull/29965)). Contributed by @florianduros.
|
|
||||||
* RLS: Remove forgotten room from skiplist ([#29933](https://github.com/element-hq/element-web/pull/29933)). Contributed by @MidhunSureshR.
|
|
||||||
* Add room list sorting ([#29951](https://github.com/element-hq/element-web/pull/29951)). Contributed by @dbkr.
|
|
||||||
* Don't use the minimised width(68px) on the new room list ([#29778](https://github.com/element-hq/element-web/pull/29778)). Contributed by @langleyd.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Close call options popup menu when option has been selected ([#30054](https://github.com/element-hq/element-web/pull/30054)). Contributed by @RiotRobot.
|
|
||||||
* RoomListStoreV3: Only add new rooms that pass `VisibilityProvider` check ([#29974](https://github.com/element-hq/element-web/pull/29974)). Contributed by @MidhunSureshR.
|
|
||||||
* Re-order primary filters ([#29957](https://github.com/element-hq/element-web/pull/29957)). Contributed by @dbkr.
|
|
||||||
* Fix leaky CSS adding `!` to all H1 elements ([#29964](https://github.com/element-hq/element-web/pull/29964)). Contributed by @t3chguy.
|
|
||||||
* Fix extensions panel style ([#29273](https://github.com/element-hq/element-web/pull/29273)). Contributed by @langleyd.
|
|
||||||
* Fix state events being hidden from widgets in read\_events actions ([#29954](https://github.com/element-hq/element-web/pull/29954)). Contributed by @robintown.
|
|
||||||
* Remove old filter test ([#29963](https://github.com/element-hq/element-web/pull/29963)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.101](https://github.com/element-hq/element-web/releases/tag/v1.11.101) (2025-05-20)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* New room list: add keyboard navigation support ([#29805](https://github.com/element-hq/element-web/pull/29805)). Contributed by @florianduros.
|
|
||||||
* Use the JoinRuleSettings component for the guest link access prompt. ([#28614](https://github.com/element-hq/element-web/pull/28614)). Contributed by @toger5.
|
|
||||||
* Add loading state to the new room list view ([#29725](https://github.com/element-hq/element-web/pull/29725)). Contributed by @langleyd.
|
|
||||||
* Make OIDC identity reset consistent with EX ([#29854](https://github.com/element-hq/element-web/pull/29854)). Contributed by @andybalaam.
|
|
||||||
* Support error code for email / phone adding unsupported (MSC4178) ([#29855](https://github.com/element-hq/element-web/pull/29855)). Contributed by @dbkr.
|
|
||||||
* Update identity reset UI (Make consistent with EX) ([#29701](https://github.com/element-hq/element-web/pull/29701)). Contributed by @andybalaam.
|
|
||||||
* Add secondary filters to the new room list ([#29818](https://github.com/element-hq/element-web/pull/29818)). Contributed by @dbkr.
|
|
||||||
* Fix battery drain from Web Audio ([#29203](https://github.com/element-hq/element-web/pull/29203)). Contributed by @mbachry.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix go home shortcut on macos and change toggle action events shortcut ([#29929](https://github.com/element-hq/element-web/pull/29929)). Contributed by @florianduros.
|
|
||||||
* New room list: fix outdated message preview when space or filter change ([#29925](https://github.com/element-hq/element-web/pull/29925)). Contributed by @florianduros.
|
|
||||||
* Stop migrating to MSC4278 if the config exists. ([#29924](https://github.com/element-hq/element-web/pull/29924)). Contributed by @Half-Shot.
|
|
||||||
* Ensure consistent download file name on download from ImageView ([#29913](https://github.com/element-hq/element-web/pull/29913)). Contributed by @t3chguy.
|
|
||||||
* Add error toast when service worker registration fails ([#29895](https://github.com/element-hq/element-web/pull/29895)). Contributed by @t3chguy.
|
|
||||||
* New Room List: Prevent old tombstoned rooms from appearing in the list ([#29881](https://github.com/element-hq/element-web/pull/29881)). Contributed by @MidhunSureshR.
|
|
||||||
* Remove lag in search field ([#29885](https://github.com/element-hq/element-web/pull/29885)). Contributed by @florianduros.
|
|
||||||
* Respect UIFeature.Voip ([#29873](https://github.com/element-hq/element-web/pull/29873)). Contributed by @langleyd.
|
|
||||||
* Allow jumping to message search from spotlight ([#29850](https://github.com/element-hq/element-web/pull/29850)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.100](https://github.com/element-hq/element-web/releases/tag/v1.11.100) (2025-05-06)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Move rich topics out of labs / stabilise MSC3765 ([#29817](https://github.com/element-hq/element-web/pull/29817)). Contributed by @Johennes.
|
|
||||||
* Spell out that Element Web does \*not\* work on mobile. ([#29211](https://github.com/element-hq/element-web/pull/29211)). Contributed by @ara4n.
|
|
||||||
* Add message preview support to the new room list ([#29784](https://github.com/element-hq/element-web/pull/29784)). Contributed by @dbkr.
|
|
||||||
* Global configuration flag for media previews ([#29582](https://github.com/element-hq/element-web/pull/29582)). Contributed by @Half-Shot.
|
|
||||||
* New room list: add partial keyboard shortcuts support ([#29783](https://github.com/element-hq/element-web/pull/29783)). Contributed by @florianduros.
|
|
||||||
* MVVM RoomSummaryCard Topic ([#29710](https://github.com/element-hq/element-web/pull/29710)). Contributed by @MarcWadai.
|
|
||||||
* Warn on self change from settings > roles ([#28926](https://github.com/element-hq/element-web/pull/28926)). Contributed by @MarcWadai.
|
|
||||||
* New room list: new visual for invitation ([#29773](https://github.com/element-hq/element-web/pull/29773)). Contributed by @florianduros.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix incorrect display of the user info display name ([#29826](https://github.com/element-hq/element-web/pull/29826)). Contributed by @langleyd.
|
|
||||||
* RoomListStore: Remove invite rooms on decline ([#29804](https://github.com/element-hq/element-web/pull/29804)). Contributed by @MidhunSureshR.
|
|
||||||
* Fix the buttons not being displayed with long preview text ([#29811](https://github.com/element-hq/element-web/pull/29811)). Contributed by @dbkr.
|
|
||||||
* New room list: fix missing/incorrect notification decoration ([#29796](https://github.com/element-hq/element-web/pull/29796)). Contributed by @florianduros.
|
|
||||||
* New Room List: Prevent potential scroll jump/flicker when switching spaces ([#29781](https://github.com/element-hq/element-web/pull/29781)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: fix incorrect decoration ([#29770](https://github.com/element-hq/element-web/pull/29770)). Contributed by @florianduros.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.99](https://github.com/element-hq/element-web/releases/tag/v1.11.99) (2025-04-23)
|
|
||||||
==================================================================================================
|
|
||||||
No changes, just bumping the version to accommodate a new Element Desktop release
|
|
||||||
|
|
||||||
Changes in [1.11.98](https://github.com/element-hq/element-web/releases/tag/v1.11.98) (2025-04-22)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* print better errors in the search view instead of a blocking modal ([#29724](https://github.com/element-hq/element-web/pull/29724)). Contributed by @Jujure.
|
|
||||||
* New room list: video room and video call decoration ([#29693](https://github.com/element-hq/element-web/pull/29693)). Contributed by @florianduros.
|
|
||||||
* Remove Secure Backup, Cross-signing and Cryptography sections in `Security & Privacy` user settings ([#29088](https://github.com/element-hq/element-web/pull/29088)). Contributed by @florianduros.
|
|
||||||
* Allow reporting a room when rejecting an invite. ([#29570](https://github.com/element-hq/element-web/pull/29570)). Contributed by @Half-Shot.
|
|
||||||
* RoomListViewModel: Reset primary and secondary filters on space change ([#29672](https://github.com/element-hq/element-web/pull/29672)). Contributed by @MidhunSureshR.
|
|
||||||
* RoomListStore: Support specific sorting requirements for muted rooms ([#29665](https://github.com/element-hq/element-web/pull/29665)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: add notification options menu ([#29639](https://github.com/element-hq/element-web/pull/29639)). Contributed by @florianduros.
|
|
||||||
* Room List: Scroll to top of the list when active room is not in the list ([#29650](https://github.com/element-hq/element-web/pull/29650)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix unwanted form submit behaviour in memberlist ([#29747](https://github.com/element-hq/element-web/pull/29747)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: fix public room icon visibility when filter change ([#29737](https://github.com/element-hq/element-web/pull/29737)). Contributed by @florianduros.
|
|
||||||
* Fix custom theme support for short hex \& rgba hex strings ([#29726](https://github.com/element-hq/element-web/pull/29726)). Contributed by @t3chguy.
|
|
||||||
* New room list: minor visual fixes ([#29723](https://github.com/element-hq/element-web/pull/29723)). Contributed by @florianduros.
|
|
||||||
* Fix getOidcCallbackUrl for Element Desktop ([#29711](https://github.com/element-hq/element-web/pull/29711)). Contributed by @t3chguy.
|
|
||||||
* Fix some webp images improperly marked as animated ([#29713](https://github.com/element-hq/element-web/pull/29713)). Contributed by @Petersmit27.
|
|
||||||
* Revert deletion of hydrateSession ([#29703](https://github.com/element-hq/element-web/pull/29703)). Contributed by @Jujure.
|
|
||||||
* Fix converttoroom \& converttodm not working ([#29705](https://github.com/element-hq/element-web/pull/29705)). Contributed by @t3chguy.
|
|
||||||
* Ensure forceCloseAllModals also closes priority/static modals ([#29706](https://github.com/element-hq/element-web/pull/29706)). Contributed by @t3chguy.
|
|
||||||
* Continue button is disabled when uploading a recovery key file ([#29695](https://github.com/element-hq/element-web/pull/29695)). Contributed by @Giwayume.
|
|
||||||
* Catch errors after syncing recovery ([#29691](https://github.com/element-hq/element-web/pull/29691)). Contributed by @andybalaam.
|
|
||||||
* New room list: fix multiple visual issues ([#29673](https://github.com/element-hq/element-web/pull/29673)). Contributed by @florianduros.
|
|
||||||
* New Room List: Fix mentions filter matching rooms with any highlight ([#29668](https://github.com/element-hq/element-web/pull/29668)). Contributed by @MidhunSureshR.
|
|
||||||
* Fix truncated emoji label during emoji SAS ([#29643](https://github.com/element-hq/element-web/pull/29643)). Contributed by @florianduros.
|
|
||||||
* Remove duplicate jitsi link ([#29642](https://github.com/element-hq/element-web/pull/29642)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros.
|
|
||||||
* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy.
|
|
||||||
* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot.
|
|
||||||
* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr.
|
|
||||||
* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy.
|
|
||||||
* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros.
|
|
||||||
* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR.
|
|
||||||
* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot.
|
|
||||||
* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown.
|
|
||||||
* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure.
|
|
||||||
* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot.
|
|
||||||
* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR.
|
|
||||||
* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot.
|
|
||||||
* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros.
|
|
||||||
* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr.
|
|
||||||
* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR.
|
|
||||||
* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy.
|
|
||||||
* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros.
|
|
||||||
* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy.
|
|
||||||
* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros.
|
|
||||||
* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr.
|
|
||||||
* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy.
|
|
||||||
* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy.
|
|
||||||
* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh.
|
|
||||||
* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR.
|
|
||||||
* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR.
|
|
||||||
* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam.
|
|
||||||
* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* RoomListViewModel: Track the index of the active room in the list ([#29519](https://github.com/element-hq/element-web/pull/29519)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: add empty state ([#29512](https://github.com/element-hq/element-web/pull/29512)). Contributed by @florianduros.
|
|
||||||
* Implement `MessagePreviewViewModel` ([#29514](https://github.com/element-hq/element-web/pull/29514)). Contributed by @MidhunSureshR.
|
|
||||||
* RoomListViewModel: Add functionality to toggle message preview setting ([#29511](https://github.com/element-hq/element-web/pull/29511)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: add more options menu on room list item ([#29445](https://github.com/element-hq/element-web/pull/29445)). Contributed by @florianduros.
|
|
||||||
* RoomListViewModel: Provide a way to resort the room list and track the active sort method ([#29499](https://github.com/element-hq/element-web/pull/29499)). Contributed by @MidhunSureshR.
|
|
||||||
* Change \*All rooms\* meta space name to \*All Chats\* ([#29498](https://github.com/element-hq/element-web/pull/29498)). Contributed by @florianduros.
|
|
||||||
* Add setting to hide avatars of rooms you have been invited to. ([#29497](https://github.com/element-hq/element-web/pull/29497)). Contributed by @Half-Shot.
|
|
||||||
* Room List Store: Save preferred sorting algorithm and use that on app launch ([#29493](https://github.com/element-hq/element-web/pull/29493)). Contributed by @MidhunSureshR.
|
|
||||||
* Add key storage toggle to Encryption settings ([#29310](https://github.com/element-hq/element-web/pull/29310)). Contributed by @dbkr.
|
|
||||||
* New room list: add primary filters ([#29481](https://github.com/element-hq/element-web/pull/29481)). Contributed by @florianduros.
|
|
||||||
* Implement MSC4142: Remove unintentional intentional mentions in replies ([#28209](https://github.com/element-hq/element-web/pull/28209)). Contributed by @tulir.
|
|
||||||
* White background for 'They do not match' button ([#29470](https://github.com/element-hq/element-web/pull/29470)). Contributed by @andybalaam.
|
|
||||||
* RoomListViewModel: Support secondary filters in the view model ([#29465](https://github.com/element-hq/element-web/pull/29465)). Contributed by @MidhunSureshR.
|
|
||||||
* RoomListViewModel: Support primary filters in the view model ([#29454](https://github.com/element-hq/element-web/pull/29454)). Contributed by @MidhunSureshR.
|
|
||||||
* Room List Store: Implement secondary filters ([#29458](https://github.com/element-hq/element-web/pull/29458)). Contributed by @MidhunSureshR.
|
|
||||||
* Room List Store: Implement rest of the primary filters ([#29444](https://github.com/element-hq/element-web/pull/29444)). Contributed by @MidhunSureshR.
|
|
||||||
* Room List Store: Support filters by implementing just the favourite filter ([#29433](https://github.com/element-hq/element-web/pull/29433)). Contributed by @MidhunSureshR.
|
|
||||||
* Move toggle switch for integration manager for a11y ([#29436](https://github.com/element-hq/element-web/pull/29436)). Contributed by @Half-Shot.
|
|
||||||
* New room list: basic flat list ([#29368](https://github.com/element-hq/element-web/pull/29368)). Contributed by @florianduros.
|
|
||||||
* Improve rageshake upload experience by providing useful error information ([#29378](https://github.com/element-hq/element-web/pull/29378)). Contributed by @Half-Shot.
|
|
||||||
* Add more functionality to the room list vm ([#29402](https://github.com/element-hq/element-web/pull/29402)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* New room list: fix compose menu action in space ([#29500](https://github.com/element-hq/element-web/pull/29500)). Contributed by @florianduros.
|
|
||||||
* Change ToggleHiddenEventVisibility \& GoToHome KeyBindingActions ([#29374](https://github.com/element-hq/element-web/pull/29374)). Contributed by @gy-mate.
|
|
||||||
* Fix Docker Healthcheck ([#29471](https://github.com/element-hq/element-web/pull/29471)). Contributed by @benbz.
|
|
||||||
* Room List Store: Fetch rooms after space store is ready + attach store to window ([#29453](https://github.com/element-hq/element-web/pull/29453)). Contributed by @MidhunSureshR.
|
|
||||||
* Room List Store: Fix bug where left rooms appear in room list ([#29452](https://github.com/element-hq/element-web/pull/29452)). Contributed by @MidhunSureshR.
|
|
||||||
* Add space to the bottom of the room summary actions below leave room ([#29270](https://github.com/element-hq/element-web/pull/29270)). Contributed by @langleyd.
|
|
||||||
* Show error screens in group calls ([#29254](https://github.com/element-hq/element-web/pull/29254)). Contributed by @robintown.
|
|
||||||
* Prevent user from accidentally triggering multiple identity resets ([#29388](https://github.com/element-hq/element-web/pull/29388)). Contributed by @uhoreg.
|
|
||||||
* Remove buggy tooltip on room intro \& homepage ([#29406](https://github.com/element-hq/element-web/pull/29406)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Room List Store: Filter rooms by active space ([#29399](https://github.com/element-hq/element-web/pull/29399)). Contributed by @MidhunSureshR.
|
|
||||||
* Room List - Update the room list store on actions from the dispatcher ([#29397](https://github.com/element-hq/element-web/pull/29397)). Contributed by @MidhunSureshR.
|
|
||||||
* Room List - Implement a minimal view model ([#29357](https://github.com/element-hq/element-web/pull/29357)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: add space menu in room header ([#29352](https://github.com/element-hq/element-web/pull/29352)). Contributed by @florianduros.
|
|
||||||
* Room List - Store sorted rooms in skip list ([#29345](https://github.com/element-hq/element-web/pull/29345)). Contributed by @MidhunSureshR.
|
|
||||||
* New room list: add dial to search section ([#29359](https://github.com/element-hq/element-web/pull/29359)). Contributed by @florianduros.
|
|
||||||
* New room list: add compose menu for spaces in header ([#29347](https://github.com/element-hq/element-web/pull/29347)). Contributed by @florianduros.
|
|
||||||
* Use EditInPlace control for Identity Server picker to improve a11y ([#29280](https://github.com/element-hq/element-web/pull/29280)). Contributed by @Half-Shot.
|
|
||||||
* First step to add header to new room list ([#29320](https://github.com/element-hq/element-web/pull/29320)). Contributed by @florianduros.
|
|
||||||
* Add Windows 64-bit arm link and remove 32-bit link on compatibility page ([#29312](https://github.com/element-hq/element-web/pull/29312)). Contributed by @t3chguy.
|
|
||||||
* Honour the backup disable flag from Element X ([#29290](https://github.com/element-hq/element-web/pull/29290)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix edited code block width ([#29394](https://github.com/element-hq/element-web/pull/29394)). Contributed by @florianduros.
|
|
||||||
* new room list: keep space name in one line in header ([#29369](https://github.com/element-hq/element-web/pull/29369)). Contributed by @florianduros.
|
|
||||||
* Dismiss "Key storage out of sync" toast when secrets received ([#29348](https://github.com/element-hq/element-web/pull/29348)). Contributed by @richvdh.
|
|
||||||
* Minor CSS fixes for the new room list ([#29334](https://github.com/element-hq/element-web/pull/29334)). Contributed by @florianduros.
|
|
||||||
* Add padding to room header icon ([#29271](https://github.com/element-hq/element-web/pull/29271)). Contributed by @langleyd.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.94](https://github.com/element-hq/element-web/releases/tag/v1.11.94) (2025-02-27)
|
|
||||||
==================================================================================================
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] fix: /tmp/element-web-config may already exist preventing the container from booting up ([#29377](https://github.com/element-hq/element-web/pull/29377)). Contributed by @RiotRobot.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [backport] Dynamically load Element Web modules in Docker entrypoint ([#29358](https://github.com/element-hq/element-web/pull/29358)). Contributed by @t3chguy.
|
|
||||||
* ChangeRecoveryKey: error handling ([#29262](https://github.com/element-hq/element-web/pull/29262)). Contributed by @richvdh.
|
|
||||||
* Dehydration: enable dehydrated device on "Set up recovery" ([#29265](https://github.com/element-hq/element-web/pull/29265)). Contributed by @richvdh.
|
|
||||||
* Render reason for invite rejection. ([#29257](https://github.com/element-hq/element-web/pull/29257)). Contributed by @Half-Shot.
|
|
||||||
* New room list: add search section ([#29251](https://github.com/element-hq/element-web/pull/29251)). Contributed by @florianduros.
|
|
||||||
* New room list: hide favourites and people meta spaces ([#29241](https://github.com/element-hq/element-web/pull/29241)). Contributed by @florianduros.
|
|
||||||
* New Room List: Create new labs flag ([#29239](https://github.com/element-hq/element-web/pull/29239)). Contributed by @MidhunSureshR.
|
|
||||||
* Stop URl preview from covering message box ([#29215](https://github.com/element-hq/element-web/pull/29215)). Contributed by @edent.
|
|
||||||
* Rename "security key" into "recovery key" ([#29217](https://github.com/element-hq/element-web/pull/29217)). Contributed by @florianduros.
|
|
||||||
* Add new verification section to user profile ([#29200](https://github.com/element-hq/element-web/pull/29200)). Contributed by @MidhunSureshR.
|
|
||||||
* Initial support for runtime modules ([#29104](https://github.com/element-hq/element-web/pull/29104)). Contributed by @t3chguy.
|
|
||||||
* Add `Forgot recovery key?` button to encryption tab ([#29202](https://github.com/element-hq/element-web/pull/29202)). Contributed by @florianduros.
|
|
||||||
* Add KeyIcon to key storage out of sync toast ([#29201](https://github.com/element-hq/element-web/pull/29201)). Contributed by @florianduros.
|
|
||||||
* Improve rendering of empty topics in the timeline ([#29152](https://github.com/element-hq/element-web/pull/29152)). Contributed by @Half-Shot.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix font scaling in member list ([#29285](https://github.com/element-hq/element-web/pull/29285)). Contributed by @florianduros.
|
|
||||||
* Grow member list search field when resizing the right panel ([#29267](https://github.com/element-hq/element-web/pull/29267)). Contributed by @langleyd.
|
|
||||||
* Don't reload roomview on offline connectivity check ([#29243](https://github.com/element-hq/element-web/pull/29243)). Contributed by @dbkr.
|
|
||||||
* Respect user's 12/24 hour preference consistently ([#29237](https://github.com/element-hq/element-web/pull/29237)). Contributed by @t3chguy.
|
|
||||||
* Restore the accessibility role on call views ([#29225](https://github.com/element-hq/element-web/pull/29225)). Contributed by @robintown.
|
|
||||||
* Revert `GoToHome` keyboard shortcut to `Ctrl`–`Shift`–`H` on macOS ([#28577](https://github.com/element-hq/element-web/pull/28577)). Contributed by @gy-mate.
|
|
||||||
* Encryption tab: display correct encryption panel when user cancels the reset identity flow ([#29216](https://github.com/element-hq/element-web/pull/29216)). Contributed by @florianduros.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [Backport staging] Log when we show, and hide, encryption setup toasts ([#29238](https://github.com/element-hq/element-web/pull/29238)). Contributed by @richvdh.
|
|
||||||
* Make profile header section match the designs ([#29163](https://github.com/element-hq/element-web/pull/29163)). Contributed by @MidhunSureshR.
|
|
||||||
* Always show back button in the right panel ([#29128](https://github.com/element-hq/element-web/pull/29128)). Contributed by @MidhunSureshR.
|
|
||||||
* Schedule dehydration on reload if the dehydration key is already cached locally ([#29021](https://github.com/element-hq/element-web/pull/29021)). Contributed by @uhoreg.
|
|
||||||
* update to twemoji 15.1.0 ([#29115](https://github.com/element-hq/element-web/pull/29115)). Contributed by @ara4n.
|
|
||||||
* Update matrix-widget-api ([#29112](https://github.com/element-hq/element-web/pull/29112)). Contributed by @toger5.
|
|
||||||
* Allow navigating through the memberlist using up/down keys ([#28949](https://github.com/element-hq/element-web/pull/28949)). Contributed by @MidhunSureshR.
|
|
||||||
* Style room header icons and facepile for toggled state ([#28968](https://github.com/element-hq/element-web/pull/28968)). Contributed by @MidhunSureshR.
|
|
||||||
* Move threads header below base card header ([#28969](https://github.com/element-hq/element-web/pull/28969)). Contributed by @MidhunSureshR.
|
|
||||||
* Add `Advanced` section to the user settings encryption tab ([#28804](https://github.com/element-hq/element-web/pull/28804)). Contributed by @florianduros.
|
|
||||||
* Fix outstanding UX issues with replies/mentions/keyword notifs ([#28270](https://github.com/element-hq/element-web/pull/28270)). Contributed by @taffyko.
|
|
||||||
* Distinguish room state and timeline events when dealing with widgets ([#28681](https://github.com/element-hq/element-web/pull/28681)). Contributed by @robintown.
|
|
||||||
* Switch OIDC primarily to new `/auth_metadata` API ([#29019](https://github.com/element-hq/element-web/pull/29019)). Contributed by @t3chguy.
|
|
||||||
* More memberlist changes ([#29069](https://github.com/element-hq/element-web/pull/29069)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast ([#29190](https://github.com/element-hq/element-web/pull/29190)). Contributed by @RiotRobot.
|
|
||||||
* Encryption tab: hide `Advanced` section when the key storage is out of sync ([#29129](https://github.com/element-hq/element-web/pull/29129)). Contributed by @florianduros.
|
|
||||||
* Fix share button in discovery settings being disabled incorrectly ([#29151](https://github.com/element-hq/element-web/pull/29151)). Contributed by @t3chguy.
|
|
||||||
* Ensure switching rooms does not wrongly focus timeline search ([#29153](https://github.com/element-hq/element-web/pull/29153)). Contributed by @t3chguy.
|
|
||||||
* Stop showing a dialog prompting the user to enter an old recovery key ([#29143](https://github.com/element-hq/element-web/pull/29143)). Contributed by @richvdh.
|
|
||||||
* Make themed widgets reflect the effective theme ([#28342](https://github.com/element-hq/element-web/pull/28342)). Contributed by @robintown.
|
|
||||||
* support non-VS16 emoji ligatures in TwemojiMozilla ([#29100](https://github.com/element-hq/element-web/pull/29100)). Contributed by @ara4n.
|
|
||||||
* e2e test: Verify session with the encryption tab instead of the security \& privacy tab ([#29090](https://github.com/element-hq/element-web/pull/29090)). Contributed by @florianduros.
|
|
||||||
* Work around cloudflare R2 / aws client incompatability ([#29086](https://github.com/element-hq/element-web/pull/29086)). Contributed by @dbkr.
|
|
||||||
* Fix identity server settings visibility ([#29083](https://github.com/element-hq/element-web/pull/29083)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Implement changes to memberlist from feedback ([#29029](https://github.com/element-hq/element-web/pull/29029)). Contributed by @MidhunSureshR.
|
|
||||||
* Add toast for recovery keys being out of sync ([#28946](https://github.com/element-hq/element-web/pull/28946)). Contributed by @dbkr.
|
|
||||||
* Refactor LegacyCallHandler event emitter to use TypedEventEmitter ([#29008](https://github.com/element-hq/element-web/pull/29008)). Contributed by @t3chguy.
|
|
||||||
* Add `Recovery` section in the new user settings `Encryption` tab ([#28673](https://github.com/element-hq/element-web/pull/28673)). Contributed by @florianduros.
|
|
||||||
* Retry loading chunks to make the app more resilient ([#29001](https://github.com/element-hq/element-web/pull/29001)). Contributed by @t3chguy.
|
|
||||||
* Clear account idb table on logout ([#28996](https://github.com/element-hq/element-web/pull/28996)). Contributed by @t3chguy.
|
|
||||||
* Implement new memberlist design with MVVM architecture ([#28874](https://github.com/element-hq/element-web/pull/28874)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Switch to secure random strings ([#29035](https://github.com/element-hq/element-web/pull/29035)). Contributed by @RiotRobot.
|
|
||||||
* React to MatrixEvent sender/target being updated for rendering state events ([#28947](https://github.com/element-hq/element-web/pull/28947)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|||||||
@@ -189,6 +189,89 @@ give away to contributors - if you feel that Matrix-branded apparel is missing
|
|||||||
from your life, please mail us your shipping address to matrix at matrix.org
|
from your life, please mail us your shipping address to matrix at matrix.org
|
||||||
and we'll try to fix it :)
|
and we'll try to fix it :)
|
||||||
|
|
||||||
|
## Sign off
|
||||||
|
|
||||||
|
In order to have a concrete record that your contribution is intentional
|
||||||
|
and you agree to license it under the same terms as the project's license, we've
|
||||||
|
adopted the same lightweight approach that the Linux Kernel
|
||||||
|
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
|
||||||
|
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||||
|
projects use: the DCO (Developer Certificate of Origin:
|
||||||
|
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||||
|
the contribution or otherwise have the right to contribute it to Matrix:
|
||||||
|
|
||||||
|
```
|
||||||
|
Developer Certificate of Origin
|
||||||
|
Version 1.1
|
||||||
|
|
||||||
|
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||||
|
660 York Street, Suite 102,
|
||||||
|
San Francisco, CA 94110 USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Developer's Certificate of Origin 1.1
|
||||||
|
|
||||||
|
By making a contribution to this project, I certify that:
|
||||||
|
|
||||||
|
(a) The contribution was created in whole or in part by me and I
|
||||||
|
have the right to submit it under the open source license
|
||||||
|
indicated in the file; or
|
||||||
|
|
||||||
|
(b) The contribution is based upon previous work that, to the best
|
||||||
|
of my knowledge, is covered under an appropriate open source
|
||||||
|
license and I have the right under that license to submit that
|
||||||
|
work with modifications, whether created in whole or in part
|
||||||
|
by me, under the same open source license (unless I am
|
||||||
|
permitted to submit under a different license), as indicated
|
||||||
|
in the file; or
|
||||||
|
|
||||||
|
(c) The contribution was provided directly to me by some other
|
||||||
|
person who certified (a), (b) or (c) and I have not modified
|
||||||
|
it.
|
||||||
|
|
||||||
|
(d) I understand and agree that this project and the contribution
|
||||||
|
are public and that a record of the contribution (including all
|
||||||
|
personal information I submit with it, including my sign-off) is
|
||||||
|
maintained indefinitely and may be redistributed consistent with
|
||||||
|
this project or the open source license(s) involved.
|
||||||
|
```
|
||||||
|
|
||||||
|
If you agree to this for your contribution, then all that's needed is to
|
||||||
|
include the line in your commit or pull request comment:
|
||||||
|
|
||||||
|
```
|
||||||
|
Signed-off-by: Your Name <your@email.example.org>
|
||||||
|
```
|
||||||
|
|
||||||
|
We accept contributions under a legally identifiable name, such as your name on
|
||||||
|
government documentation or common-law names (names claimed by legitimate usage
|
||||||
|
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||||
|
time.
|
||||||
|
|
||||||
|
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||||
|
`git commit`, which uses the name and email set in your `user.name` and
|
||||||
|
`user.email` git configs.
|
||||||
|
|
||||||
|
If you forgot to sign off your commits before making your pull request and are
|
||||||
|
on Git 2.17+ you can mass signoff using rebase:
|
||||||
|
|
||||||
|
```
|
||||||
|
git rebase --signoff origin/develop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Private sign off
|
||||||
|
|
||||||
|
If you would like to provide your legal name privately to the Matrix.org
|
||||||
|
Foundation (instead of in a public commit or comment), you can do so by emailing
|
||||||
|
your legal name and a link to the pull request to dco@matrix.org. It helps to
|
||||||
|
include "sign off" or similar in the subject line. You will then be instructed
|
||||||
|
further.
|
||||||
|
|
||||||
|
Once private sign off is complete, doing so for future contributions will not
|
||||||
|
be required.
|
||||||
|
|
||||||
# Review expectations
|
# Review expectations
|
||||||
|
|
||||||
See https://github.com/element-hq/element-meta/wiki/Review-process
|
See https://github.com/element-hq/element-meta/wiki/Review-process
|
||||||
|
|||||||
24
Dockerfile
24
Dockerfile
@@ -1,7 +1,5 @@
|
|||||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
|
||||||
|
|
||||||
# Builder
|
# Builder
|
||||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f7f28d1962d93cc096ea6327378d990284757fec281ce48e42436e7b4b167fa2 AS builder
|
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
|
||||||
|
|
||||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||||
ARG USE_CUSTOM_SDKS=false
|
ARG USE_CUSTOM_SDKS=false
|
||||||
@@ -10,7 +8,7 @@ ARG JS_SDK_BRANCH="master"
|
|||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY --exclude=docker . /src
|
COPY . /src
|
||||||
RUN /src/scripts/docker-link-repos.sh
|
RUN /src/scripts/docker-link-repos.sh
|
||||||
RUN yarn --network-timeout=200000 install
|
RUN yarn --network-timeout=200000 install
|
||||||
RUN /src/scripts/docker-package.sh
|
RUN /src/scripts/docker-package.sh
|
||||||
@@ -19,20 +17,20 @@ RUN /src/scripts/docker-package.sh
|
|||||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0d019e980f83728002de7a6d8819d0d4af7179046d3946b8b37749953fbb28e6
|
FROM nginx:alpine-slim
|
||||||
|
|
||||||
# Need root user to install packages & manipulate the usr directory
|
|
||||||
USER root
|
|
||||||
|
|
||||||
# Install jq and moreutils for sponge, both used by our entrypoints
|
|
||||||
RUN apk add jq moreutils
|
|
||||||
|
|
||||||
COPY --from=builder /src/webapp /app
|
COPY --from=builder /src/webapp /app
|
||||||
|
|
||||||
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
|
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
|
||||||
# through `envsubst` by the nginx docker image entry point.
|
# through `envsubst` by the nginx docker image entry point.
|
||||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
||||||
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
|
|
||||||
|
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
|
||||||
|
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
|
||||||
|
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
|
||||||
|
RUN chmod -R g+w /var/cache/nginx /etc/nginx
|
||||||
|
|
||||||
RUN rm -rf /usr/share/nginx/html \
|
RUN rm -rf /usr/share/nginx/html \
|
||||||
&& ln -s /app /usr/share/nginx/html
|
&& ln -s /app /usr/share/nginx/html
|
||||||
@@ -42,5 +40,3 @@ USER nginx
|
|||||||
|
|
||||||
# HTTP listen port
|
# HTTP listen port
|
||||||
ENV ELEMENT_WEB_PORT=80
|
ENV ELEMENT_WEB_PORT=80
|
||||||
|
|
||||||
HEALTHCHECK --start-period=5s CMD wget -q --spider http://localhost:$ELEMENT_WEB_PORT/config.json
|
|
||||||
|
|||||||
124
README.md
124
README.md
@@ -27,7 +27,7 @@ Element has several tiers of support for different environments:
|
|||||||
- Best effort
|
- Best effort
|
||||||
- Definition:
|
- Definition:
|
||||||
- Issues **accepted**, regressions **do not block** the release
|
- Issues **accepted**, regressions **do not block** the release
|
||||||
- The wider Element Products (including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
|
- The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
|
||||||
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
|
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
|
||||||
- Last major release of Firefox ESR and Chrome/Edge Extended Stable
|
- Last major release of Firefox ESR and Chrome/Edge Extended Stable
|
||||||
- Community Supported
|
- Community Supported
|
||||||
@@ -126,7 +126,7 @@ guide](https://classic.yarnpkg.com/en/docs/install) if you do not have it alread
|
|||||||
1. Install the prerequisites: `yarn install`.
|
1. Install the prerequisites: `yarn install`.
|
||||||
- If you're using the `develop` branch, then it is recommended to set up a
|
- If you're using the `develop` branch, then it is recommended to set up a
|
||||||
proper development environment (see [Setting up a dev
|
proper development environment (see [Setting up a dev
|
||||||
environment](./developer_guide.md#setting-up-a-dev-environment) below). Alternatively, you
|
environment](#setting-up-a-dev-environment) below). Alternatively, you
|
||||||
can use <https://develop.element.io> - the continuous integration release of
|
can use <https://develop.element.io> - the continuous integration release of
|
||||||
the develop branch.
|
the develop branch.
|
||||||
1. Configure the app by copying `config.sample.json` to `config.json` and
|
1. Configure the app by copying `config.sample.json` to `config.json` and
|
||||||
@@ -182,11 +182,123 @@ Dockerfile.
|
|||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
Please read through the following:
|
Before attempting to develop on Element you **must** read the [developer guide
|
||||||
|
for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk#developer-guide), which
|
||||||
|
also defines the design, architecture and style for Element too.
|
||||||
|
|
||||||
1. [Developer guide](./developer_guide.md)
|
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
||||||
2. [Code style](./code_style.md)
|
about where to start. Before starting work on a feature, it's best to ensure
|
||||||
3. [Contribution guide](./CONTRIBUTING.md)
|
your plan aligns well with our vision for Element. Please chat with the team in
|
||||||
|
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
||||||
|
you start so we can ensure it's something we'd be willing to merge.
|
||||||
|
|
||||||
|
You should also familiarise yourself with the ["Here be Dragons" guide
|
||||||
|
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
||||||
|
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
||||||
|
|
||||||
|
The idea of Element is to be a relatively lightweight "skin" of customisations on
|
||||||
|
top of the underlying `matrix-react-sdk`. `matrix-react-sdk` provides both the
|
||||||
|
higher and lower level React components useful for building Matrix communication
|
||||||
|
apps using React.
|
||||||
|
|
||||||
|
Please note that Element is intended to run correctly without access to the public
|
||||||
|
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
||||||
|
hosted by external CDNs or servers but instead please package all dependencies
|
||||||
|
into Element itself.
|
||||||
|
|
||||||
|
# Setting up a dev environment
|
||||||
|
|
||||||
|
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
||||||
|
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
||||||
|
in git and to make local changes without having to manually rebuild each time.
|
||||||
|
|
||||||
|
First clone and build `matrix-js-sdk`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||||
|
pushd matrix-js-sdk
|
||||||
|
yarn link
|
||||||
|
yarn install
|
||||||
|
popd
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone the repo and switch to the `element-web` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/element-hq/element-web.git
|
||||||
|
cd element-web
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the app by copying `config.sample.json` to `config.json` and
|
||||||
|
modifying it. See the [configuration docs](docs/config.md) for details.
|
||||||
|
|
||||||
|
Finally, build and start Element itself:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn link matrix-js-sdk
|
||||||
|
yarn install
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait a few seconds for the initial build to finish; you should see something like:
|
||||||
|
|
||||||
|
```
|
||||||
|
[element-js] <s> [webpack.Progress] 100%
|
||||||
|
[element-js]
|
||||||
|
[element-js] ℹ 「wdm」: 1840 modules
|
||||||
|
[element-js] ℹ 「wdm」: Compiled successfully.
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember, the command will not terminate since it runs the web server
|
||||||
|
and rebuilds source files when they change. This development server also
|
||||||
|
disables caching, so do NOT use it in production.
|
||||||
|
|
||||||
|
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
||||||
|
|
||||||
|
**Note**: The build script uses inotify by default on Linux to monitor directories
|
||||||
|
for changes. If the inotify limits are too low your build will fail silently or with
|
||||||
|
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
||||||
|
of at least `128M` and instance limit around `512`.
|
||||||
|
|
||||||
|
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
||||||
|
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
||||||
|
|
||||||
|
To set a new inotify watch and instance limit, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo sysctl fs.inotify.max_user_watches=131072
|
||||||
|
sudo sysctl fs.inotify.max_user_instances=512
|
||||||
|
sudo sysctl -p
|
||||||
|
```
|
||||||
|
|
||||||
|
If you wish, you can make the new limits permanent, by executing:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
||||||
|
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
||||||
|
sudo sysctl -p
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
||||||
|
|
||||||
|
If any of these steps error with, `file table overflow`, you are probably on a mac
|
||||||
|
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
||||||
|
You'll need to do this in each new terminal you open before building Element.
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
There are a number of application-level tests in the `tests` directory; these
|
||||||
|
are designed to run with Jest and JSDOM. To run them
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
### End-to-End tests
|
||||||
|
|
||||||
|
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,5 @@ module.exports = {
|
|||||||
|
|
||||||
"@babel/plugin-syntax-dynamic-import",
|
"@babel/plugin-syntax-dynamic-import",
|
||||||
"@babel/plugin-transform-runtime",
|
"@babel/plugin-transform-runtime",
|
||||||
["@babel/plugin-proposal-decorators", { version: "2023-11" }], // only needed by the js-sdk
|
|
||||||
"@babel/plugin-transform-class-static-block", // only needed by the js-sdk for decorators
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ adjacent to. As of writing, these are:
|
|||||||
|
|
||||||
- element-desktop
|
- element-desktop
|
||||||
- element-web
|
- element-web
|
||||||
|
- matrix-js-sdk
|
||||||
|
|
||||||
|
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
|
||||||
|
has stricter code organization to reduce the maintenance burden. These projects will declare their code
|
||||||
|
style within their own repos.
|
||||||
|
|
||||||
|
Note that some requirements will be layer-specific. Where the requirements don't make sense for the
|
||||||
|
project, they are used to the best of their ability, used in spirit, or ignored if not applicable,
|
||||||
|
in that order.
|
||||||
|
|
||||||
## Guiding principles
|
## Guiding principles
|
||||||
|
|
||||||
@@ -127,6 +136,7 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
|
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
|
||||||
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
|
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
|
||||||
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
|
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
|
||||||
|
|
||||||
1. Note that an explicit type is optional if not expected to be used outside of the function call,
|
1. Note that an explicit type is optional if not expected to be used outside of the function call,
|
||||||
unlike in this example:
|
unlike in this example:
|
||||||
|
|
||||||
@@ -160,6 +170,7 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
28. Export only what can be reused.
|
28. Export only what can be reused.
|
||||||
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
||||||
of truly optional parameters.
|
of truly optional parameters.
|
||||||
|
|
||||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
||||||
takes an argument that is more often not required than required. An example where the
|
takes an argument that is more often not required than required. An example where the
|
||||||
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||||
@@ -223,19 +234,17 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
|
|
||||||
Inheriting all the rules of TypeScript, the following additionally apply:
|
Inheriting all the rules of TypeScript, the following additionally apply:
|
||||||
|
|
||||||
1. Component source files are named with upper camel case (e.g. views/rooms/EventTile.js)
|
1. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
||||||
2. They are organised in a typically two-level hierarchy - first whether the component is a view or a structure, and then a broad functional grouping (e.g. 'rooms' here)
|
2. Class components must always have a `Props` interface declared immediately above them. It can be
|
||||||
3. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
|
||||||
4. Class components must always have a `Props` interface declared immediately above them. It can be
|
|
||||||
empty if the component accepts no props.
|
empty if the component accepts no props.
|
||||||
5. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
3. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
||||||
6. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
4. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
||||||
instead.
|
instead.
|
||||||
7. One component per file, except when a component is a utility component specifically for the "primary"
|
5. One component per file, except when a component is a utility component specifically for the "primary"
|
||||||
component. The utility component should not be exported.
|
component. The utility component should not be exported.
|
||||||
8. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
6. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
||||||
or stores.
|
or stores.
|
||||||
9. Stores should use a singleton pattern with a static instance property:
|
7. Stores should use a singleton pattern with a static instance property:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class FooStore {
|
class FooStore {
|
||||||
@@ -252,40 +261,44 @@ Inheriting all the rules of TypeScript, the following additionally apply:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
10. Stores must support using an alternative MatrixClient and dispatcher instance.
|
8. Stores must support using an alternative MatrixClient and dispatcher instance.
|
||||||
11. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
||||||
cycles during runtime where components accidentally include more of the app than they intended.
|
cycles during runtime where components accidentally include more of the app than they intended.
|
||||||
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
10. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||||
if at all possible.
|
if at all possible.
|
||||||
13. A component should only use CSS class names in line with the component name.
|
11. A component should only use CSS class names in line with the component name.
|
||||||
|
|
||||||
1. When knowingly using a class name from another component, document it with a [comment](#comments).
|
1. When knowingly using a class name from another component, document it with a [comment](#comments).
|
||||||
|
|
||||||
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
12. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
||||||
See above code example.
|
See above code example.
|
||||||
15. Functions used as properties should either be defined on the class or stored in a variable. They should not
|
13. Functions used as properties should either be defined on the class or stored in a variable. They should not
|
||||||
be inline unless mocking/short-circuiting the value.
|
be inline unless mocking/short-circuiting the value.
|
||||||
16. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
|
14. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
|
||||||
which should be used.
|
which should be used.
|
||||||
1. Unless the component is considered a "structure", in which case use classes.
|
1. Unless the component is considered a "structure", in which case use classes.
|
||||||
17. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
15. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
||||||
isolated components.
|
isolated components.
|
||||||
18. Components should serve a single, or near-single, purpose.
|
16. Components should serve a single, or near-single, purpose.
|
||||||
19. Prefer to derive information from component properties rather than establish state.
|
17. Prefer to derive information from component properties rather than establish state.
|
||||||
20. Do not use `React.Component::forceUpdate`.
|
18. Do not use `React.Component::forceUpdate`.
|
||||||
|
|
||||||
## Stylesheets (\*.pcss = PostCSS + Plugins)
|
## Stylesheets (\*.pcss = PostCSS + Plugins)
|
||||||
|
|
||||||
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
|
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
|
||||||
|
|
||||||
1. The view's CSS file MUST have the same name as the component (e.g. `view/rooms/_MessageTile.css` for `MessageTile.tsx` component).
|
1. Class names must be prefixed with "mx\_".
|
||||||
2. Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual.
|
2. Class names must denote the component which defines them, followed by any context.
|
||||||
3. Class names must be prefixed with "mx\_".
|
The context is not further specified here in terms of meaning or syntax.
|
||||||
4. Class names must strictly denote the component which defines them.
|
Use whatever is appropriate for your implementation use case.
|
||||||
For example: `mx_MyFoo` for `MyFoo` component.
|
Some examples:
|
||||||
5. Class names for DOM elements within a view which aren't components are named by appending a lower camel case identifier to the view's class name - e.g. .mx_MyFoo_randomDiv is how you'd name the class of an arbitrary div within the MyFoo view.
|
1. `mx_MyFoo`
|
||||||
6. Use the `$font` variables instead of manual values.
|
2. `mx_MyFoo_avatar`
|
||||||
7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
3. `mx_MyFoo_avatarUser`
|
||||||
8. Use the whole class name instead of shortcuts:
|
4. `mx_MyFoo_avatar--user`
|
||||||
|
3. Use the `$font` variables instead of manual values.
|
||||||
|
4. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
||||||
|
5. Use the whole class name instead of shortcuts:
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
.mx_MyFoo {
|
.mx_MyFoo {
|
||||||
@@ -296,7 +309,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
9. Break multiple selectors over multiple lines this way:
|
6. Break multiple selectors over multiple lines this way:
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
.mx_MyFoo,
|
.mx_MyFoo,
|
||||||
@@ -306,9 +319,9 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
10. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
7. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
||||||
11. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
||||||
[documented](#comments) for what the values mean:
|
[documented](#comments) for what the values mean:
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
.mx_MyFoo {
|
.mx_MyFoo {
|
||||||
@@ -318,9 +331,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
12. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
9. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
||||||
13. The CSS for a component can override the rules for child components. For instance, .mxRoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides must be scoped to the View's CSS class - i.e. don't just define .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override only to the context of RoomList views. N.B. overrides should be relatively rare as in general CSS inheritance should be enough.
|
|
||||||
14. Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are generally not cool and stop the component from being reused easily in different places.
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
@@ -385,6 +396,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
properties should be clearly documented.
|
properties should be clearly documented.
|
||||||
|
|
||||||
4. Inside a function, there is no need to comment every line, but consider:
|
4. Inside a function, there is no need to comment every line, but consider:
|
||||||
|
|
||||||
- before a particular multiline section of code within the function, give an overview of what it does,
|
- before a particular multiline section of code within the function, give an overview of what it does,
|
||||||
to make it easier for a reader to follow the flow through the function as a whole.
|
to make it easier for a reader to follow the flow through the function as a whole.
|
||||||
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
|
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"default_widget_container_height": 280,
|
"default_widget_container_height": 280,
|
||||||
"default_country_code": "GB",
|
"default_country_code": "GB",
|
||||||
|
|||||||
13
contribute.json
Normal file
13
contribute.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "Element",
|
||||||
|
"description": "A glossy Matrix collaboration client for the web.",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/element-hq/element-web",
|
||||||
|
"license": "AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"list": "https://github.com/element-hq/element-web/issues",
|
||||||
|
"report": "https://github.com/element-hq/element-web/issues/new/choose"
|
||||||
|
},
|
||||||
|
"keywords": ["chat", "riot", "matrix"]
|
||||||
|
}
|
||||||
2
debian/control
vendored
2
debian/control
vendored
@@ -8,6 +8,6 @@ Package: element-web
|
|||||||
Architecture: all
|
Architecture: all
|
||||||
Recommends: httpd, element-io-archive-keyring
|
Recommends: httpd, element-io-archive-keyring
|
||||||
Description:
|
Description:
|
||||||
Element: the future of secure communication
|
A feature-rich client for Matrix.org
|
||||||
This package contains the web-based client that can be served through a web
|
This package contains the web-based client that can be served through a web
|
||||||
server.
|
server.
|
||||||
|
|||||||
8
declaration.d.ts
vendored
8
declaration.d.ts
vendored
@@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare module "*.module.css";
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# Developer Guide
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
|
||||||
about where to start. Before starting work on a feature, it's best to ensure
|
|
||||||
your plan aligns well with our vision for Element. Please chat with the team in
|
|
||||||
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
|
||||||
you start so we can ensure it's something we'd be willing to merge.
|
|
||||||
|
|
||||||
You should also familiarise yourself with the ["Here be Dragons" guide
|
|
||||||
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
|
||||||
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
|
||||||
|
|
||||||
Please note that Element is intended to run correctly without access to the public
|
|
||||||
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
|
||||||
hosted by external CDNs or servers but instead please package all dependencies
|
|
||||||
into Element itself.
|
|
||||||
|
|
||||||
## Setting up a dev environment
|
|
||||||
|
|
||||||
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
|
||||||
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
|
||||||
in git and to make local changes without having to manually rebuild each time.
|
|
||||||
|
|
||||||
First clone and build `matrix-js-sdk`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
|
||||||
pushd matrix-js-sdk
|
|
||||||
yarn link
|
|
||||||
yarn install
|
|
||||||
popd
|
|
||||||
```
|
|
||||||
|
|
||||||
Clone the repo and switch to the `element-web` directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/element-hq/element-web.git
|
|
||||||
cd element-web
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure the app by copying `config.sample.json` to `config.json` and
|
|
||||||
modifying it. See the [configuration docs](docs/config.md) for details.
|
|
||||||
|
|
||||||
Finally, build and start Element itself:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn install
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
Wait a few seconds for the initial build to finish; you should see something like:
|
|
||||||
|
|
||||||
```
|
|
||||||
[element-js] <s> [webpack.Progress] 100%
|
|
||||||
[element-js]
|
|
||||||
[element-js] ℹ 「wdm」: 1840 modules
|
|
||||||
[element-js] ℹ 「wdm」: Compiled successfully.
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember, the command will not terminate since it runs the web server
|
|
||||||
and rebuilds source files when they change. This development server also
|
|
||||||
disables caching, so do NOT use it in production.
|
|
||||||
|
|
||||||
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
|
||||||
|
|
||||||
**Note**: The build script uses inotify by default on Linux to monitor directories
|
|
||||||
for changes. If the inotify limits are too low your build will fail silently or with
|
|
||||||
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
|
||||||
of at least `128M` and instance limit around `512`.
|
|
||||||
|
|
||||||
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
|
||||||
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
|
||||||
|
|
||||||
To set a new inotify watch and instance limit, execute:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo sysctl fs.inotify.max_user_watches=131072
|
|
||||||
sudo sysctl fs.inotify.max_user_instances=512
|
|
||||||
sudo sysctl -p
|
|
||||||
```
|
|
||||||
|
|
||||||
If you wish, you can make the new limits permanent, by executing:
|
|
||||||
|
|
||||||
```
|
|
||||||
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
|
||||||
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
|
||||||
sudo sysctl -p
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
|
||||||
|
|
||||||
If any of these steps error with, `file table overflow`, you are probably on a mac
|
|
||||||
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
|
||||||
You'll need to do this in each new terminal you open before building Element.
|
|
||||||
|
|
||||||
## Running the tests
|
|
||||||
|
|
||||||
There are a number of application-level tests in the `tests` directory; these
|
|
||||||
are designed to run with Jest and JSDOM. To run them
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn test
|
|
||||||
```
|
|
||||||
|
|
||||||
### End-to-End tests
|
|
||||||
|
|
||||||
See [`docs/playwright.md`](./docs/playwright.md) for how to run the end-to-end tests.
|
|
||||||
|
|
||||||
## General github guidelines
|
|
||||||
|
|
||||||
1. **Pull requests must only be filed against the `develop` branch.**
|
|
||||||
2. Try to keep your pull requests concise. Split them up if necessary.
|
|
||||||
3. Ensure that you provide a description that explains the fix/feature and its intent.
|
|
||||||
|
|
||||||
## Adding new code
|
|
||||||
|
|
||||||
New code should be committed as follows:
|
|
||||||
|
|
||||||
- All new components: https://github.com/element-hq/element-web/tree/develop/src/components
|
|
||||||
- CSS: https://github.com/element-hq/element-web/tree/develop/res/css
|
|
||||||
- Theme specific CSS & resources: https://github.com/element-hq/element-web/tree/develop/res/themes
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Loads modules from `/modules` into config.json's `modules` field
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
entrypoint_log() {
|
|
||||||
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
|
|
||||||
echo "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Copy these config files as a base
|
|
||||||
mkdir -p /tmp/element-web-config
|
|
||||||
cp /app/config*.json /tmp/element-web-config/
|
|
||||||
|
|
||||||
# If the module directory exists AND the module directory has modules in it
|
|
||||||
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
|
|
||||||
cd /modules
|
|
||||||
for MODULE in *
|
|
||||||
do
|
|
||||||
# If the module has a package.json, use its main field as the entrypoint
|
|
||||||
ENTRYPOINT="index.js"
|
|
||||||
if [ -f "/modules/$MODULE/package.json" ]; then
|
|
||||||
ENTRYPOINT=$(jq -r '.main' "/modules/$MODULE/package.json")
|
|
||||||
fi
|
|
||||||
|
|
||||||
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
|
|
||||||
|
|
||||||
# Append the module to the config
|
|
||||||
jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
@@ -18,12 +18,8 @@ server {
|
|||||||
}
|
}
|
||||||
# covers config.json and config.hostname.json requests as it is prefix.
|
# covers config.json and config.hostname.json requests as it is prefix.
|
||||||
location /config {
|
location /config {
|
||||||
root /tmp/element-web-config;
|
|
||||||
add_header Cache-Control "no-cache";
|
add_header Cache-Control "no-cache";
|
||||||
}
|
}
|
||||||
location /modules {
|
|
||||||
alias /modules;
|
|
||||||
}
|
|
||||||
# redirect server error pages to the static page /50x.html
|
# redirect server error pages to the static page /50x.html
|
||||||
#
|
#
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|||||||
67
docs/MVVM.md
67
docs/MVVM.md
@@ -1,67 +0,0 @@
|
|||||||
# MVVM
|
|
||||||
|
|
||||||
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
|
|
||||||
|
|
||||||
1. Model: This is where the business logic and data resides.
|
|
||||||
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
|
|
||||||
3. View: This is the UI code itself and depends on the view model.
|
|
||||||
|
|
||||||
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
|
|
||||||
|
|
||||||
### Practical guidelines for MVVM in element-web
|
|
||||||
|
|
||||||
#### Model
|
|
||||||
|
|
||||||
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
|
||||||
|
|
||||||
#### View Model
|
|
||||||
|
|
||||||
1. View model is always a custom react hook named like `useFooViewModel()`.
|
|
||||||
2. The return type of your view model (known as view state) must be defined as a typescript interface:
|
|
||||||
```ts
|
|
||||||
inteface FooViewState {
|
|
||||||
somethingUseful: string;
|
|
||||||
somethingElse: BarType;
|
|
||||||
update: () => Promise<void>
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. Any react state that your UI needs must be in the view model.
|
|
||||||
|
|
||||||
#### View
|
|
||||||
|
|
||||||
1. Views are simple react components (eg: `FooView`).
|
|
||||||
2. Views usually start by calling the view model hook, eg:
|
|
||||||
```tsx
|
|
||||||
const FooView: React.FC<IProps> = (props: IProps) => {
|
|
||||||
const vm = useFooViewModel();
|
|
||||||
....
|
|
||||||
return(
|
|
||||||
<div>
|
|
||||||
{vm.somethingUseful}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. Views are also allowed to accept the view model as a prop, eg:
|
|
||||||
```tsx
|
|
||||||
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
|
|
||||||
....
|
|
||||||
return(
|
|
||||||
<div>
|
|
||||||
{vm.somethingUseful}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
4. Multiple views can share the same view model if necessary.
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
|
|
||||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
|
||||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
|
||||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
|
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
- [Skinning](skinning.md)
|
- [Skinning](skinning.md)
|
||||||
- [Cider editor](ciderEditor.md)
|
- [Cider editor](ciderEditor.md)
|
||||||
- [Iconography](icons.md)
|
- [Iconography](icons.md)
|
||||||
|
- [Jitsi](jitsi.md)
|
||||||
- [Local echo](local-echo-dev.md)
|
- [Local echo](local-echo-dev.md)
|
||||||
- [Media](media-handling.md)
|
- [Media](media-handling.md)
|
||||||
- [Room List Store](room-list-store.md)
|
- [Room List Store](room-list-store.md)
|
||||||
|
|||||||
@@ -130,37 +130,32 @@ complete re-branding/private labeling, a more personalised experience can be ach
|
|||||||
6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.
|
6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.
|
||||||
7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to
|
7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to
|
||||||
download the app instead.
|
download the app instead.
|
||||||
8. `mobile_guide_app_variant`: Optional. The mobile app that the user is prompted to download from the `/mobile_guide` page. When omitted
|
8. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
|
||||||
the mobile guide will be configured for the new Element X apps. Allowed values are as follows:
|
|
||||||
1. `element`: Element X Android/iOS.
|
|
||||||
2. `element-classic`: Element Classic Android/iOS.
|
|
||||||
3. `element-pro`: Element Pro Android/iOS.
|
|
||||||
9. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
|
|
||||||
containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/`
|
containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/`
|
||||||
in production.
|
in production.
|
||||||
10. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
|
9. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
|
||||||
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
|
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
|
||||||
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
|
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
|
||||||
configuration found in the well-known location is used instead.
|
configuration found in the well-known location is used instead.
|
||||||
11. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
|
10. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
|
||||||
12. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
|
11. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
|
||||||
`{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
|
`{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
|
||||||
13. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
|
12. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
|
||||||
14. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
|
13. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
|
||||||
15. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
|
14. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
|
||||||
`true` to hide these options.
|
`true` to hide these options.
|
||||||
16. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
|
15. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
|
||||||
to hide this dropdown.
|
to hide this dropdown.
|
||||||
17. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
|
16. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
|
||||||
users. Set to `true` to disable this functionality.
|
users. Set to `true` to disable this functionality.
|
||||||
18. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
|
17. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
|
||||||
Takes a configuration object as below:
|
Takes a configuration object as below:
|
||||||
1. `title`: Required. Title to show at the top of the notice.
|
1. `title`: Required. Title to show at the top of the notice.
|
||||||
2. `description`: Required. The description to use for the notice.
|
2. `description`: Required. The description to use for the notice.
|
||||||
3. `show_once`: Optional. If true then the notice will only be shown once per device.
|
3. `show_once`: Optional. If true then the notice will only be shown once per device.
|
||||||
19. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
||||||
20. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
||||||
21. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
|
20. `force_verification`: If true, users must verify new logins (eg. with another device / their security key)
|
||||||
|
|
||||||
### `desktop_builds` and `mobile_builds`
|
### `desktop_builds` and `mobile_builds`
|
||||||
|
|
||||||
@@ -168,14 +163,14 @@ These two options describe the various availability for the application. When th
|
|||||||
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
|
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
|
||||||
at to see if the link should be to somewhere else.
|
at to see if the link should be to somewhere else.
|
||||||
|
|
||||||
Starting with `desktop_builds`, the following sub-properties are available:
|
Starting with `desktop_builds`, the following subproperties are available:
|
||||||
|
|
||||||
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
|
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
|
||||||
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
|
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
|
||||||
3. `url`: Required. The download URL for the app. This is used as a hyperlink.
|
3. `url`: Required. The download URL for the app. This is used as a hyperlink.
|
||||||
4. `url_macos`: Optional. Direct link to download macOS desktop app.
|
4. `url_macos`: Optional. Direct link to download macOS desktop app.
|
||||||
5. `url_win64`: Optional. Direct link to download Windows x86 64-bit desktop app.
|
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
|
||||||
6. `url_win64arm`: Optional. Direct link to download Windows ARM 64-bit desktop app.
|
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app.
|
||||||
7. `url_linux`: Optional. Direct link to download Linux desktop app.
|
7. `url_linux`: Optional. Direct link to download Linux desktop app.
|
||||||
|
|
||||||
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io
|
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io
|
||||||
@@ -389,6 +384,8 @@ The VoIP and Jitsi options are:
|
|||||||
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
||||||
at any time without notice.
|
at any time without notice.
|
||||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||||
|
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
|
||||||
|
and may be removed at any time without notice. Defaults to `https://call.element.io`.
|
||||||
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
||||||
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
||||||
- `participant_limit`: The maximum number of users who can join a call; if
|
- `participant_limit`: The maximum number of users who can join a call; if
|
||||||
@@ -450,7 +447,8 @@ If you would like to use Scalar, the integration manager maintained by Element,
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -594,4 +592,3 @@ The following are undocumented or intended for developer use only.
|
|||||||
2. `sync_timeline_limit`
|
2. `sync_timeline_limit`
|
||||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||||
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.
|
|
||||||
|
|||||||
@@ -66,18 +66,6 @@ on other runtimes may require root privileges. To resolve this, either run the
|
|||||||
image as root (`docker run --user 0`) or, better, change the port that nginx
|
image as root (`docker run --user 0`) or, better, change the port that nginx
|
||||||
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
||||||
|
|
||||||
[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
|
|
||||||
by being made available (e.g. via bind mount) in a directory within `/modules/`.
|
|
||||||
The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
|
|
||||||
These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
|
|
||||||
|
|
||||||
If you wish to use docker in read-only mode,
|
|
||||||
you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
|
|
||||||
but additionally include the following directories:
|
|
||||||
|
|
||||||
- /tmp/
|
|
||||||
- /etc/nginx/conf.d/
|
|
||||||
|
|
||||||
The behaviour of the docker image can be customised via the following
|
The behaviour of the docker image can be customised via the following
|
||||||
environment variables:
|
environment variables:
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
||||||
"defaultCountryCode": "GB",
|
"defaultCountryCode": "GB",
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ Under the hood this stops Element Web from adding the `perParticipantE2EE` flag
|
|||||||
|
|
||||||
This is useful while we experiment with encryption and to make calling compatible with platforms that don't use encryption yet.
|
This is useful while we experiment with encryption and to make calling compatible with platforms that don't use encryption yet.
|
||||||
|
|
||||||
|
## Rich text in room topics (`feature_html_topic`) [In Development]
|
||||||
|
|
||||||
|
Enables rendering of MD / HTML in room topics.
|
||||||
|
|
||||||
## Enable the notifications panel in the room header (`feature_notifications`)
|
## Enable the notifications panel in the room header (`feature_notifications`)
|
||||||
|
|
||||||
Unreliable in encrypted rooms.
|
Unreliable in encrypted rooms.
|
||||||
@@ -108,7 +112,3 @@ Unreliable in encrypted rooms.
|
|||||||
## Knock rooms (`feature_ask_to_join`) [In Development]
|
## Knock rooms (`feature_ask_to_join`) [In Development]
|
||||||
|
|
||||||
Enables knock feature for rooms. This allows users to ask to join a room.
|
Enables knock feature for rooms. This allows users to ask to join a room.
|
||||||
|
|
||||||
## New room list (`feature_new_room_list`) [In Development]
|
|
||||||
|
|
||||||
Enable the new room list that is currently in development.
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ flowchart TD
|
|||||||
|
|
||||||
subgraph Deploying
|
subgraph Deploying
|
||||||
D1[\Deploy staging.element.io/]
|
D1[\Deploy staging.element.io/]
|
||||||
D2[\Check docker build/]
|
D2[\Check dockerhub/]
|
||||||
D3[\Deploy app.element.io/]
|
D3[\Deploy app.element.io/]
|
||||||
D4[\Check desktop package/]
|
D4[\Check desktop package/]
|
||||||
|
|
||||||
@@ -213,10 +213,10 @@ switched back to the version of the dependency from the master branch to not lea
|
|||||||
# Deploying
|
# Deploying
|
||||||
|
|
||||||
We ship the SDKs to npm, this happens as part of the release process.
|
We ship the SDKs to npm, this happens as part of the release process.
|
||||||
We ship Element Web to dockerhub, ghcr.io, `*.element.io`, and packages.element.io.
|
We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
|
||||||
We ship Element Desktop to packages.element.io.
|
We ship Element Desktop to packages.element.io.
|
||||||
|
|
||||||
- [ ] Check that element-web has shipped to dockerhub & ghcr.io
|
- [ ] Check that element-web has shipped to dockerhub
|
||||||
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
|
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
|
||||||
- [ ] Test staging.element.io
|
- [ ] Test staging.element.io
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
||||||
"uisi_autorageshake_app": "element-auto-uisi",
|
"uisi_autorageshake_app": "element-auto-uisi",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
||||||
"uisi_autorageshake_app": "element-auto-uisi",
|
"uisi_autorageshake_app": "element-auto-uisi",
|
||||||
|
|||||||
@@ -17,13 +17,11 @@ const config: Config = {
|
|||||||
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
||||||
customExportConditions: ["browser", "node"],
|
customExportConditions: ["browser", "node"],
|
||||||
},
|
},
|
||||||
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/src/shared-components/**/*.test.[t]s?(x)"],
|
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
|
||||||
globalSetup: "<rootDir>/test/globalSetup.ts",
|
globalSetup: "<rootDir>/test/globalSetup.ts",
|
||||||
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
// Support CSS module
|
|
||||||
"\\.(module.css)$": "identity-obj-proxy",
|
|
||||||
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
||||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||||
|
|||||||
14
knip.ts
14
knip.ts
@@ -2,6 +2,7 @@ import { KnipConfig } from "knip";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
entry: [
|
entry: [
|
||||||
|
"src/vector/index.ts",
|
||||||
"src/serviceworker/index.ts",
|
"src/serviceworker/index.ts",
|
||||||
"src/workers/*.worker.ts",
|
"src/workers/*.worker.ts",
|
||||||
"src/utils/exportUtils/exportJS.js",
|
"src/utils/exportUtils/exportJS.js",
|
||||||
@@ -11,12 +12,13 @@ export default {
|
|||||||
"res/decoder-ring/**",
|
"res/decoder-ring/**",
|
||||||
"res/jitsi_external_api.min.js",
|
"res/jitsi_external_api.min.js",
|
||||||
"docs/**",
|
"docs/**",
|
||||||
|
// Used by jest
|
||||||
|
"__mocks__/maplibre-gl.js",
|
||||||
],
|
],
|
||||||
project: ["**/*.{js,ts,jsx,tsx}"],
|
project: ["**/*.{js,ts,jsx,tsx}"],
|
||||||
ignore: [
|
ignore: [
|
||||||
// Keep for now
|
// Keep for now
|
||||||
"src/hooks/useLocalStorageState.ts",
|
"src/hooks/useLocalStorageState.ts",
|
||||||
"src/hooks/useTimeout.ts",
|
|
||||||
"src/components/views/elements/InfoTooltip.tsx",
|
"src/components/views/elements/InfoTooltip.tsx",
|
||||||
"src/components/views/elements/StyledCheckbox.tsx",
|
"src/components/views/elements/StyledCheckbox.tsx",
|
||||||
],
|
],
|
||||||
@@ -37,20 +39,10 @@ export default {
|
|||||||
// Used by webpack
|
// Used by webpack
|
||||||
"process",
|
"process",
|
||||||
"util",
|
"util",
|
||||||
// Embedded into webapp
|
|
||||||
"@element-hq/element-call-embedded",
|
|
||||||
|
|
||||||
// Used by matrix-js-sdk, which means we have to include them as a
|
|
||||||
// dependency so that // we can run `tsc` (since we import the typescript
|
|
||||||
// source of js-sdk, rather than the transpiled and annotated JS like you
|
|
||||||
// would with a normal library).
|
|
||||||
"@types/content-type",
|
|
||||||
"@types/sdp-transform",
|
|
||||||
],
|
],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// Used in scripts & workflows
|
// Used in scripts & workflows
|
||||||
"jq",
|
"jq",
|
||||||
"wait-on",
|
|
||||||
],
|
],
|
||||||
ignoreExportsUsedInFile: true,
|
ignoreExportsUsedInFile: true,
|
||||||
} satisfies KnipConfig;
|
} satisfies KnipConfig;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import * as fs from "fs";
|
|||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
import * as semver from "semver";
|
import * as semver from "semver";
|
||||||
|
|
||||||
import { type BuildConfig } from "./BuildConfig";
|
import { BuildConfig } from "./BuildConfig";
|
||||||
|
|
||||||
// This expects to be run from ./scripts/install.ts
|
// This expects to be run from ./scripts/install.ts
|
||||||
|
|
||||||
@@ -23,9 +23,10 @@ const MODULES_TS_HEADER = `
|
|||||||
* You are not a salmon.
|
* You are not a salmon.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
||||||
`;
|
`;
|
||||||
const MODULES_TS_DEFINITIONS = `
|
const MODULES_TS_DEFINITIONS = `
|
||||||
export const INSTALLED_MODULES = [];
|
export const INSTALLED_MODULES: RuntimeModule[] = [];
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function installer(config: BuildConfig): void {
|
export function installer(config: BuildConfig): void {
|
||||||
@@ -77,8 +78,8 @@ export function installer(config: BuildConfig): void {
|
|||||||
return; // hit the finally{} block before exiting
|
return; // hit the finally{} block before exiting
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we reach here, everything seems fine. Write modules.js and log some output
|
// If we reach here, everything seems fine. Write modules.ts and log some output
|
||||||
// Note: we compile modules.js in two parts for developer friendliness if they
|
// Note: we compile modules.ts in two parts for developer friendliness if they
|
||||||
// happen to look at it.
|
// happen to look at it.
|
||||||
console.log("The following modules have been installed: ", installedModules);
|
console.log("The following modules have been installed: ", installedModules);
|
||||||
let modulesTsHeader = MODULES_TS_HEADER;
|
let modulesTsHeader = MODULES_TS_HEADER;
|
||||||
@@ -192,5 +193,5 @@ function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeModulesTs(content: string): void {
|
function writeModulesTs(content: string): void {
|
||||||
fs.writeFileSync("./src/modules.js", content, "utf-8");
|
fs.writeFileSync("./src/modules.ts", content, "utf-8");
|
||||||
}
|
}
|
||||||
|
|||||||
139
package.json
139
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.11.110",
|
"version": "1.11.90",
|
||||||
"description": "Element: the future of secure communication",
|
"description": "A feature-rich client for Matrix.org",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
"AUTHORS.rst",
|
"AUTHORS.rst",
|
||||||
"package.json"
|
"package.json",
|
||||||
|
"contribute.json"
|
||||||
],
|
],
|
||||||
"style": "bundle.css",
|
"style": "bundle.css",
|
||||||
"matrix_i18n_extra_translation_funcs": [
|
"matrix_i18n_extra_translation_funcs": [
|
||||||
@@ -61,44 +62,37 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:playwright": "playwright test",
|
"test:playwright": "playwright test",
|
||||||
"test:playwright:open": "yarn test:playwright --ui",
|
"test:playwright:open": "yarn test:playwright --ui",
|
||||||
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
||||||
|
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
|
||||||
|
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome",
|
||||||
"coverage": "yarn test --coverage",
|
"coverage": "yarn test --coverage",
|
||||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
|
||||||
"postinstall": "patch-package",
|
|
||||||
"storybook": "storybook dev -p 6007",
|
|
||||||
"build-storybook": "storybook build",
|
|
||||||
"test:storybook": "test-storybook --url http://localhost:6007/",
|
|
||||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
|
|
||||||
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/pretty-format/react-is": "19.1.1",
|
"@types/react": "18.3.18",
|
||||||
"@playwright/test": "1.54.2",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/react": "19.1.12",
|
"oidc-client-ts": "3.1.0",
|
||||||
"@types/react-dom": "19.1.9",
|
|
||||||
"oidc-client-ts": "3.3.0",
|
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"caniuse-lite": "1.0.30001724",
|
"caniuse-lite": "1.0.30001692",
|
||||||
"testcontainers": "^11.0.0",
|
|
||||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@element-hq/element-web-module-api": "1.4.1",
|
|
||||||
"@fontsource/inconsolata": "^5",
|
"@fontsource/inconsolata": "^5",
|
||||||
"@fontsource/inter": "^5",
|
"@fontsource/inter": "^5",
|
||||||
"@formatjs/intl-segmenter": "^11.5.7",
|
"@formatjs/intl-segmenter": "^11.5.7",
|
||||||
"@matrix-org/analytics-events": "^0.29.2",
|
"@matrix-org/analytics-events": "^0.29.0",
|
||||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
"@matrix-org/emojibase-bindings": "^1.3.3",
|
||||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||||
"@matrix-org/spec": "^1.7.0",
|
"@matrix-org/spec": "^1.7.0",
|
||||||
"@sentry/browser": "^10.0.0",
|
"@sentry/browser": "^8.0.0",
|
||||||
"@types/png-chunks-extract": "^1.0.2",
|
"@types/png-chunks-extract": "^1.0.2",
|
||||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
"@types/react-virtualized": "^9.21.30",
|
||||||
"@vector-im/compound-web": "^8.1.2",
|
"@vector-im/compound-design-tokens": "^2.1.0",
|
||||||
"@vector-im/matrix-wysiwyg": "2.39.0",
|
"@vector-im/compound-web": "^7.5.0",
|
||||||
|
"@vector-im/matrix-wysiwyg": "2.38.0",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||||
@@ -112,67 +106,64 @@
|
|||||||
"css-tree": "^3.0.0",
|
"css-tree": "^3.0.0",
|
||||||
"diff-dom": "^5.0.0",
|
"diff-dom": "^5.0.0",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"domutils": "^3.2.2",
|
|
||||||
"emojibase-regex": "15.3.2",
|
"emojibase-regex": "15.3.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "11.0.2",
|
"filesize": "10.1.6",
|
||||||
"github-markdown-css": "^5.5.1",
|
"github-markdown-css": "^5.5.1",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.3.1",
|
||||||
"html-entities": "^2.0.0",
|
"html-entities": "^2.0.0",
|
||||||
"html-react-parser": "^5.2.2",
|
|
||||||
"is-ip": "^3.1.0",
|
"is-ip": "^3.1.0",
|
||||||
"js-xxhash": "^4.0.0",
|
"js-xxhash": "^4.0.0",
|
||||||
"jsrsasign": "^11.0.0",
|
"jsrsasign": "^11.0.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.16.0",
|
"katex": "^0.16.0",
|
||||||
"linkify-react": "4.3.2",
|
"linkify-element": "4.2.0",
|
||||||
"linkify-string": "4.3.2",
|
"linkify-react": "4.2.0",
|
||||||
"linkifyjs": "4.3.2",
|
"linkify-string": "4.2.0",
|
||||||
|
"linkifyjs": "4.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"maplibre-gl": "^5.0.0",
|
"maplibre-gl": "^5.0.0",
|
||||||
"matrix-encrypt-attachment": "^1.0.3",
|
"matrix-encrypt-attachment": "^1.0.3",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"matrix-widget-api": "^1.10.0",
|
"matrix-widget-api": "1.11.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"oidc-client-ts": "^3.0.1",
|
"oidc-client-ts": "^3.0.1",
|
||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.261.0",
|
"posthog-js": "1.157.2",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"re-resizable": "6.11.2",
|
"re-resizable": "6.10.3",
|
||||||
"react": "^19.0.0",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-blurhash": "^0.3.0",
|
"react-blurhash": "^0.3.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-focus-lock": "^2.5.1",
|
"react-focus-lock": "^2.5.1",
|
||||||
"react-string-replace": "^1.1.1",
|
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"react-virtuoso": "^4.14.0",
|
"react-virtualized": "^9.22.5",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.14.0",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"temporal-polyfill": "^0.3.0",
|
"temporal-polyfill": "^0.2.5",
|
||||||
"ua-parser-js": "1.0.40",
|
"ua-parser-js": "^1.0.2",
|
||||||
"uuid": "^11.0.0",
|
"uuid": "^11.0.0",
|
||||||
"what-input": "^5.2.10"
|
"what-input": "^5.2.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@action-validator/cli": "^0.6.0",
|
"@action-validator/cli": "^0.6.0",
|
||||||
"@action-validator/core": "^0.6.0",
|
"@action-validator/core": "^0.6.0",
|
||||||
|
"@axe-core/playwright": "^4.8.1",
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@babel/eslint-parser": "^7.12.10",
|
"@babel/eslint-parser": "^7.12.10",
|
||||||
"@babel/eslint-plugin": "^7.12.10",
|
"@babel/eslint-plugin": "^7.12.10",
|
||||||
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
||||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/plugin-transform-class-properties": "^7.12.1",
|
"@babel/plugin-transform-class-properties": "^7.12.1",
|
||||||
"@babel/plugin-transform-class-static-block": "^7.26.0",
|
|
||||||
"@babel/plugin-transform-logical-assignment-operators": "^7.20.7",
|
"@babel/plugin-transform-logical-assignment-operators": "^7.20.7",
|
||||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
|
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
|
||||||
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
||||||
@@ -184,26 +175,18 @@
|
|||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||||
"@element-hq/element-call-embedded": "0.15.0",
|
|
||||||
"@element-hq/element-web-playwright-common": "^1.4.6",
|
|
||||||
"@peculiar/webcrypto": "^1.4.3",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.40.1",
|
||||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||||
"@rrweb/types": "^2.0.0-alpha.18",
|
"@sentry/webpack-plugin": "^3.0.0",
|
||||||
"@sentry/webpack-plugin": "^4.0.0",
|
"@stylistic/eslint-plugin": "^2.9.0",
|
||||||
"@storybook/addon-designs": "^10.0.1",
|
|
||||||
"@storybook/addon-docs": "^9.0.12",
|
|
||||||
"@storybook/icons": "^1.4.0",
|
|
||||||
"@storybook/react-vite": "^9.0.15",
|
|
||||||
"@storybook/test-runner": "^0.23.0",
|
|
||||||
"@stylistic/eslint-plugin": "^5.0.0",
|
|
||||||
"@svgr/webpack": "^8.0.0",
|
"@svgr/webpack": "^8.0.0",
|
||||||
|
"@testcontainers/postgresql": "^10.16.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.4.8",
|
"@testing-library/jest-dom": "^6.4.8",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/commonmark": "^0.27.4",
|
"@types/commonmark": "^0.27.4",
|
||||||
"@types/content-type": "^1.1.9",
|
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/css-tree": "^2.3.8",
|
"@types/css-tree": "^2.3.8",
|
||||||
"@types/diff-match-patch": "^1.0.32",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
@@ -222,12 +205,11 @@
|
|||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.2",
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^2.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "19.1.12",
|
"@types/react": "18.3.18",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "2.16.0",
|
"@types/sanitize-html": "2.13.0",
|
||||||
"@types/sdp-transform": "^2.4.10",
|
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/tar-js": "^0.3.5",
|
"@types/tar-js": "^0.3.5",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
@@ -235,17 +217,17 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||||
"@typescript-eslint/parser": "^8.19.0",
|
"@typescript-eslint/parser": "^8.19.0",
|
||||||
"babel-jest": "^29.0.0",
|
"babel-jest": "^29.0.0",
|
||||||
"babel-loader": "^10.0.0",
|
"babel-loader": "^9.0.0",
|
||||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||||
"blob-polyfill": "^9.0.0",
|
"blob-polyfill": "^9.0.0",
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"concurrently": "^9.0.0",
|
"concurrently": "^9.0.0",
|
||||||
"copy-webpack-plugin": "^13.0.0",
|
"copy-webpack-plugin": "^12.0.0",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.38.1",
|
||||||
"cronstrue": "^3.0.0",
|
"cronstrue": "^2.41.0",
|
||||||
"css-loader": "^7.0.0",
|
"css-loader": "^7.0.0",
|
||||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
"dotenv": "^17.0.0",
|
"dotenv": "^16.0.2",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
@@ -257,60 +239,56 @@
|
|||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-storybook": "^9.0.12",
|
|
||||||
"eslint-plugin-unicorn": "^56.0.0",
|
"eslint-plugin-unicorn": "^56.0.0",
|
||||||
"express": "^5.0.0",
|
"express": "^4.18.2",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"fetch-mock-jest": "^1.5.1",
|
"fetch-mock-jest": "^1.5.1",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
|
"glob": "^11.0.0",
|
||||||
"html-webpack-plugin": "^5.5.3",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
|
||||||
"jest": "^29.6.2",
|
"jest": "^29.6.2",
|
||||||
"jest-canvas-mock": "^2.5.2",
|
"jest-canvas-mock": "^2.5.2",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-image-snapshot": "^6.5.1",
|
|
||||||
"jest-mock": "^29.6.2",
|
"jest-mock": "^29.6.2",
|
||||||
"jest-raw-loader": "^1.0.1",
|
"jest-raw-loader": "^1.0.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"knip": "^5.36.2",
|
"knip": "^5.36.2",
|
||||||
"lint-staged": "^16.0.0",
|
"lint-staged": "^15.0.2",
|
||||||
|
"mailhog": "^4.16.0",
|
||||||
"matrix-web-i18n": "^3.2.1",
|
"matrix-web-i18n": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"minimist": "^1.2.6",
|
"minimist": "^1.2.6",
|
||||||
"modernizr": "^3.12.0",
|
"modernizr": "^3.12.0",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"patch-package": "^8.0.0",
|
"playwright-core": "^1.45.1",
|
||||||
"playwright-core": "^1.51.0",
|
|
||||||
"postcss": "8.4.46",
|
"postcss": "8.4.46",
|
||||||
"postcss-easings": "^4.0.0",
|
"postcss-easings": "^4.0.0",
|
||||||
"postcss-hexrgba": "2.1.0",
|
"postcss-hexrgba": "2.1.0",
|
||||||
"postcss-import": "16.1.0",
|
"postcss-import": "16.1.0",
|
||||||
"postcss-loader": "8.1.1",
|
"postcss-loader": "8.1.1",
|
||||||
"postcss-mixins": "^12.0.0",
|
"postcss-mixins": "^11.0.0",
|
||||||
"postcss-nested": "^7.0.0",
|
"postcss-nested": "^7.0.0",
|
||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
"postcss-scss": "^4.0.4",
|
"postcss-scss": "^4.0.4",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.4.2",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
"semver": "^7.5.2",
|
"semver": "^7.5.2",
|
||||||
"source-map-loader": "^5.0.0",
|
"source-map-loader": "^5.0.0",
|
||||||
"storybook": "^9.0.12",
|
"strip-ansi": "^7.1.0",
|
||||||
"stylelint": "^16.23.0",
|
"stylelint": "^16.1.0",
|
||||||
"stylelint-config-standard": "^39.0.0",
|
"stylelint-config-standard": "^36.0.0",
|
||||||
"stylelint-scss": "^6.0.0",
|
"stylelint-scss": "^6.0.0",
|
||||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||||
"terser-webpack-plugin": "^5.3.9",
|
"terser-webpack-plugin": "^5.3.9",
|
||||||
"testcontainers": "^11.0.0",
|
"testcontainers": "^10.16.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.7.3",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vite": "^7.0.1",
|
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
|
||||||
"web-streams-polyfill": "^4.0.0",
|
"web-streams-polyfill": "^4.0.0",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
@@ -327,6 +305,5 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
|
||||||
index 917a7fc..a2710c6 100644
|
|
||||||
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
|
||||||
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
|
||||||
@@ -37,7 +37,7 @@ export interface ModuleApi {
|
|
||||||
* @returns Whether the user submitted the dialog or closed it, and the model returned by the
|
|
||||||
* dialog component if submitted.
|
|
||||||
*/
|
|
||||||
- openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
|
||||||
+ openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C | null>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
|
||||||
didOkOrSubmit: boolean;
|
|
||||||
model: M;
|
|
||||||
}>;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
diff --git a/node_modules/@types/mdx/types.d.ts b/node_modules/@types/mdx/types.d.ts
|
|
||||||
index 498bb69..4e89216 100644
|
|
||||||
--- a/node_modules/@types/mdx/types.d.ts
|
|
||||||
+++ b/node_modules/@types/mdx/types.d.ts
|
|
||||||
@@ -5,7 +5,7 @@
|
|
||||||
*/
|
|
||||||
// @ts-ignore JSX runtimes may optionally define JSX.ElementType. The MDX types need to work regardless whether this is
|
|
||||||
// defined or not.
|
|
||||||
-type ElementType = any extends JSX.ElementType ? never : JSX.ElementType;
|
|
||||||
+type ElementType = any extends JSX.ElementType ? never : React.JSX.ElementType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This matches any function component types that ar part of `ElementType`.
|
|
||||||
@@ -20,12 +20,12 @@ type ClassElementType = Extract<ElementType, new(props: Record<string, any>) =>
|
|
||||||
/**
|
|
||||||
* A valid JSX string component.
|
|
||||||
*/
|
|
||||||
-type StringComponent = Extract<keyof JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
|
|
||||||
+type StringComponent = Extract<keyof React.JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A JSX element returned by MDX content.
|
|
||||||
*/
|
|
||||||
-export type Element = JSX.Element;
|
|
||||||
+export type Element = React.JSX.Element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A valid JSX function component.
|
|
||||||
@@ -44,7 +44,7 @@ type FunctionComponent<Props> = ElementType extends never
|
|
||||||
*/
|
|
||||||
type ClassComponent<Props> = ElementType extends never
|
|
||||||
// If JSX.ElementType isn’t defined, the valid return type is a constructor that returns JSX.ElementClass
|
|
||||||
- ? new(props: Props) => JSX.ElementClass
|
|
||||||
+ ? new(props: Props) => React.JSX.ElementClass
|
|
||||||
: ClassElementType extends never
|
|
||||||
// If JSX.ElementType is defined, but doesn’t allow constructors, function components are disallowed.
|
|
||||||
? never
|
|
||||||
@@ -70,7 +70,7 @@ interface NestedMDXComponents {
|
|
||||||
export type MDXComponents =
|
|
||||||
& NestedMDXComponents
|
|
||||||
& {
|
|
||||||
- [Key in StringComponent]?: Component<JSX.IntrinsicElements[Key]>;
|
|
||||||
+ [Key in StringComponent]?: Component<React.JSX.IntrinsicElements[Key]>;
|
|
||||||
}
|
|
||||||
& {
|
|
||||||
/**
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
diff --git a/node_modules/react-blurhash/dist/index.d.ts b/node_modules/react-blurhash/dist/index.d.ts
|
|
||||||
index 3adbd0a..32e8c13 100644
|
|
||||||
--- a/node_modules/react-blurhash/dist/index.d.ts
|
|
||||||
+++ b/node_modules/react-blurhash/dist/index.d.ts
|
|
||||||
@@ -19,7 +19,7 @@ declare class Blurhash extends React.PureComponent<Props$1> {
|
|
||||||
resolutionY: number;
|
|
||||||
};
|
|
||||||
componentDidUpdate(): void;
|
|
||||||
- render(): JSX.Element;
|
|
||||||
+ render(): React.JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare type Props = React.CanvasHTMLAttributes<HTMLCanvasElement> & {
|
|
||||||
@@ -37,7 +37,7 @@ declare class BlurhashCanvas extends React.PureComponent<Props> {
|
|
||||||
componentDidUpdate(): void;
|
|
||||||
handleRef: (canvas: HTMLCanvasElement) => void;
|
|
||||||
draw: () => void;
|
|
||||||
- render(): JSX.Element;
|
|
||||||
+ render(): React.JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Blurhash, BlurhashCanvas };
|
|
||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
import { type WorkerOptions } from "./playwright/services";
|
import { Options } from "./playwright/services";
|
||||||
|
|
||||||
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ const chromeProject = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig<WorkerOptions>({
|
export default defineConfig<Options>({
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "Chrome",
|
name: "Chrome",
|
||||||
@@ -83,7 +83,6 @@ export default defineConfig<WorkerOptions>({
|
|||||||
url: `${baseURL}/config.json`,
|
url: `${baseURL}/config.json`,
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
timeout: (process.env.CI ? 30 : 120) * 1000,
|
timeout: (process.env.CI ? 30 : 120) * 1000,
|
||||||
stdout: "pipe",
|
|
||||||
},
|
},
|
||||||
testDir: "playwright/e2e",
|
testDir: "playwright/e2e",
|
||||||
outputDir: "playwright/test-results",
|
outputDir: "playwright/test-results",
|
||||||
|
|||||||
12
playwright/@types/playwright-core.d.ts
vendored
Normal file
12
playwright/@types/playwright-core.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module "playwright-core/lib/utils" {
|
||||||
|
// This type is not public in playwright-core utils
|
||||||
|
export function sanitizeForFilePath(filePath: string): string;
|
||||||
|
}
|
||||||
9
playwright/Dockerfile
Normal file
9
playwright/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM mcr.microsoft.com/playwright:v1.49.1-noble
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
# fonts-dejavu is needed for the same RTL rendering as on CI
|
||||||
|
RUN apt-get update && apt-get -y install docker.io fonts-dejavu
|
||||||
|
|
||||||
|
COPY docker-entrypoint.sh /opt/docker-entrypoint.sh
|
||||||
|
ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"]
|
||||||
5
playwright/docker-entrypoint.sh
Normal file
5
playwright/docker-entrypoint.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
npx playwright test --update-snapshots --reporter line $@
|
||||||
@@ -11,7 +11,7 @@ import type { Locator, Page } from "@playwright/test";
|
|||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
import { Layout } from "../../../src/settings/enums/Layout";
|
import { Layout } from "../../../src/settings/enums/Layout";
|
||||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
|
|
||||||
// Find and click "Reply" button
|
// Find and click "Reply" button
|
||||||
const clickButtonReply = async (tile: Locator) => {
|
const clickButtonReply = async (tile: Locator) => {
|
||||||
@@ -19,7 +19,6 @@ const clickButtonReply = async (tile: Locator) => {
|
|||||||
await tile.hover();
|
await tile.hover();
|
||||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||||
}).toPass();
|
}).toPass();
|
||||||
await expect(tile.page().getByText("Replying", { exact: true })).toBeVisible();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||||
@@ -40,7 +39,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// wait for the tile to finish loading
|
// wait for the tile to finish loading
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.getByTestId("audio-player-name")
|
.locator(".mx_AudioPlayer_mediaName")
|
||||||
.last()
|
.last()
|
||||||
.filter({ hasText: file.split("/").at(-1) }),
|
.filter({ hasText: file.split("/").at(-1) }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -55,10 +54,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// Check that the audio player is rendered and its button becomes visible
|
// Check that the audio player is rendered and its button becomes visible
|
||||||
const checkPlayerVisibility = async (locator: Locator) => {
|
const checkPlayerVisibility = async (locator: Locator) => {
|
||||||
// Assert that the audio player and media information are visible
|
// Assert that the audio player and media information are visible
|
||||||
const mediaInfo = locator.getByRole("region", { name: "Audio player" });
|
const mediaInfo = locator.locator(
|
||||||
await expect(mediaInfo.getByText(".ogg")).toBeVisible(); // extension
|
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container .mx_AudioPlayer_mediaInfo",
|
||||||
await expect(mediaInfo.getByRole("time")).toHaveText("00:01"); // duration
|
);
|
||||||
await expect(mediaInfo.getByText("(3.56 KB)")).toBeVisible(); // actual size;
|
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName", { hasText: ".ogg" })).toBeVisible(); // extension
|
||||||
|
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
|
||||||
|
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
|
||||||
|
|
||||||
// Assert that the play button can be found and is visible
|
// Assert that the play button can be found and is visible
|
||||||
await expect(locator.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(locator.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -77,7 +78,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check the status of the seek bar
|
// Check the status of the seek bar
|
||||||
expect(await page.getByRole("region", { name: "Audio player" }).getByRole("slider").count()).toBeGreaterThan(0);
|
expect(await page.locator(".mx_AudioPlayer_seek input[type='range']").count()).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Enable IRC layout
|
// Enable IRC layout
|
||||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||||
@@ -99,7 +100,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
mask: [page.getByTestId("audio-player-seek")],
|
mask: [page.locator(".mx_AudioPlayer_seek")],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Take a snapshot of mx_EventTile_last on IRC layout
|
// Take a snapshot of mx_EventTile_last on IRC layout
|
||||||
@@ -185,9 +186,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
const container = page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" });
|
const container = page.locator(".mx_EventTile_last .mx_AudioPlayer_container");
|
||||||
// Assert that the counter is zero before clicking the play button
|
// Assert that the counter is zero before clicking the play button
|
||||||
await expect(container.getByRole("timer")).toHaveText("00:00");
|
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Find and click "Play" button, the wait is to make the test less flaky
|
// Find and click "Play" button, the wait is to make the test less flaky
|
||||||
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -197,7 +198,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await expect(container.getByRole("button", { name: "Pause" })).toBeVisible();
|
await expect(container.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that the timer is reset when the audio file finished playing
|
// Assert that the timer is reset when the audio file finished playing
|
||||||
await expect(container.getByRole("timer")).toHaveText("00:00");
|
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that "Play" button can be found
|
// Assert that "Play" button can be found
|
||||||
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -225,7 +226,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert the audio player is rendered
|
// Assert the audio player is rendered
|
||||||
await expect(page.getByRole("region", { name: "Audio player" })).toBeVisible();
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
// Find and click "Reply" button on MessageActionBar
|
// Find and click "Reply" button on MessageActionBar
|
||||||
const tile = page.locator(".mx_EventTile_last");
|
const tile = page.locator(".mx_EventTile_last");
|
||||||
@@ -235,7 +236,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
// Assert that replied audio file is rendered as file button inside ReplyChain
|
// Assert that replied audio file is rendered as file button inside ReplyChain
|
||||||
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
|
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
|
||||||
@@ -260,9 +261,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await clickButtonReply(tile);
|
await clickButtonReply(tile);
|
||||||
|
|
||||||
@@ -270,9 +269,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await clickButtonReply(tile);
|
await clickButtonReply(tile);
|
||||||
|
|
||||||
@@ -280,7 +277,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
// Assert that there are two "mx_ReplyChain" elements
|
// Assert that there are two "mx_ReplyChain" elements
|
||||||
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
|
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
|
||||||
@@ -316,9 +313,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// On the main timeline
|
// On the main timeline
|
||||||
const messageList = page.locator(".mx_RoomView_MessageList");
|
const messageList = page.locator(".mx_RoomView_MessageList");
|
||||||
// Assert the audio player is rendered
|
// Assert the audio player is rendered
|
||||||
await expect(
|
await expect(messageList.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
messageList.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
|
||||||
).toBeVisible();
|
|
||||||
// Find and click "Reply in thread" button
|
// Find and click "Reply in thread" button
|
||||||
await messageList.locator(".mx_EventTile_last").hover();
|
await messageList.locator(".mx_EventTile_last").hover();
|
||||||
await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click();
|
await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click();
|
||||||
@@ -326,10 +321,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// On a thread
|
// On a thread
|
||||||
const thread = page.locator(".mx_ThreadView");
|
const thread = page.locator(".mx_ThreadView");
|
||||||
const threadTile = thread.locator(".mx_EventTile_last");
|
const threadTile = thread.locator(".mx_EventTile_last");
|
||||||
const audioPlayer = threadTile.getByRole("region", { name: "Audio player" });
|
const audioPlayer = threadTile.locator(".mx_AudioPlayer_container");
|
||||||
|
|
||||||
// Assert that the counter is zero before clicking the play button
|
// Assert that the counter is zero before clicking the play button
|
||||||
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
|
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Find and click "Play" button, the wait is to make the test less flaky
|
// Find and click "Play" button, the wait is to make the test less flaky
|
||||||
await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -339,7 +334,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible();
|
await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that the timer is reset when the audio file finished playing
|
// Assert that the timer is reset when the audio file finished playing
|
||||||
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
|
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that "Play" button can be found
|
// Assert that "Play" button can be found
|
||||||
await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled();
|
await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ test.describe("Composer", () => {
|
|||||||
|
|
||||||
test.describe("CIDER", () => {
|
test.describe("CIDER", () => {
|
||||||
test("sends a message when you click send or press Enter", async ({ page }) => {
|
test("sends a message when you click send or press Enter", async ({ page }) => {
|
||||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
|
|
||||||
// Type a message
|
// Type a message
|
||||||
await composer.pressSequentially("my message 0");
|
await composer.pressSequentially("my message 0");
|
||||||
@@ -52,7 +52,7 @@ test.describe("Composer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can write formatted text", async ({ page }) => {
|
test("can write formatted text", async ({ page }) => {
|
||||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
|
|
||||||
await composer.pressSequentially("my bold");
|
await composer.pressSequentially("my bold");
|
||||||
await composer.press(`${CtrlOrMeta}+KeyB`);
|
await composer.press(`${CtrlOrMeta}+KeyB`);
|
||||||
@@ -68,7 +68,7 @@ test.describe("Composer", () => {
|
|||||||
await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click();
|
await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click();
|
||||||
|
|
||||||
await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker
|
await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); // Send message
|
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message
|
||||||
|
|
||||||
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
||||||
});
|
});
|
||||||
@@ -79,7 +79,7 @@ test.describe("Composer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("only sends when you press Control+Enter", async ({ page }) => {
|
test("only sends when you press Control+Enter", async ({ page }) => {
|
||||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
// Type a message and press Enter
|
// Type a message and press Enter
|
||||||
await composer.pressSequentially("my message 3");
|
await composer.pressSequentially("my message 3");
|
||||||
await composer.press("Enter");
|
await composer.press("Enter");
|
||||||
|
|||||||
34
playwright/e2e/create-room/create-room.spec.ts
Normal file
34
playwright/e2e/create-room/create-room.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Create Room", () => {
|
||||||
|
test.use({ displayName: "Jim" });
|
||||||
|
|
||||||
|
test("should allow us to create a public room with name, topic & address set", async ({ page, user, app }) => {
|
||||||
|
const name = "Test room 1";
|
||||||
|
const topic = "This room is dedicated to this test and this test only!";
|
||||||
|
|
||||||
|
const dialog = await app.openCreateRoomDialog();
|
||||||
|
// Fill name & topic
|
||||||
|
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
||||||
|
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
||||||
|
// Change room to public
|
||||||
|
await dialog.getByRole("button", { name: "Room visibility" }).click();
|
||||||
|
await dialog.getByRole("option", { name: "Public room" }).click();
|
||||||
|
// Fill room address
|
||||||
|
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-room-1");
|
||||||
|
// Submit
|
||||||
|
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||||
|
const header = page.locator(".mx_RoomHeader");
|
||||||
|
await expect(header).toContainText(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,6 @@ import { registerAccountMas } from "../oidc";
|
|||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
import { TestClientServerAPI } from "../csAPI";
|
import { TestClientServerAPI } from "../csAPI";
|
||||||
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
|
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
|
||||||
import { checkDeviceIsConnectedKeyBackup } from "./utils";
|
|
||||||
|
|
||||||
// These tests register an account with MAS because then we go through the "normal" registration flow
|
// These tests register an account with MAS because then we go through the "normal" registration flow
|
||||||
// and crypto gets set up. Using the 'user' fixture create a user and synthesizes an existing login,
|
// and crypto gets set up. Using the 'user' fixture create a user and synthesizes an existing login,
|
||||||
@@ -20,34 +19,19 @@ test.use(masHomeserver);
|
|||||||
test.describe("Encryption state after registration", () => {
|
test.describe("Encryption state after registration", () => {
|
||||||
test.skip(isDendrite, "does not yet support MAS");
|
test.skip(isDendrite, "does not yet support MAS");
|
||||||
|
|
||||||
test("Key backup is enabled by default", async ({ page, mailpitClient, app }, testInfo) => {
|
test("Key backup is enabled by default", async ({ page, mailhogClient, app }, testInfo) => {
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
await registerAccountMas(
|
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||||
page,
|
|
||||||
mailpitClient,
|
|
||||||
`alice_${testInfo.testId}`,
|
|
||||||
`alice_${testInfo.testId}@email.com`,
|
|
||||||
"Pa$sW0rD!",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for the ui to load
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
await expect(page.locator(".mx_MatrixChat")).toBeVisible();
|
await expect(page.getByText("This session is backing up your keys.")).toBeVisible();
|
||||||
|
|
||||||
// Recovery is not set up yet
|
|
||||||
await checkDeviceIsConnectedKeyBackup(app, "1", true, false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => {
|
test("user is prompted to set up recovery", async ({ page, mailhogClient, app }, testInfo) => {
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
await registerAccountMas(
|
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||||
page,
|
|
||||||
mailpitClient,
|
|
||||||
`alice_${testInfo.testId}`,
|
|
||||||
`alice_${testInfo.testId}@email.com`,
|
|
||||||
"Pa$sW0rD!",
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Add room" }).click();
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
@@ -63,7 +47,7 @@ test.describe("Key backup reset from elsewhere", () => {
|
|||||||
|
|
||||||
test("Key backup is disabled when reset from elsewhere", async ({
|
test("Key backup is disabled when reset from elsewhere", async ({
|
||||||
page,
|
page,
|
||||||
mailpitClient,
|
mailhogClient,
|
||||||
request,
|
request,
|
||||||
homeserver,
|
homeserver,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
@@ -76,7 +60,7 @@ test.describe("Key backup reset from elsewhere", () => {
|
|||||||
|
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
|
await registerAccountMas(page, mailhogClient, testUsername, "alice@email.com", testPassword);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Add room" }).click();
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
@@ -91,10 +75,10 @@ test.describe("Key backup reset from elsewhere", () => {
|
|||||||
|
|
||||||
await csAPI.deleteBackupVersion(backupInfo.version);
|
await csAPI.deleteBackupVersion(backupInfo.version);
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("/discardsession");
|
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession");
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("Message with broken key backup");
|
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup");
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
// Should be the message we sent plus the room creation event
|
// Should be the message we sent plus the room creation event
|
||||||
|
|||||||
118
playwright/e2e/crypto/backups.spec.ts
Normal file
118
playwright/e2e/crypto/backups.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
|
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||||
|
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||||
|
version + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Backups", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||||
|
test.use({
|
||||||
|
displayName: "Hanako",
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Create, delete and recreate a keys backup",
|
||||||
|
{ tag: "@no-webkit" },
|
||||||
|
async ({ page, user, app }, workerInfo) => {
|
||||||
|
// Create a backup
|
||||||
|
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
|
||||||
|
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||||
|
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||||
|
|
||||||
|
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||||
|
|
||||||
|
// It's the first time and secure storage is not set up, so it will create one
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||||
|
// copy the recovery key to use it later
|
||||||
|
const securityKey = await app.getClipboard();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||||
|
|
||||||
|
// Open the settings again
|
||||||
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||||
|
|
||||||
|
// expand the advanced section to see the active version in the reports
|
||||||
|
await page
|
||||||
|
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
||||||
|
.locator("..")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expectBackupVersionToBe(page, "1");
|
||||||
|
|
||||||
|
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||||
|
// Delete it
|
||||||
|
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
||||||
|
|
||||||
|
// Create another
|
||||||
|
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
||||||
|
await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
|
// Should be successful
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
|
||||||
|
|
||||||
|
// Open the settings again
|
||||||
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||||
|
|
||||||
|
// expand the advanced section to see the active version in the reports
|
||||||
|
await page
|
||||||
|
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
||||||
|
.locator("..")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expectBackupVersionToBe(page, "2");
|
||||||
|
|
||||||
|
// ==
|
||||||
|
// Ensure that if you don't have the secret storage passphrase the backup won't be created
|
||||||
|
// ==
|
||||||
|
|
||||||
|
// First delete version 2
|
||||||
|
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||||
|
// Click "Delete Backup"
|
||||||
|
await currentDialogLocator.getByTestId("dialog-primary-button").click();
|
||||||
|
|
||||||
|
// Try to create another
|
||||||
|
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
||||||
|
// But cancel the security key dialog, to simulate not having the secret storage passphrase
|
||||||
|
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||||
|
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
|
||||||
|
// check that it failed
|
||||||
|
await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
|
||||||
|
// cancel
|
||||||
|
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||||
|
|
||||||
|
// go back to the settings to check that no backup was created (the setup button should still be there)
|
||||||
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -8,9 +8,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||||
import { type Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
const checkDMRoom = async (page: Page) => {
|
const checkDMRoom = async (page: Page) => {
|
||||||
@@ -21,7 +21,7 @@ const checkDMRoom = async (page: Page) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startDMWithBob = async (page: Page, bob: Bot) => {
|
const startDMWithBob = async (page: Page, bob: Bot) => {
|
||||||
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||||
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
||||||
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
||||||
await expect(
|
await expect(
|
||||||
@@ -77,43 +77,96 @@ test.describe("Cryptography", function () {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
for (const isDeviceVerified of [true, false]) {
|
||||||
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => {
|
||||||
* @param keyType
|
/**
|
||||||
*/
|
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
||||||
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
|
* @param keyType
|
||||||
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
|
*/
|
||||||
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
|
||||||
keyType,
|
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
|
||||||
);
|
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
||||||
|
keyType,
|
||||||
|
);
|
||||||
|
expect(accountData.encrypted).toBeDefined();
|
||||||
|
const keys = Object.keys(accountData.encrypted);
|
||||||
|
const key = accountData.encrypted[keys[0]];
|
||||||
|
expect(key.ciphertext).toBeDefined();
|
||||||
|
expect(key.iv).toBeDefined();
|
||||||
|
expect(key.mac).toBeDefined();
|
||||||
|
}
|
||||||
|
|
||||||
expect(accountData.encrypted).toBeDefined();
|
test("by recovery code", async ({ page, app, user: aliceCredentials }) => {
|
||||||
const keys = Object.keys(accountData.encrypted);
|
// Verified the device
|
||||||
const key = accountData.encrypted[keys[0]];
|
if (isDeviceVerified) {
|
||||||
expect(key.ciphertext).toBeDefined();
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
expect(key.iv).toBeDefined();
|
}
|
||||||
expect(key.mac).toBeDefined();
|
|
||||||
|
await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => {
|
||||||
|
// We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||||
|
|
||||||
|
const dialog = page.locator(".mx_Dialog");
|
||||||
|
// Recovery key is selected by default
|
||||||
|
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await copyAndContinue(page);
|
||||||
|
|
||||||
|
// If the device is unverified, there should be a "Setting up keys" step; however, it
|
||||||
|
// can be quite quick, and playwright can miss it, so we can't test for it.
|
||||||
|
|
||||||
|
// Either way, we end up at a success dialog:
|
||||||
|
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||||
|
await dialog.getByRole("button", { name: "Done" }).click();
|
||||||
|
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||||
|
|
||||||
|
// Verify that the SSSS keys are in the account data stored in the server
|
||||||
|
await verifyKey(app, "master");
|
||||||
|
await verifyKey(app, "self_signing");
|
||||||
|
await verifyKey(app, "user_signing");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("by passphrase", async ({ page, app, user: aliceCredentials }) => {
|
||||||
|
// Verified the device
|
||||||
|
if (isDeviceVerified) {
|
||||||
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||||
|
|
||||||
|
const dialog = page.locator(".mx_Dialog");
|
||||||
|
// Select passphrase option
|
||||||
|
await dialog.getByText("Enter a Security Phrase").click();
|
||||||
|
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
// Fill passphrase input
|
||||||
|
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
||||||
|
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||||
|
// Confirm passphrase
|
||||||
|
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
||||||
|
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||||
|
|
||||||
|
await copyAndContinue(page);
|
||||||
|
|
||||||
|
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||||
|
await dialog.getByRole("button", { name: "Done" }).click();
|
||||||
|
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||||
|
|
||||||
|
// Verify that the SSSS keys are in the account data stored in the server
|
||||||
|
await verifyKey(app, "master");
|
||||||
|
await verifyKey(app, "self_signing");
|
||||||
|
await verifyKey(app, "user_signing");
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Setting up key backup by recovery key", async ({ page, app, user: aliceCredentials }) => {
|
|
||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
|
||||||
|
|
||||||
await enableKeyBackup(app);
|
|
||||||
|
|
||||||
// Wait for the cross signing keys to be uploaded
|
|
||||||
// Waiting for "Change the recovery key" button ensure that all the secrets are uploaded and cached locally
|
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
|
||||||
await expect(encryptionTab.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
|
||||||
|
|
||||||
// Verify that the SSSS keys are in the account data stored in the server
|
|
||||||
await verifyKey(app, "master");
|
|
||||||
await verifyKey(app, "self_signing");
|
|
||||||
await verifyKey(app, "user_signing");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
const secretStorageKey = await enableKeyBackup(app);
|
||||||
await enableKeyBackup(app);
|
|
||||||
|
|
||||||
// Fetch the current cross-signing keys
|
// Fetch the current cross-signing keys
|
||||||
async function fetchMasterKey() {
|
async function fetchMasterKey() {
|
||||||
@@ -127,15 +180,18 @@ test.describe("Cryptography", function () {
|
|||||||
return k;
|
return k;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const masterKey1 = await fetchMasterKey();
|
const masterKey1 = await fetchMasterKey();
|
||||||
|
|
||||||
// Find "the Reset cryptographic identity" button
|
// Find the "reset cross signing" button, and click it
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click();
|
||||||
|
|
||||||
// Confirm
|
// Confirm
|
||||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
|
||||||
|
|
||||||
|
// Enter the 4S key
|
||||||
|
await page.getByPlaceholder("Security Key").fill(secretStorageKey);
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// Enter the password
|
// Enter the password
|
||||||
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
|
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
|
||||||
@@ -145,6 +201,9 @@ test.describe("Cryptography", function () {
|
|||||||
const masterKey2 = await fetchMasterKey();
|
const masterKey2 = await fetchMasterKey();
|
||||||
expect(masterKey1).not.toEqual(masterKey2);
|
expect(masterKey1).not.toEqual(masterKey2);
|
||||||
}).toPass();
|
}).toPass();
|
||||||
|
|
||||||
|
// The dialog should have gone away
|
||||||
|
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -154,13 +213,10 @@ test.describe("Cryptography", function () {
|
|||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
await startDMWithBob(page, bob);
|
await startDMWithBob(page, bob);
|
||||||
// send first message
|
// send first message
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!");
|
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
|
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||||
await checkDMRoom(page);
|
await checkDMRoom(page);
|
||||||
const bobRoomId = await bobJoin(page, bob);
|
const bobRoomId = await bobJoin(page, bob);
|
||||||
// We no longer show the grey badge in the composer, check that it is not there.
|
|
||||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toHaveCount(0);
|
|
||||||
|
|
||||||
await testMessages(page, bob, bobRoomId);
|
await testMessages(page, bob, bobRoomId);
|
||||||
await verify(app, bob);
|
await verify(app, bob);
|
||||||
|
|
||||||
@@ -171,7 +227,6 @@ test.describe("Cryptography", function () {
|
|||||||
|
|
||||||
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
||||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
|
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
|
||||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon.png");
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { Locator, type Page } from "@playwright/test";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
|
||||||
import { createBot, logIntoElement } from "./utils.ts";
|
|
||||||
import { type Client } from "../../pages/client.ts";
|
|
||||||
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
import { viewRoomSummaryByName } from "../right-panel/utils";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
|
const ROOM_NAME = "Test room";
|
||||||
const NAME = "Alice";
|
const NAME = "Alice";
|
||||||
|
|
||||||
|
function getMemberTileByName(page: Page, name: string): Locator {
|
||||||
|
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
displayName: NAME,
|
displayName: NAME,
|
||||||
synapseConfig: {
|
synapseConfig: {
|
||||||
@@ -22,182 +27,73 @@ test.use({
|
|||||||
msc3814_enabled: true,
|
msc3814_enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
config: async ({ config, context }, use) => {
|
||||||
|
const wellKnown = {
|
||||||
|
...config.default_server_config,
|
||||||
|
"org.matrix.msc3814": true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
|
||||||
|
await route.fulfill({ json: wellKnown });
|
||||||
|
});
|
||||||
|
|
||||||
|
await use(config);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Dehydration", () => {
|
test.describe("Dehydration", () => {
|
||||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||||
|
|
||||||
test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
|
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||||
// Verify the device by resetting the identity key, and then set up recovery (which will create SSSS, and dehydrated device)
|
// Create a backup (which will create SSSS, and dehydrated device)
|
||||||
|
|
||||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
|
||||||
|
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||||
|
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||||
|
|
||||||
await app.closeDialog();
|
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||||
|
|
||||||
// Reset the identity key
|
// It's the first time and secure storage is not set up, so it will create one
|
||||||
const settings = await app.settings.openUserSettings("Encryption");
|
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
// Set up recovery
|
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Set up recovery" }).click();
|
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
const recoveryKey = await page.getByTestId("recoveryKey").innerText();
|
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
await page.getByRole("textbox").fill(recoveryKey);
|
|
||||||
await page.getByRole("button", { name: "Finish set up" }).click();
|
|
||||||
await page.getByRole("button", { name: "Close" }).click();
|
|
||||||
|
|
||||||
await expectDehydratedDeviceEnabled(app);
|
// Open the settings again
|
||||||
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
|
||||||
|
// The Security tab should indicate that there is a dehydrated device present
|
||||||
|
await expect(securityTab.getByText("Offline device enabled")).toBeVisible();
|
||||||
|
|
||||||
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
// the dehydrated device gets created with the name "Dehydrated
|
// the dehydrated device gets created with the name "Dehydrated
|
||||||
// device". We want to make sure that it is not visible as a normal
|
// device". We want to make sure that it is not visible as a normal
|
||||||
// device.
|
// device.
|
||||||
const sessionsTab = await app.settings.openUserSettings("Sessions");
|
const sessionsTab = await app.settings.openUserSettings("Sessions");
|
||||||
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
|
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
|
||||||
});
|
|
||||||
|
|
||||||
test("'Set up recovery' creates dehydrated device", async ({ app, credentials, page }) => {
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
|
|
||||||
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
|
|
||||||
|
|
||||||
// First it displays an informative panel about the recovery key
|
|
||||||
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
// Next, it displays the new recovery key. We click on the copy button.
|
|
||||||
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
|
|
||||||
const recoveryKey = await app.getClipboard();
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
|
|
||||||
).toBeVisible();
|
|
||||||
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
|
|
||||||
|
|
||||||
await app.settings.closeDialog();
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
await expectDehydratedDeviceEnabled(app);
|
// now check that the user info right-panel shows the dehydrated device
|
||||||
});
|
// as a feature rather than as a normal device
|
||||||
|
await app.client.createRoom({ name: ROOM_NAME });
|
||||||
|
|
||||||
test("Reset identity during login and set up recovery re-creates dehydrated device", async ({
|
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||||
page,
|
|
||||||
homeserver,
|
|
||||||
app,
|
|
||||||
credentials,
|
|
||||||
}) => {
|
|
||||||
// Set up cross-signing and recovery
|
|
||||||
const { botClient } = await createBot(page, homeserver, credentials);
|
|
||||||
// ... and dehydration
|
|
||||||
await botClient.evaluate(async (client) => await client.getCrypto().startDehydration());
|
|
||||||
|
|
||||||
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient);
|
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
|
||||||
expect(initialDehydratedDeviceIds.length).toBe(1);
|
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||||
|
|
||||||
await botClient.evaluate(async (client) => client.stopClient());
|
await getMemberTileByName(page, NAME).click();
|
||||||
|
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
|
||||||
|
|
||||||
// Log in our client
|
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
|
||||||
await logIntoElement(page, credentials);
|
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
|
||||||
|
|
||||||
// Oh no, we forgot our recovery key - reset our identity
|
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
|
||||||
await expect(
|
|
||||||
page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
|
|
||||||
).toBeVisible();
|
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
// And set up recovery
|
|
||||||
const settings = await app.settings.openUserSettings("Encryption");
|
|
||||||
await settings.getByRole("button", { name: "Set up recovery" }).click();
|
|
||||||
await settings.getByRole("button", { name: "Continue" }).click();
|
|
||||||
const recoveryKey = await settings.getByTestId("recoveryKey").innerText();
|
|
||||||
await settings.getByRole("button", { name: "Continue" }).click();
|
|
||||||
await settings.getByRole("textbox").fill(recoveryKey);
|
|
||||||
await settings.getByRole("button", { name: "Finish set up" }).click();
|
|
||||||
|
|
||||||
// There should be a brand new dehydrated device
|
|
||||||
await expectDehydratedDeviceEnabled(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => {
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
|
|
||||||
// Create a dehydrated device by setting up recovery (see "'Set up
|
|
||||||
// recovery' creates dehydrated device" test above)
|
|
||||||
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
|
|
||||||
|
|
||||||
// First it displays an informative panel about the recovery key
|
|
||||||
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
// Next, it displays the new recovery key. We click on the copy button.
|
|
||||||
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
|
|
||||||
const recoveryKey = await app.getClipboard();
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
|
|
||||||
).toBeVisible();
|
|
||||||
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
|
|
||||||
|
|
||||||
await expectDehydratedDeviceEnabled(app);
|
|
||||||
|
|
||||||
// After recovery is set up, we reset our cryptographic identity, which
|
|
||||||
// should drop the dehydrated device.
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
|
||||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
await expectDehydratedDeviceDisabled(app);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
|
|
||||||
return await client.evaluate(async (client) => {
|
|
||||||
const userId = client.getUserId();
|
|
||||||
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
|
|
||||||
return Array.from(
|
|
||||||
devices
|
|
||||||
.get(userId)
|
|
||||||
.values()
|
|
||||||
.filter((d) => d.dehydrated)
|
|
||||||
.map((d) => d.deviceId),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Wait for our user to have a dehydrated device */
|
|
||||||
async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise<void> {
|
|
||||||
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
|
|
||||||
//
|
|
||||||
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
|
|
||||||
await expect
|
|
||||||
.poll(async () => {
|
|
||||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
|
||||||
return dehydratedDeviceIds.length;
|
|
||||||
})
|
|
||||||
.toEqual(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Wait for our user to not have a dehydrated device */
|
|
||||||
async function expectDehydratedDeviceDisabled(app: ElementAppPage): Promise<void> {
|
|
||||||
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
|
|
||||||
//
|
|
||||||
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
|
|
||||||
await expect
|
|
||||||
.poll(async () => {
|
|
||||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
|
||||||
return dehydratedDeviceIds.length;
|
|
||||||
})
|
|
||||||
.toEqual(0);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ import {
|
|||||||
logIntoElement,
|
logIntoElement,
|
||||||
waitForVerificationRequest,
|
waitForVerificationRequest,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { type Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
import { Toasts } from "../../pages/toasts.ts";
|
|
||||||
import type { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
|
||||||
|
|
||||||
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||||
let aliceBotClient: Bot;
|
let aliceBotClient: Bot;
|
||||||
@@ -31,7 +29,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
let expectedBackupVersion: string;
|
let expectedBackupVersion: string;
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||||
const res = await createBot(page, homeserver, credentials, true);
|
const res = await createBot(page, homeserver, credentials);
|
||||||
aliceBotClient = res.botClient;
|
aliceBotClient = res.botClient;
|
||||||
expectedBackupVersion = res.expectedBackupVersion;
|
expectedBackupVersion = res.expectedBackupVersion;
|
||||||
});
|
});
|
||||||
@@ -48,62 +46,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
return promiseVerificationRequest;
|
return promiseVerificationRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
"Verify device with SAS during login",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, credentials, homeserver }) => {
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
|
|
||||||
// Launch the verification request between alice and the bot
|
|
||||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
|
||||||
|
|
||||||
// Handle emoji SAS verification
|
|
||||||
const infoDialog = page.locator(".mx_InfoDialog");
|
|
||||||
// the bot chooses to do an emoji verification
|
|
||||||
const verifier = await verificationRequest.evaluateHandle((request) =>
|
|
||||||
request.startVerification("m.sas.v1"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle emoji request and check that emojis are matching
|
|
||||||
await doTwoWaySasVerification(page, verifier);
|
|
||||||
|
|
||||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
|
||||||
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png");
|
|
||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
|
||||||
|
|
||||||
// Check that our device is now cross-signed
|
|
||||||
await checkDeviceIsCrossSigned(app);
|
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
|
||||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
|
||||||
// as we need to wait for the secret gossiping to happen.
|
|
||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Regression test for https://github.com/element-hq/element-web/issues/29110
|
|
||||||
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
|
|
||||||
// Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens
|
|
||||||
// when we are in an encrypted room.
|
|
||||||
await aliceBotClient.createRoom({
|
|
||||||
initial_state: [
|
|
||||||
{
|
|
||||||
type: "m.room.encryption",
|
|
||||||
state_key: "",
|
|
||||||
content: { algorithm: "m.megolm.v1.aes-sha2" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// In order to simulate a real environment more accurately, we need to slow down the arrival of the
|
|
||||||
// `m.secret.send` to-device messages. That's slightly tricky to do directly, so instead we delay the *outgoing*
|
|
||||||
// `m.secret.request` messages.
|
|
||||||
await page.route("**/_matrix/client/v3/sendToDevice/m.secret.request/**", async (route) => {
|
|
||||||
await route.fulfill({ json: {} });
|
|
||||||
await new Promise((f) => setTimeout(f, 1000));
|
|
||||||
await route.fetch();
|
|
||||||
});
|
|
||||||
|
|
||||||
await logIntoElement(page, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
// Launch the verification request between alice and the bot
|
// Launch the verification request between alice and the bot
|
||||||
@@ -120,14 +63,13 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||||
|
|
||||||
// There should be no toast (other than the notifications one)
|
// Check that our device is now cross-signed
|
||||||
const toasts = new Toasts(page);
|
await checkDeviceIsCrossSigned(app);
|
||||||
await toasts.rejectToast("Notifications");
|
|
||||||
await toasts.assertNoToasts();
|
|
||||||
|
|
||||||
// There may still be a `/sendToDevice/m.secret.request` in flight, which will later throw an error and cause
|
// Check that the current device is connected to key backup
|
||||||
// a *subsequent* test to fail. Tell playwright to ignore any errors resulting from in-flight routes.
|
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||||
await page.unrouteAll({ behavior: "ignoreErrors" });
|
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||||
|
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
@@ -170,57 +112,54 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
await checkDeviceIsCrossSigned(app);
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||||
|
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||||
|
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
await logIntoElement(page, credentials);
|
await logIntoElement(page, credentials);
|
||||||
await enterRecoveryKeyAndCheckVerified(page, app, "new passphrase");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
|
// Select the security phrase
|
||||||
const recoveryKey = (await aliceBotClient.getRecoveryKey()).encodedPrivateKey;
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||||
|
|
||||||
await logIntoElement(page, credentials);
|
// Fill the passphrase
|
||||||
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Verify device with Recovery Key from settings", async ({ page, app, credentials }) => {
|
|
||||||
const recoveryKey = (await aliceBotClient.getRecoveryKey()).encodedPrivateKey;
|
|
||||||
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
|
|
||||||
/* Dismiss "Verify this device" */
|
|
||||||
const authPage = page.locator(".mx_AuthPage");
|
|
||||||
await authPage.getByRole("button", { name: "Skip verification for now" }).click();
|
|
||||||
await authPage.getByRole("button", { name: "I'll verify later" }).click();
|
|
||||||
await page.waitForSelector(".mx_MatrixChat");
|
|
||||||
|
|
||||||
const settings = await app.settings.openUserSettings("Encryption");
|
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
|
||||||
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Helper for the three tests above which verify by recovery key */
|
|
||||||
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
|
|
||||||
await page.getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
|
||||||
|
|
||||||
// Enter the recovery key
|
|
||||||
const dialog = page.locator(".mx_Dialog");
|
const dialog = page.locator(".mx_Dialog");
|
||||||
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
|
await dialog.locator("input").fill("new passphrase");
|
||||||
// (cf https://github.com/element-hq/element-web/issues/30089)
|
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||||
await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey);
|
|
||||||
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Done" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
|
||||||
|
|
||||||
// Check that our device is now cross-signed
|
// Check that our device is now cross-signed
|
||||||
await checkDeviceIsCrossSigned(app);
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
|
// Select the security phrase
|
||||||
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||||
|
|
||||||
|
// Fill the security key
|
||||||
|
const dialog = page.locator(".mx_Dialog");
|
||||||
|
await dialog.getByRole("button", { name: "use your Security Key" }).click();
|
||||||
|
const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
|
||||||
|
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
|
||||||
|
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||||
|
|
||||||
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
|
||||||
|
|
||||||
|
// Check that our device is now cross-signed
|
||||||
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
|
// Check that the current device is connected to key backup
|
||||||
|
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||||
|
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||||
|
});
|
||||||
|
|
||||||
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
||||||
await logIntoElement(page, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Locator } from "@playwright/test";
|
import { Locator } from "@playwright/test";
|
||||||
|
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import {
|
import {
|
||||||
@@ -17,10 +17,9 @@ import {
|
|||||||
logIntoElement,
|
logIntoElement,
|
||||||
logOutOfElement,
|
logOutOfElement,
|
||||||
verify,
|
verify,
|
||||||
waitForDevices,
|
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||||
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||||
|
|
||||||
test.describe("Cryptography", function () {
|
test.describe("Cryptography", function () {
|
||||||
test.use({
|
test.use({
|
||||||
@@ -58,108 +57,124 @@ test.describe("Cryptography", function () {
|
|||||||
await app.client.network.setupRoute();
|
await app.client.network.setupRoute();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test("should show the correct shield on e2e events", async ({
|
||||||
"should show the correct shield on e2e events",
|
page,
|
||||||
{ tag: "@screenshot" },
|
app,
|
||||||
async ({ page, app, bot: bob, homeserver }, workerInfo) => {
|
bot: bob,
|
||||||
// Bob has a second, not cross-signed, device
|
homeserver,
|
||||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
}, workerInfo) => {
|
||||||
|
// Bob has a second, not cross-signed, device
|
||||||
|
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||||
|
|
||||||
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
|
// Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list
|
||||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
await page.getByRole("button", { name: "Not now" }).click();
|
||||||
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
|
||||||
|
|
||||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||||
algorithm: "m.megolm.v1.aes-sha2",
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
ciphertext: "the bird is in the hand",
|
ciphertext: "the bird is in the hand",
|
||||||
});
|
});
|
||||||
|
|
||||||
const last = page.locator(".mx_EventTile_last");
|
const last = page.locator(".mx_EventTile_last");
|
||||||
await expect(last).toContainText("Unable to decrypt message");
|
await expect(last).toContainText("Unable to decrypt message");
|
||||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
"This message could not be decrypted",
|
"This message could not be decrypted",
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Should show a red padlock for an unencrypted message in an e2e room */
|
/* Should show a red padlock for an unencrypted message in an e2e room */
|
||||||
await bob.evaluate(
|
await bob.evaluate(
|
||||||
(cli, testRoomId) =>
|
(cli, testRoomId) =>
|
||||||
cli.http.authedRequest(
|
cli.http.authedRequest(
|
||||||
window.matrixcs.Method.Put,
|
window.matrixcs.Method.Put,
|
||||||
`/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`,
|
`/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`,
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "test unencrypted",
|
body: "test unencrypted",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
testRoomId,
|
testRoomId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(last).toContainText("test unencrypted");
|
await expect(last).toContainText("test unencrypted");
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png");
|
await lastE2eIcon.focus();
|
||||||
await lastE2eIcon.focus();
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
|
||||||
|
|
||||||
/* Should show no padlock for an unverified user */
|
/* Should show no padlock for an unverified user */
|
||||||
// bob sends a valid event
|
// bob sends a valid event
|
||||||
await bob.sendMessage(testRoomId, "test encrypted 1");
|
await bob.sendMessage(testRoomId, "test encrypted 1");
|
||||||
|
|
||||||
// the message should appear, decrypted, with no warning, but also no "verified"
|
// the message should appear, decrypted, with no warning, but also no "verified"
|
||||||
const lastTile = page.locator(".mx_EventTile_last");
|
const lastTile = page.locator(".mx_EventTile_last");
|
||||||
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
||||||
await expect(lastTile).toContainText("test encrypted 1");
|
await expect(lastTile).toContainText("test encrypted 1");
|
||||||
// no e2e icon
|
// no e2e icon
|
||||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||||
|
|
||||||
/* Now verify Bob */
|
/* Now verify Bob */
|
||||||
await verify(app, bob);
|
await verify(app, bob);
|
||||||
|
|
||||||
/* Existing message should be updated when user is verified. */
|
/* Existing message should be updated when user is verified. */
|
||||||
await expect(last).toContainText("test encrypted 1");
|
await expect(last).toContainText("test encrypted 1");
|
||||||
// still no e2e icon
|
// still no e2e icon
|
||||||
await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||||
|
|
||||||
/* should show no padlock, and be verified, for a message from a verified device */
|
/* should show no padlock, and be verified, for a message from a verified device */
|
||||||
await bob.sendMessage(testRoomId, "test encrypted 2");
|
await bob.sendMessage(testRoomId, "test encrypted 2");
|
||||||
|
|
||||||
await expect(lastTile).toContainText("test encrypted 2");
|
await expect(lastTile).toContainText("test encrypted 2");
|
||||||
// no e2e icon
|
// no e2e icon
|
||||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||||
|
|
||||||
/* should show red padlock for a message from an unverified device */
|
/* should show red padlock for a message from an unverified device */
|
||||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||||
await expect(lastTile).toContainText("test encrypted from unverified");
|
await expect(lastTile).toContainText("test encrypted from unverified");
|
||||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastTileE2eIcon.focus();
|
await lastTileE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||||
"Encrypted by a device not verified by its owner.",
|
"Encrypted by a device not verified by its owner.",
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Should show a red padlock for a message from an unverified device.
|
/* Should show a red padlock for a message from an unverified device.
|
||||||
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
||||||
* unverified, even if it gets deleted. */
|
* unverified, even if it gets deleted. */
|
||||||
// bob deletes his second device
|
// bob deletes his second device
|
||||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||||
|
|
||||||
// wait for the logout to propagate.
|
// wait for the logout to propagate. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info.
|
||||||
await waitForDevices(app, bob.credentials.userId, 1);
|
async function awaitOneDevice(iterations = 1) {
|
||||||
|
const rightPanel = page.locator(".mx_RightPanel");
|
||||||
|
await rightPanel.getByTestId("base-card-back-button").click();
|
||||||
|
await rightPanel.getByText("Bob").click();
|
||||||
|
const sessionCountText = await rightPanel
|
||||||
|
.locator(".mx_UserInfo_devices")
|
||||||
|
.getByText(" session", { exact: false })
|
||||||
|
.textContent();
|
||||||
|
// cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here
|
||||||
|
if (sessionCountText != "1 session" && sessionCountText != "1 verified session") {
|
||||||
|
if (iterations >= 10) {
|
||||||
|
throw new Error(`Bob still has ${sessionCountText} after 10 iterations`);
|
||||||
|
}
|
||||||
|
await awaitOneDevice(iterations + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// close and reopen the room, to get the shield to update.
|
await awaitOneDevice();
|
||||||
await app.viewRoomByName("Bob");
|
|
||||||
await app.viewRoomByName("TestRoom");
|
|
||||||
|
|
||||||
await expect(last).toContainText("test encrypted from unverified");
|
// close and reopen the room, to get the shield to update.
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await app.viewRoomByName("Bob");
|
||||||
await lastE2eIcon.focus();
|
await app.viewRoomByName("TestRoom");
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
|
||||||
"Encrypted by a device not verified by its owner.",
|
await expect(last).toContainText("test encrypted from unverified");
|
||||||
);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
},
|
await lastE2eIcon.focus();
|
||||||
);
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
|
"Encrypted by a device not verified by its owner.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("Should show a grey padlock for a key restored from backup", async ({
|
test("Should show a grey padlock for a key restored from backup", async ({
|
||||||
page,
|
page,
|
||||||
@@ -270,7 +285,11 @@ test.describe("Cryptography", function () {
|
|||||||
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
||||||
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
||||||
// his user info.
|
// his user info.
|
||||||
await waitForDevices(app, bob.credentials.userId, 1);
|
await app.toggleRoomInfoPanel();
|
||||||
|
const rightPanel = page.locator(".mx_RightPanel");
|
||||||
|
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
||||||
|
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
|
||||||
|
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
|
||||||
|
|
||||||
// Our app is blocked from syncing while Bob sends his messages.
|
// Our app is blocked from syncing while Bob sends his messages.
|
||||||
await app.client.network.goOffline();
|
await app.client.network.goOffline();
|
||||||
@@ -325,7 +344,7 @@ test.describe("Cryptography", function () {
|
|||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
"Sender's verified identity was reset",
|
"Sender's verified identity has changed",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
|||||||
/* should show an error for a message from a previously verified device */
|
/* should show an error for a message from a previously verified device */
|
||||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||||
const lastTile = page.locator(".mx_EventTile_last");
|
const lastTile = page.locator(".mx_EventTile_last");
|
||||||
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
|
|
||||||
import { type Bot } from "../../pages/bot";
|
|
||||||
|
|
||||||
test.describe("Key storage out of sync toast", () => {
|
|
||||||
let recoveryKey: GeneratedSecretStorageKey;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
|
||||||
const res = await createBot(page, homeserver, credentials);
|
|
||||||
recoveryKey = res.recoveryKey;
|
|
||||||
|
|
||||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
|
||||||
|
|
||||||
await deleteCachedSecrets(page);
|
|
||||||
|
|
||||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
|
||||||
await page.getByRole("button", { name: "Add room" }).click();
|
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
|
||||||
// We need to wait for there to be two toasts as the wait below won't work in isolation:
|
|
||||||
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
|
|
||||||
// it would always be checking the same toast, even if another one is now the first.
|
|
||||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
|
||||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Recovery Key" }).fill(recoveryKey.encodedPrivateKey);
|
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
|
|
||||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("'Turn on key storage' toast", () => {
|
|
||||||
let botClient: Bot | undefined;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials, toasts }) => {
|
|
||||||
// Set up all crypto stuff. Key storage defaults to on.
|
|
||||||
|
|
||||||
const res = await createBot(page, homeserver, credentials);
|
|
||||||
const recoveryKey = res.recoveryKey;
|
|
||||||
botClient = res.botClient;
|
|
||||||
|
|
||||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
|
||||||
|
|
||||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
|
||||||
await page.getByRole("button", { name: "Add room" }).click();
|
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
|
||||||
|
|
||||||
await toasts.rejectToast("Notifications");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not show toast if key storage is on", async ({ page, toasts }) => {
|
|
||||||
// Given the default situation after signing in
|
|
||||||
// Then no toast is shown (because key storage is on)
|
|
||||||
await toasts.assertNoToasts();
|
|
||||||
|
|
||||||
// When we reload
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Give the toasts time to appear
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// Then still no toast is shown
|
|
||||||
await toasts.assertNoToasts();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not show toast if key storage is off because we turned it off", async ({ app, page, toasts }) => {
|
|
||||||
// Given the backup is disabled because we disabled it
|
|
||||||
await disableKeyBackup(app);
|
|
||||||
|
|
||||||
// Then no toast is shown
|
|
||||||
await toasts.assertNoToasts();
|
|
||||||
|
|
||||||
// When we reload
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Give the toasts time to appear
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// Then still no toast is shown
|
|
||||||
await toasts.assertNoToasts();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should show toast if key storage is off but account data is missing", async ({ app, page, toasts }) => {
|
|
||||||
// Given the backup is disabled but we didn't set account data saying that is expected
|
|
||||||
await disableKeyBackup(app);
|
|
||||||
await botClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false });
|
|
||||||
|
|
||||||
// Wait for the account data setting to stick
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// When we enter the app
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Then the toast is displayed
|
|
||||||
let toast = await toasts.getToast("Turn on key storage");
|
|
||||||
|
|
||||||
// And when we click "Continue"
|
|
||||||
await toast.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
// Then we see the Encryption settings dialog with an option to turn on key storage
|
|
||||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
|
||||||
|
|
||||||
// And when we close that
|
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
|
||||||
|
|
||||||
// Then we see the toast again
|
|
||||||
toast = await toasts.getToast("Turn on key storage");
|
|
||||||
|
|
||||||
// And when we click "Dismiss"
|
|
||||||
await toast.getByRole("button", { name: "Dismiss" }).click();
|
|
||||||
|
|
||||||
// Then we see the "are you sure?" dialog
|
|
||||||
await expect(
|
|
||||||
page.getByRole("heading", { name: "Are you sure you want to keep key storage turned off?" }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// And when we close it by clicking away
|
|
||||||
await page.getByTestId("dialog-background").click({ force: true, position: { x: 10, y: 10 } });
|
|
||||||
|
|
||||||
// Then we see the toast again
|
|
||||||
toast = await toasts.getToast("Turn on key storage");
|
|
||||||
|
|
||||||
// And when we click Dismiss and then "Go to Settings"
|
|
||||||
await toast.getByRole("button", { name: "Dismiss" }).click();
|
|
||||||
await page.getByRole("button", { name: "Go to Settings" }).click();
|
|
||||||
|
|
||||||
// Then we see Encryption settings again
|
|
||||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
|
||||||
|
|
||||||
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
|
||||||
toast = await toasts.getToast("Turn on key storage");
|
|
||||||
await toast.getByRole("button", { name: "Dismiss" }).click();
|
|
||||||
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
|
||||||
|
|
||||||
// Then the toast is gone
|
|
||||||
await toasts.assertNoToasts();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -8,9 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils";
|
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
|
||||||
import { type Client } from "../../pages/client";
|
import { Client } from "../../pages/client";
|
||||||
|
|
||||||
test.describe("User verification", () => {
|
test.describe("User verification", () => {
|
||||||
// note that there are other tests that check user verification works in `crypto.spec.ts`.
|
// note that there are other tests that check user verification works in `crypto.spec.ts`.
|
||||||
@@ -32,17 +33,13 @@ test.describe("User verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can receive a verification request when there is no existing DM", async ({
|
test("can receive a verification request when there is no existing DM", async ({
|
||||||
app,
|
|
||||||
page,
|
page,
|
||||||
bot: bob,
|
bot: bob,
|
||||||
user: aliceCredentials,
|
user: aliceCredentials,
|
||||||
toasts,
|
toasts,
|
||||||
room: { roomId: dmRoomId },
|
room: { roomId: dmRoomId },
|
||||||
}) => {
|
}) => {
|
||||||
await waitForDevices(app, bob.credentials.userId, 1);
|
await waitForDeviceKeys(page);
|
||||||
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
|
||||||
const avatar = page.getByRole("button", { name: "Avatar" });
|
|
||||||
await avatar.click();
|
|
||||||
|
|
||||||
// once Alice has joined, Bob starts the verification
|
// once Alice has joined, Bob starts the verification
|
||||||
const bobVerificationRequest = await bob.evaluateHandle(
|
const bobVerificationRequest = await bob.evaluateHandle(
|
||||||
@@ -87,17 +84,13 @@ test.describe("User verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can abort emoji verification when emoji mismatch", async ({
|
test("can abort emoji verification when emoji mismatch", async ({
|
||||||
app,
|
|
||||||
page,
|
page,
|
||||||
bot: bob,
|
bot: bob,
|
||||||
user: aliceCredentials,
|
user: aliceCredentials,
|
||||||
toasts,
|
toasts,
|
||||||
room: { roomId: dmRoomId },
|
room: { roomId: dmRoomId },
|
||||||
}) => {
|
}) => {
|
||||||
await waitForDevices(app, bob.credentials.userId, 1);
|
await waitForDeviceKeys(page);
|
||||||
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
|
||||||
const avatar = page.getByRole("button", { name: "Avatar" });
|
|
||||||
await avatar.click();
|
|
||||||
|
|
||||||
// once Alice has joined, Bob starts the verification
|
// once Alice has joined, Bob starts the verification
|
||||||
const bobVerificationRequest = await bob.evaluateHandle(
|
const bobVerificationRequest = await bob.evaluateHandle(
|
||||||
@@ -161,3 +154,15 @@ async function createDMRoom(client: Client, userId: string): Promise<string> {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until we get the other user's device keys.
|
||||||
|
* In newer rust-crypto versions, the verification request will be ignored if we
|
||||||
|
* don't have the sender's device keys.
|
||||||
|
*/
|
||||||
|
async function waitForDeviceKeys(page: Page): Promise<void> {
|
||||||
|
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||||
|
const avatar = await page.getByRole("button", { name: "Avatar" });
|
||||||
|
await avatar.click();
|
||||||
|
await expect(page.getByText("1 session")).toBeVisible();
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, type JSHandle, type Page } from "@playwright/test";
|
import { expect, JSHandle, type Page } from "@playwright/test";
|
||||||
|
|
||||||
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import type {
|
import type {
|
||||||
@@ -18,9 +18,9 @@ import type {
|
|||||||
Verifier,
|
Verifier,
|
||||||
VerifierEvent,
|
VerifierEvent,
|
||||||
} from "matrix-js-sdk/src/crypto-api";
|
} from "matrix-js-sdk/src/crypto-api";
|
||||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||||
import { type Client } from "../../pages/client";
|
import { Client } from "../../pages/client";
|
||||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,13 +28,11 @@ import { Bot } from "../../pages/bot";
|
|||||||
* @param page - the playwright `page` fixture
|
* @param page - the playwright `page` fixture
|
||||||
* @param homeserver - the homeserver to use
|
* @param homeserver - the homeserver to use
|
||||||
* @param credentials - the credentials to use for the bot client
|
* @param credentials - the credentials to use for the bot client
|
||||||
* @param usePassphrase - whether to use a passphrase when creating the recovery key
|
|
||||||
*/
|
*/
|
||||||
export async function createBot(
|
export async function createBot(
|
||||||
page: Page,
|
page: Page,
|
||||||
homeserver: HomeserverInstance,
|
homeserver: HomeserverInstance,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
usePassphrase = false,
|
|
||||||
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
|
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
|
||||||
// Visit the login page of the app, to load the matrix sdk
|
// Visit the login page of the app, to load the matrix sdk
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
@@ -46,7 +44,6 @@ export async function createBot(
|
|||||||
const botClient = new Bot(page, homeserver, {
|
const botClient = new Bot(page, homeserver, {
|
||||||
bootstrapCrossSigning: true,
|
bootstrapCrossSigning: true,
|
||||||
bootstrapSecretStorage: true,
|
bootstrapSecretStorage: true,
|
||||||
usePassphrase,
|
|
||||||
});
|
});
|
||||||
botClient.setCredentials(credentials);
|
botClient.setCredentials(credentials);
|
||||||
// Backup is prepared in the background. Poll until it is ready.
|
// Backup is prepared in the background. Poll until it is ready.
|
||||||
@@ -142,16 +139,14 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
|
|||||||
* Check that the current device is connected to the expected key backup.
|
* Check that the current device is connected to the expected key backup.
|
||||||
* Also checks that the decryption key is known and cached locally.
|
* Also checks that the decryption key is known and cached locally.
|
||||||
*
|
*
|
||||||
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
|
* @param page - the page to check
|
||||||
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
|
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
|
||||||
* @param checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally
|
* @param checkBackupKeyInCache - whether to check that the backup key is cached locally.
|
||||||
* @param checkBackupKeyIn4S - whether to check that the backup key is stored in 4S
|
|
||||||
*/
|
*/
|
||||||
export async function checkDeviceIsConnectedKeyBackup(
|
export async function checkDeviceIsConnectedKeyBackup(
|
||||||
app: ElementAppPage,
|
page: Page,
|
||||||
expectedBackupVersion: string,
|
expectedBackupVersion: string,
|
||||||
checkBackupPrivateKeyInCache: boolean,
|
checkBackupKeyInCache: boolean,
|
||||||
checkBackupKeyIn4S: boolean = true,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
|
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
|
||||||
if (!expectedBackupVersion) {
|
if (!expectedBackupVersion) {
|
||||||
@@ -160,48 +155,23 @@ export async function checkDeviceIsConnectedKeyBackup(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const backupData = await app.client.evaluate(async (client: MatrixClient) => {
|
await page.getByRole("button", { name: "User menu" }).click();
|
||||||
const crypto = client.getCrypto();
|
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
|
||||||
if (!crypto) return;
|
await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
|
||||||
|
|
||||||
const backupInfo = await crypto.getKeyBackupInfo();
|
// expand the advanced section to see the active version in the reports
|
||||||
const backupKeyIn4S = Boolean(await client.isKeyBackupKeyStored());
|
await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
|
||||||
const backupPrivateKeyFromCache = await crypto.getSessionBackupPrivateKey();
|
|
||||||
const hasBackupPrivateKeyFromCache = Boolean(backupPrivateKeyFromCache);
|
|
||||||
const backupPrivateKeyWellFormed = backupPrivateKeyFromCache instanceof Uint8Array;
|
|
||||||
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
|
|
||||||
|
|
||||||
return {
|
if (checkBackupKeyInCache) {
|
||||||
backupInfo,
|
const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
|
||||||
hasBackupPrivateKeyFromCache,
|
await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
|
||||||
backupPrivateKeyWellFormed,
|
|
||||||
backupKeyIn4S,
|
|
||||||
activeBackupVersion,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!backupData) {
|
|
||||||
throw new Error("Crypto module is not available");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { backupInfo, backupKeyIn4S, hasBackupPrivateKeyFromCache, backupPrivateKeyWellFormed, activeBackupVersion } =
|
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||||
backupData;
|
expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
||||||
|
);
|
||||||
|
|
||||||
// We have a key backup
|
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
|
||||||
expect(backupInfo).toBeDefined();
|
|
||||||
// The key backup version is as expected
|
|
||||||
expect(backupInfo.version).toBe(expectedBackupVersion);
|
|
||||||
// The active backup version is as expected
|
|
||||||
expect(activeBackupVersion).toBe(expectedBackupVersion);
|
|
||||||
// The backup key is stored in 4S
|
|
||||||
if (checkBackupKeyIn4S) expect(backupKeyIn4S).toBe(true);
|
|
||||||
|
|
||||||
if (checkBackupPrivateKeyInCache) {
|
|
||||||
// The backup key is available locally
|
|
||||||
expect(hasBackupPrivateKeyFromCache).toBe(true);
|
|
||||||
// The backup key is well-formed
|
|
||||||
expect(backupPrivateKeyWellFormed).toBe(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,18 +188,10 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
|||||||
|
|
||||||
// if a securityKey was given, verify the new device
|
// if a securityKey was given, verify the new device
|
||||||
if (securityKey !== undefined) {
|
if (securityKey !== undefined) {
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
|
||||||
|
// Fill in the security key
|
||||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||||
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||||
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
|
||||||
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
|
||||||
if (await useSecurityKey.isVisible()) {
|
|
||||||
await useSecurityKey.click();
|
|
||||||
}
|
|
||||||
// Fill in the recovery key
|
|
||||||
await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
|
|
||||||
await page.getByRole("button", { name: "Continue", disabled: false }).click();
|
|
||||||
await page.getByRole("button", { name: "Done" }).click();
|
await page.getByRole("button", { name: "Done" }).click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,19 +216,18 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the encryption settings, and verify the current session using the recovery key.
|
* Open the security settings, and verify the current session using the security key.
|
||||||
*
|
*
|
||||||
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
||||||
* @param securityKey - The recovery key (i.e., 4S key), set up during a previous session.
|
* @param securityKey - The security key (i.e., 4S key), set up during a previous session.
|
||||||
*/
|
*/
|
||||||
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||||
const settings = await app.settings.openUserSettings("Encryption");
|
const settings = await app.settings.openUserSettings("Security & Privacy");
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
await settings.getByRole("button", { name: "Verify this session" }).click();
|
||||||
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
|
||||||
await app.page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
|
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
await app.page.getByRole("button", { name: "Done" }).click();
|
await app.page.getByRole("button", { name: "Done" }).click();
|
||||||
await app.settings.closeDialog();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -292,95 +253,32 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the encryption settings and enable key storage and recovery
|
* Open the security settings and enable secure key backup.
|
||||||
* Assumes that the current device has been verified
|
|
||||||
*
|
*
|
||||||
* Returns the recovery key
|
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
|
||||||
|
*
|
||||||
|
* Returns the security key
|
||||||
*/
|
*/
|
||||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||||
|
const dialog = app.page.locator(".mx_Dialog");
|
||||||
|
// Recovery key is selected by default
|
||||||
|
await dialog.getByRole("button", { name: "Continue" }).click({ timeout: 60000 });
|
||||||
|
|
||||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
// copy the text ourselves
|
||||||
if (!(await keyStorageToggle.isChecked())) {
|
const securityKey = await dialog.locator(".mx_CreateSecretStorageDialog_recoveryKey code").textContent();
|
||||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
await copyAndContinue(app.page);
|
||||||
}
|
|
||||||
|
|
||||||
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
await dialog.getByRole("button", { name: "Done" }).click();
|
||||||
|
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||||
|
|
||||||
const recoveryKey = await encryptionTab.getByTestId("recoveryKey").innerText();
|
return securityKey;
|
||||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
|
||||||
await encryptionTab.getByRole("textbox").fill(recoveryKey);
|
|
||||||
await encryptionTab.getByRole("button", { name: "Finish set up" }).click();
|
|
||||||
await app.settings.closeDialog();
|
|
||||||
return recoveryKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the encryption settings and disable key storage (and recovery)
|
* Click on copy and continue buttons to dismiss the security key dialog
|
||||||
* Assumes that the current device has been verified
|
|
||||||
*/
|
|
||||||
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
|
||||||
|
|
||||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
|
||||||
if (await keyStorageToggle.isChecked()) {
|
|
||||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
|
||||||
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
|
||||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
|
|
||||||
|
|
||||||
// Wait for the update to account data to stick
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
}
|
|
||||||
await app.settings.closeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
|
|
||||||
*
|
|
||||||
* Assumes the dialog is already open for some reason (see also {@link enableKeyBackup}).
|
|
||||||
*
|
|
||||||
* @param page - The playwright `Page` fixture.
|
|
||||||
* @param opts - Options object
|
|
||||||
* @param opts.accountPassword - The user's account password. If we are also resetting cross-signing, then we will need
|
|
||||||
* to upload the public cross-signing keys, which will cause the app to prompt for the password.
|
|
||||||
*
|
|
||||||
* @returns the new recovery key.
|
|
||||||
*/
|
|
||||||
export async function completeCreateSecretStorageDialog(
|
|
||||||
page: Page,
|
|
||||||
opts?: { accountPassword?: string },
|
|
||||||
): Promise<string> {
|
|
||||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
|
||||||
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
|
||||||
// "Generate a Recovery Key" is selected by default
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Recovery Key" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
|
||||||
// copy the recovery key to use it later
|
|
||||||
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
|
|
||||||
// If the device is unverified, there should be a "Setting up keys" step.
|
|
||||||
// If this is not the first time we are setting up cross-signing, the app will prompt for our password; otherwise
|
|
||||||
// the step is quite quick, and playwright can miss it, so we can't test for it.
|
|
||||||
if (opts && Object.hasOwn(opts, "accountPassword")) {
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Setting up keys" })).toBeVisible();
|
|
||||||
await page.getByPlaceholder("Password").fill(opts!.accountPassword);
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue" }).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either way, we end up at a success dialog:
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByText("Secure Backup successful")).not.toBeVisible();
|
|
||||||
|
|
||||||
return recoveryKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click on copy and continue buttons to dismiss the recovery key dialog
|
|
||||||
*/
|
*/
|
||||||
export async function copyAndContinue(page: Page) {
|
export async function copyAndContinue(page: Page) {
|
||||||
await page.getByRole("button", { name: "Copy" }).click();
|
await page.getByRole("button", { name: "Copy" }).click();
|
||||||
@@ -537,31 +435,3 @@ export async function deleteCachedSecrets(page: Page) {
|
|||||||
});
|
});
|
||||||
await page.reload();
|
await page.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait until the given user has a given number of devices.
|
|
||||||
* This function will check the device keys ten times and if
|
|
||||||
* the expected number of devices were not found by then, an
|
|
||||||
* error is thrown.
|
|
||||||
*/
|
|
||||||
export async function waitForDevices(
|
|
||||||
app: ElementAppPage,
|
|
||||||
userId: string,
|
|
||||||
expectedNumberOfDevices: number,
|
|
||||||
): Promise<void> {
|
|
||||||
const result = await app.client.evaluate(
|
|
||||||
async (cli, { userId, expectedNumberOfDevices }) => {
|
|
||||||
for (let i = 0; i < 10; ++i) {
|
|
||||||
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], true);
|
|
||||||
const deviceMap = userDeviceMap?.get(userId);
|
|
||||||
if (deviceMap.size === expectedNumberOfDevices) return true;
|
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
{ userId, expectedNumberOfDevices },
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(`User ${userId} did not have ${expectedNumberOfDevices} devices within ten iterations!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type APIRequestContext } from "@playwright/test";
|
import { APIRequestContext } from "playwright-core";
|
||||||
import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
|
|
||||||
|
|
||||||
import { type HomeserverInstance } from "../plugins/homeserver";
|
import { HomeserverInstance } from "../plugins/homeserver";
|
||||||
|
import { ClientServerApi } from "../plugins/utils/api.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A small subset of the Client-Server API used to manipulate the state of the
|
* A small subset of the Client-Server API used to manipulate the state of the
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Devtools", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Alice",
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should render the devtools", { tag: "@screenshot" }, async ({ page, homeserver, user, app, axe }) => {
|
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
|
|
||||||
const composer = app.getComposer().locator("[contenteditable]");
|
|
||||||
await composer.fill("/devtools");
|
|
||||||
await composer.press("Enter");
|
|
||||||
const dialog = page.locator(".mx_Dialog");
|
|
||||||
await dialog.getByLabel("Developer mode").check();
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(dialog).toMatchScreenshot("devtools-dialog.png", {
|
|
||||||
css: `.mx_CopyableText {
|
|
||||||
display: none;
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Room upgrade dialog", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Alice",
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should render the room upgrade dialog",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, homeserver, user, app, axe }) => {
|
|
||||||
// Enable developer mode
|
|
||||||
await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true);
|
|
||||||
|
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
|
|
||||||
const composer = app.getComposer().locator("[contenteditable]");
|
|
||||||
// Pick a room version that is likely to be supported by all our target homeservers.
|
|
||||||
await composer.fill("/upgraderoom 5");
|
|
||||||
await composer.press("Enter");
|
|
||||||
const dialog = page.locator(".mx_Dialog");
|
|
||||||
await dialog.getByLabel("Automatically invite members from this room to the new one").check();
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(dialog).toMatchScreenshot("upgrade-room.png");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Locator, type Page } from "@playwright/test";
|
import { Locator, Page } from "@playwright/test";
|
||||||
|
|
||||||
import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
|
import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
@@ -267,6 +267,7 @@ test.describe("Editing", () => {
|
|||||||
app,
|
app,
|
||||||
room,
|
room,
|
||||||
axe,
|
axe,
|
||||||
|
checkA11y,
|
||||||
}) => {
|
}) => {
|
||||||
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
||||||
|
|
||||||
@@ -281,7 +282,7 @@ test.describe("Editing", () => {
|
|||||||
const line = tile.locator(".mx_EventTile_line");
|
const line = tile.locator(".mx_EventTile_line");
|
||||||
await line.hover();
|
await line.hover();
|
||||||
await line.getByRole("button", { name: "Edit" }).click();
|
await line.getByRole("button", { name: "Edit" }).click();
|
||||||
await expect(axe).toHaveNoViolations();
|
await checkA11y();
|
||||||
const editComposer = page.getByRole("textbox", { name: "Edit message" });
|
const editComposer = page.getByRole("textbox", { name: "Edit message" });
|
||||||
await editComposer.pressSequentially("Foo");
|
await editComposer.pressSequentially("Foo");
|
||||||
await editComposer.press("Backspace");
|
await editComposer.press("Backspace");
|
||||||
@@ -289,7 +290,7 @@ test.describe("Editing", () => {
|
|||||||
await editComposer.press("Backspace");
|
await editComposer.press("Backspace");
|
||||||
await editComposer.press("Enter");
|
await editComposer.press("Enter");
|
||||||
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
|
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
|
||||||
await expect(axe).toHaveNoViolations();
|
await checkA11y();
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
|
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
|
||||||
@@ -304,6 +305,7 @@ test.describe("Editing", () => {
|
|||||||
user,
|
user,
|
||||||
app,
|
app,
|
||||||
axe,
|
axe,
|
||||||
|
checkA11y,
|
||||||
bot: bob,
|
bot: bob,
|
||||||
}) => {
|
}) => {
|
||||||
// This tests the behaviour when a message has been edited some time after it has been sent, and we
|
// This tests the behaviour when a message has been edited some time after it has been sent, and we
|
||||||
|
|||||||
@@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type CredentialsWithDisplayName, expect, test as base } from "../../element-web-test";
|
import { expect, test as base } from "../../element-web-test";
|
||||||
import { selectHomeserver } from "../utils";
|
import { selectHomeserver } from "../utils";
|
||||||
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
import { Credentials } from "../../plugins/homeserver";
|
||||||
|
|
||||||
const email = "user@nowhere.dummy";
|
const email = "user@nowhere.dummy";
|
||||||
|
|
||||||
const test = base.extend({
|
const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
credentials: async ({}, use, testInfo) => {
|
credentials: async ({}, use, testInfo) => {
|
||||||
await use({
|
await use({
|
||||||
username: `user_${testInfo.testId}`,
|
username: `user_${testInfo.testId}`,
|
||||||
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||||
password: "oETo7MPf0o",
|
password: "oETo7MPf0o",
|
||||||
} as CredentialsWithDisplayName);
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Decline and block invite dialog", function () {
|
|
||||||
test.use({
|
|
||||||
displayName: "Hanako",
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should show decline and block dialog for a room",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, user, bot, axe }) => {
|
|
||||||
await bot.createRoom({ name: "Test Room", invite: [user.userId] });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
await page.getByRole("button", { name: "Decline and block" }).click();
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("decline-and-block-invite-empty.png");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -77,7 +77,7 @@ test.describe("Invite dialog", function () {
|
|||||||
"should support inviting a user to Direct Messages",
|
"should support inviting a user to Direct Messages",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ page, app, user, bot }) => {
|
async ({ page, app, user, bot }) => {
|
||||||
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||||
|
|
||||||
const other = page.locator(".mx_InviteDialog_other");
|
const other = page.locator(".mx_InviteDialog_other");
|
||||||
// Assert that the header is rendered
|
// Assert that the header is rendered
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Bot } from "../../pages/bot";
|
|||||||
import type { Locator, Page } from "@playwright/test";
|
import type { Locator, Page } from "@playwright/test";
|
||||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { type Credentials } from "../../plugins/homeserver";
|
import { Credentials } from "../../plugins/homeserver";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
test.describe("Lazy Loading", () => {
|
test.describe("Lazy Loading", () => {
|
||||||
@@ -30,10 +30,6 @@ test.describe("Lazy Loading", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
|
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
|
||||||
// The charlies were running off the bottom of the screen.
|
|
||||||
// We no longer overscan the member list so the result is they are not in the dom.
|
|
||||||
// Increase the viewport size to ensure they are.
|
|
||||||
await page.setViewportSize({ width: 1000, height: 1000 });
|
|
||||||
for (let i = 1; i <= 10; i++) {
|
for (let i = 1; i <= 10; i++) {
|
||||||
const displayName = `Charly #${i}`;
|
const displayName = `Charly #${i}`;
|
||||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
||||||
|
|||||||
@@ -1,376 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type Visibility } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { type Locator, type Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import { expect, test } from "../../../element-web-test";
|
|
||||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
|
||||||
|
|
||||||
test.describe("Room list filters and sort", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Alice",
|
|
||||||
botCreateOpts: {
|
|
||||||
displayName: "BotBob",
|
|
||||||
autoAcceptInvites: true,
|
|
||||||
},
|
|
||||||
labsFlags: ["feature_new_room_list"],
|
|
||||||
});
|
|
||||||
|
|
||||||
function getPrimaryFilters(page: Page): Locator {
|
|
||||||
return page.getByTestId("primary-filters");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoomOptionsMenu(page: Page): Locator {
|
|
||||||
return page.getByRole("button", { name: "Room Options" });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilterExpandButton(page: Page): Locator {
|
|
||||||
return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilterCollapseButton(page: Page): Locator {
|
|
||||||
return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the room list
|
|
||||||
* @param page
|
|
||||||
*/
|
|
||||||
function getRoomList(page: Page) {
|
|
||||||
return page.getByTestId("room-list");
|
|
||||||
}
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
|
||||||
// The notification toast is displayed above the search section
|
|
||||||
await app.closeNotificationToast();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Tombstoned rooms are not shown even when they receive updates", async ({ page, app, bot }) => {
|
|
||||||
// This bug shows up with this setting turned on
|
|
||||||
await app.settings.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, true);
|
|
||||||
|
|
||||||
/*
|
|
||||||
We will first create a room named 'Old Room' and will invite the bot user to this room.
|
|
||||||
We will also send a simple message in this room.
|
|
||||||
*/
|
|
||||||
const oldRoomId = await app.client.createRoom({ name: "Old Room" });
|
|
||||||
await app.client.inviteUser(oldRoomId, bot.credentials.userId);
|
|
||||||
await bot.joinRoom(oldRoomId);
|
|
||||||
const response = await app.client.sendMessage(oldRoomId, "Hello!");
|
|
||||||
|
|
||||||
/*
|
|
||||||
At this point, we haven't done anything interesting.
|
|
||||||
So we expect 'Old Room' to show up in the room list.
|
|
||||||
*/
|
|
||||||
const roomListView = getRoomList(page);
|
|
||||||
const oldRoomTile = roomListView.getByRole("option", { name: "Open room Old Room" });
|
|
||||||
await expect(oldRoomTile).toBeVisible();
|
|
||||||
|
|
||||||
/*
|
|
||||||
Now let's tombstone 'Old Room'.
|
|
||||||
First we create a new room ('New Room') with the predecessor set to the old room..
|
|
||||||
*/
|
|
||||||
const newRoomId = await bot.createRoom({
|
|
||||||
name: "New Room",
|
|
||||||
creation_content: {
|
|
||||||
predecessor: {
|
|
||||||
event_id: response.event_id,
|
|
||||||
room_id: oldRoomId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visibility: "public" as Visibility,
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
Now we can send the tombstone event itself to the 'Old Room'.
|
|
||||||
*/
|
|
||||||
await app.client.sendStateEvent(oldRoomId, "m.room.tombstone", {
|
|
||||||
body: "This room has been replaced",
|
|
||||||
replacement_room: newRoomId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Let's join the replaced room.
|
|
||||||
await app.client.joinRoom(newRoomId);
|
|
||||||
|
|
||||||
// We expect 'Old Room' to be hidden from the room list.
|
|
||||||
await expect(oldRoomTile).not.toBeVisible();
|
|
||||||
|
|
||||||
/*
|
|
||||||
Let's say some user in the 'Old Room' changes their display name.
|
|
||||||
This will send events to the all the rooms including 'Old Room'.
|
|
||||||
Nevertheless, the replaced room should not be shown in the room list.
|
|
||||||
*/
|
|
||||||
await bot.setDisplayName("MyNewName");
|
|
||||||
await expect(oldRoomTile).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Scroll behaviour", () => {
|
|
||||||
test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({
|
|
||||||
page,
|
|
||||||
app,
|
|
||||||
}) => {
|
|
||||||
const createFavouriteRoom = async (name: string) => {
|
|
||||||
const id = await app.client.createRoom({
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
await app.client.evaluate(async (client, favouriteId) => {
|
|
||||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
|
||||||
}, id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create 5 favourite rooms
|
|
||||||
let i = 0;
|
|
||||||
for (; i < 5; i++) {
|
|
||||||
await createFavouriteRoom(`room${i}-fav`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a non-favourite room
|
|
||||||
await app.client.createRoom({ name: `room-non-fav` });
|
|
||||||
|
|
||||||
// Create rest of the favourite rooms
|
|
||||||
for (; i < 20; i++) {
|
|
||||||
await createFavouriteRoom(`room${i}-fav`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the non-favourite room
|
|
||||||
const roomListView = getRoomList(page);
|
|
||||||
const tile = roomListView.getByRole("option", { name: "Open room room-non-fav" });
|
|
||||||
// item may not be in the DOM using scrollListToBottom rather than scrollIntoViewIfNeeded
|
|
||||||
await app.scrollListToBottom(roomListView);
|
|
||||||
await tile.click();
|
|
||||||
|
|
||||||
// Enable Favourite filter
|
|
||||||
await getFilterExpandButton(page).click();
|
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
|
||||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
|
||||||
await expect(tile).not.toBeVisible();
|
|
||||||
|
|
||||||
// Ensure the room list is not scrolled
|
|
||||||
const isScrolledDown = await page
|
|
||||||
.getByRole("listbox", { name: "Room list", exact: true })
|
|
||||||
.evaluate((e) => e.scrollTop !== 0);
|
|
||||||
expect(isScrolledDown).toStrictEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Room list", () => {
|
|
||||||
let unReadDmId: string | undefined;
|
|
||||||
let unReadRoomId: string | undefined;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
|
||||||
await app.client.createRoom({ name: "empty room" });
|
|
||||||
|
|
||||||
unReadDmId = await bot.createRoom({
|
|
||||||
name: "unread dm",
|
|
||||||
invite: [user.userId],
|
|
||||||
is_direct: true,
|
|
||||||
});
|
|
||||||
await app.client.joinRoom(unReadDmId);
|
|
||||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
|
||||||
|
|
||||||
unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
|
||||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
|
||||||
await bot.joinRoom(unReadRoomId);
|
|
||||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
|
||||||
|
|
||||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
|
||||||
await app.client.evaluate(async (client, favouriteId) => {
|
|
||||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
|
||||||
}, favouriteId);
|
|
||||||
|
|
||||||
const lowPrioId = await app.client.createRoom({ name: "Low prio room" });
|
|
||||||
await app.client.evaluate(async (client, id) => {
|
|
||||||
await client.setRoomTag(id, "m.lowpriority", { order: 0.5 });
|
|
||||||
}, lowPrioId);
|
|
||||||
|
|
||||||
await bot.createRoom({
|
|
||||||
name: "invited room",
|
|
||||||
invite: [user.userId],
|
|
||||||
is_direct: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mentionRoomId = await app.client.createRoom({ name: "room with mention" });
|
|
||||||
await app.client.inviteUser(mentionRoomId, bot.credentials.userId);
|
|
||||||
await bot.joinRoom(mentionRoomId);
|
|
||||||
|
|
||||||
const clientBot = await bot.prepareClient();
|
|
||||||
await clientBot.evaluate(
|
|
||||||
async (client, { mentionRoomId, userId }) => {
|
|
||||||
await client.sendMessage(mentionRoomId, {
|
|
||||||
// @ts-ignore ignore usage of MsgType.text
|
|
||||||
"msgtype": "m.text",
|
|
||||||
"body": "User",
|
|
||||||
"format": "org.matrix.custom.html",
|
|
||||||
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
|
||||||
"m.mentions": {
|
|
||||||
user_ids: [userId],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ mentionRoomId, userId: user.userId },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
|
||||||
const roomList = getRoomList(page);
|
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
|
||||||
|
|
||||||
const allFilters = await primaryFilters.locator("option").all();
|
|
||||||
for (const filter of allFilters) {
|
|
||||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
|
||||||
}
|
|
||||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
|
||||||
// only one room should be visible
|
|
||||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
|
||||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(4);
|
|
||||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
|
||||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
|
||||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(2);
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
|
||||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
|
||||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
|
||||||
await expect(roomList.getByRole("option", { name: "empty room" })).toBeVisible();
|
|
||||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
|
||||||
await expect(roomList.getByRole("option", { name: "Low prio room" })).toBeVisible();
|
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(5);
|
|
||||||
|
|
||||||
await getFilterExpandButton(page).click();
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
|
||||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
|
||||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
|
||||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
|
||||||
|
|
||||||
await getFilterCollapseButton(page).click();
|
|
||||||
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"unread filter should only match unread rooms that have a count",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, bot }) => {
|
|
||||||
const roomListView = getRoomList(page);
|
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
|
||||||
|
|
||||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
|
||||||
await app.viewRoomById(unReadDmId);
|
|
||||||
await app.settings.openRoomSettings("Notifications");
|
|
||||||
await page.getByText("@mentions & keywords").click();
|
|
||||||
await app.settings.closeDialog();
|
|
||||||
|
|
||||||
// Let's open a room other than unread room or unread dm
|
|
||||||
await roomListView.getByRole("option", { name: "Open room favourite room" }).click();
|
|
||||||
|
|
||||||
// Let's make the bot send a new message in both rooms
|
|
||||||
await bot.sendMessage(unReadDmId, "Hello!");
|
|
||||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
|
||||||
|
|
||||||
// Let's activate the unread filter now
|
|
||||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
|
||||||
|
|
||||||
// Unread filter should only show unread room and not unread dm!
|
|
||||||
const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" });
|
|
||||||
await expect(unreadDm).toBeVisible();
|
|
||||||
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test("should sort the room list alphabetically", async ({ page }) => {
|
|
||||||
const roomListView = getRoomList(page);
|
|
||||||
|
|
||||||
await getRoomOptionsMenu(page).click();
|
|
||||||
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
|
||||||
|
|
||||||
await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
|
||||||
const roomListView = getRoomList(page);
|
|
||||||
|
|
||||||
await bot.sendMessage(unReadDmId, "Hello!");
|
|
||||||
|
|
||||||
await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Empty room list", () => {
|
|
||||||
/**
|
|
||||||
* Get the empty state
|
|
||||||
* @param page
|
|
||||||
*/
|
|
||||||
function getEmptyRoomList(page: Page) {
|
|
||||||
return page.getByTestId("empty-room-list");
|
|
||||||
}
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should render the default placeholder when there is no filter",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, user }) => {
|
|
||||||
const emptyRoomList = getEmptyRoomList(page);
|
|
||||||
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
|
|
||||||
await expect(page.getByRole("navigation", { name: "Room list" })).toMatchScreenshot(
|
|
||||||
"room-panel-empty-room-list.png",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
[
|
|
||||||
{ filter: "Unreads", action: "Show all chats" },
|
|
||||||
{ filter: "Mentions", action: "See all activity" },
|
|
||||||
{ filter: "Invites", action: "See all activity" },
|
|
||||||
].forEach(({ filter, action }) => {
|
|
||||||
test(
|
|
||||||
`should render the placeholder for ${filter} filter`,
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, user }) => {
|
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
|
||||||
await getFilterExpandButton(page).click();
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: filter }).click();
|
|
||||||
|
|
||||||
const emptyRoomList = getEmptyRoomList(page);
|
|
||||||
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
|
||||||
|
|
||||||
await emptyRoomList.getByRole("button", { name: action }).click();
|
|
||||||
await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
["People", "Rooms", "Favourite"].forEach((filter) => {
|
|
||||||
test(
|
|
||||||
`should render the placeholder for ${filter} filter`,
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, user }) => {
|
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
|
||||||
await getFilterExpandButton(page).click();
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: filter }).click();
|
|
||||||
|
|
||||||
const emptyRoomList = getEmptyRoomList(page);
|
|
||||||
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user