Merge remote-tracking branch 'github.com/develop' into develop
Some checks failed
Localazy Upload / upload (push) Failing after 0s
Static Analysis / i18n Check (push) Failing after 0s
Docker / Docker Buildx (push) Has been cancelled
Build / Build on macos-14 (push) Has been cancelled
Build / Build on ubuntu-24.04 (push) Has been cancelled
Build / Build on windows-2022 (push) Has been cancelled
Build and Deploy develop / Build & Deploy develop.element.io (push) Has been cancelled
Deploy documentation / GitHub Pages (push) Has been cancelled
Static Analysis / Typescript Syntax Check (push) Has been cancelled
Static Analysis / Rethemendex Check (push) Has been cancelled
Static Analysis / ESLint (push) Has been cancelled
Static Analysis / Style Lint (push) Has been cancelled
Static Analysis / Workflow Lint (push) Has been cancelled
Static Analysis / Analyse Dead Code (push) Has been cancelled
Deploy documentation / deploy (push) Has been cancelled
Close stale issues & PRs / close (push) Has been cancelled
Update Playwright docker images / update (push) Has been cancelled
Update Jitsi / update (push) Has been cancelled
Sync labels / sync-labels (push) Failing after 1s
Localazy Download / download (push) Failing after 1s

# Conflicts:
#	package.json
#	src/components/views/rooms/EventTile.tsx
#	yarn.lock
This commit is contained in:
2025-06-01 20:28:25 +02:00
472 changed files with 13046 additions and 7390 deletions

View File

@@ -30,6 +30,10 @@ 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.",
@@ -55,6 +59,11 @@ 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",

View File

@@ -43,9 +43,9 @@ jobs:
run: run:
shell: bash shell: bash
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: webapp-${{ matrix.image }} name: webapp-${{ matrix.image }}
path: webapp path: webapp

View File

@@ -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@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: element-web.deb name: element-web.deb
path: element-web.deb path: element-web.deb

View File

@@ -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@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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@v4 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: webapp name: webapp
path: dist/develop.tar.gz path: dist/develop.tar.gz

View File

@@ -34,7 +34,7 @@ jobs:
env: env:
SITE: ${{ inputs.site || 'staging.element.io' }} SITE: ${{ inputs.site || 'staging.element.io' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Load GPG key - name: Load GPG key
run: | run: |

View File

@@ -20,12 +20,12 @@ jobs:
env: env:
TEST_TAG: vectorim/element-web:test TEST_TAG: vectorim/element-web:test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3 uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
- name: Set up QEMU - name: Set up QEMU

View File

@@ -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@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
path: element-web path: element-web
- name: Fetch matrix-js-sdk - name: Fetch matrix-js-sdk
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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@v2 uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # 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@v3 uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # 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@v4 uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

View File

@@ -25,7 +25,7 @@ jobs:
actions: read actions: read
steps: steps:
- name: Download HTML report - name: Download HTML report
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 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@v3 uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # 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 }}

View File

@@ -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@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
repository: element-hq/element-web repository: element-hq/element-web
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@v7 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: with:
name: webapp name: webapp
path: webapp path: webapp
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
cache: "yarn" cache: "yarn"
cache-dependency-path: yarn.lock cache-dependency-path: yarn.lock
@@ -154,12 +154,11 @@ 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@v4 uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
id: playwright-cache id: playwright-cache
with: with:
path: | path: ~/.cache/ms-playwright
~/.cache/ms-playwright key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}
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'
@@ -180,25 +179,35 @@ 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@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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: playwright needs:
- playwright
- downstream-modules
if: always() if: always()
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: inputs.skip != true if: inputs.skip != true
with: with:
cache: "yarn" cache: "yarn"
@@ -210,7 +219,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@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: with:
pattern: all-blob-reports-* pattern: all-blob-reports-*
path: all-blob-reports path: all-blob-reports
@@ -226,11 +235,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@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: html-report name: html-report
path: playwright-report path: playwright-report
retention-days: 14 retention-days: 14
- if: needs.playwright.result != 'skipped' && needs.playwright.result != 'success' - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1 run: exit 1

View File

@@ -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@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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:

View File

@@ -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@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 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@v3 uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # 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 }}

View File

@@ -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@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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 }}

View File

@@ -10,7 +10,7 @@ jobs:
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Update synapse image - name: Update synapse image
run: | run: |

View File

@@ -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@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with: with:
script: | script: |
const baseBranch = context.payload.pull_request.base.ref; const baseBranch = context.payload.pull_request.base.ref;

View File

@@ -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@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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

View File

@@ -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@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version: "lts/*"
@@ -57,7 +57,7 @@ jobs:
name: "Rethemendex Check" name: "Rethemendex Check"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- run: ./res/css/rethemendex.sh - run: ./res/css/rethemendex.sh
@@ -67,9 +67,9 @@ jobs:
name: "ESLint" name: "ESLint"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version: "lts/*"
@@ -85,9 +85,9 @@ jobs:
name: "Style Lint" name: "Style Lint"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version: "lts/*"
@@ -103,9 +103,9 @@ jobs:
name: "Workflow Lint" name: "Workflow Lint"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version: "lts/*"
@@ -121,9 +121,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@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version: "lts/*"

View File

@@ -39,12 +39,12 @@ jobs:
runner: [1, 2] runner: [1, 2]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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@v4 uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # 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@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: coverage-${{ matrix.runner }} name: coverage-${{ matrix.runner }}
path: | path: |

View File

@@ -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@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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@v1.0.2 uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # 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@v1.0.2 uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # 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 }}
@@ -150,15 +150,15 @@ jobs:
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 }}
verticals_feature: crypto:
name: Add labelled issues to Verticals Feature project name: Add labelled issues to Crypto project
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
if: > if: >
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature') contains(github.event.issue.labels.*.name, 'Team: Crypto')
steps: steps:
- uses: actions/add-to-project@main - uses: actions/add-to-project@main
with: with:
project-url: https://github.com/orgs/element-hq/projects/57 project-url: https://github.com/orgs/element-hq/projects/76
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
tech_debt: tech_debt:

View File

@@ -12,7 +12,7 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with: with:
operations-per-run: 100 operations-per-run: 100
# Flaky test issue closing # Flaky test issue closing

View File

@@ -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@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with: with:
script: | script: |
github.rest.issues.removeLabel({ github.rest.issues.removeLabel({

View File

@@ -9,9 +9,9 @@ jobs:
update: update:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version: "lts/*"

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
environment: Matrix environment: Matrix
steps: steps:
- uses: actions/github-script@v7 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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 }}

1
.gitignore vendored
View File

@@ -25,7 +25,6 @@ electron/pub
.env .env
/coverage /coverage
# Auto-generated file # Auto-generated file
/src/modules.ts
/src/modules.js /src/modules.js
/build_config.yaml /build_config.yaml
/book /book

View File

@@ -1,3 +1,88 @@
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) Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
================================================================================================== ==================================================================================================
## ✨ Features ## ✨ Features

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.15-labs # syntax=docker.io/docker/dockerfile:1.16-labs@sha256:bb5e2b225985193779991f3256d1901a0b3e6a0b284c7bffa0972064f4a6d458
# Builder # Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f16d8e8af67bb6361231e932b8b3e7afa040cbfed181719a450b02c3821b26c1 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
@@ -19,7 +19,7 @@ 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 FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:2acffd86b1bdefb8fa6b48b6e9aadf75430e8ab9c43c54c515ea7df77897f987
# Need root user to install packages & manipulate the usr directory # Need root user to install packages & manipulate the usr directory
USER root USER root

View File

@@ -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](#setting-up-a-dev-environment) below). Alternatively, you environment](./developer_guide.md#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

View File

@@ -101,10 +101,6 @@ 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.

View File

@@ -1,6 +1,6 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.97", "version": "1.11.101",
"description": "Element: the future of secure communication", "description": "Element: the future of secure communication",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
@@ -69,19 +69,19 @@
}, },
"resolutions": { "resolutions": {
"**/pretty-format/react-is": "19.1.0", "**/pretty-format/react-is": "19.1.0",
"@playwright/test": "1.51.1", "@playwright/test": "1.52.0",
"@types/react": "19.1.1", "@types/react": "19.1.6",
"@types/react-dom": "19.1.2", "@types/react-dom": "19.1.5",
"oidc-client-ts": "3.2.0", "oidc-client-ts": "3.2.1",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001714", "caniuse-lite": "1.0.30001720",
"testcontainers": "10.24.2", "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": "^0.1.1", "@element-hq/element-web-module-api": "1.0.0",
"@fontsource/inconsolata": "^5", "@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5", "@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7", "@formatjs/intl-segmenter": "^11.5.7",
@@ -95,7 +95,7 @@
"@types/png-chunks-extract": "^1.0.2", "@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30", "@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^4.0.0", "@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.10.1", "@vector-im/compound-web": "^7.11.0",
"@vector-im/matrix-wysiwyg": "2.38.3", "@vector-im/matrix-wysiwyg": "2.38.3",
"@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",
@@ -125,9 +125,9 @@
"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.2.0", "linkify-react": "4.3.1",
"linkify-string": "4.2.0", "linkify-string": "4.3.1",
"linkifyjs": "4.2.0", "linkifyjs": "4.3.1",
"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",
@@ -140,7 +140,7 @@
"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.236.1", "posthog-js": "1.248.1",
"qrcode": "1.5.4", "qrcode": "1.5.4",
"re-resizable": "6.11.2", "re-resizable": "6.11.2",
"react": "^19.0.0", "react": "^19.0.0",
@@ -153,7 +153,7 @@
"react-virtualized": "^9.22.5", "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.15.0", "sanitize-html": "2.17.0",
"styled-components": "^6.1.17", "styled-components": "^6.1.17",
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"temporal-polyfill": "^0.3.0", "temporal-polyfill": "^0.3.0",
@@ -183,7 +183,7 @@
"@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.9.0", "@element-hq/element-call-embedded": "0.12.0",
"@element-hq/element-web-playwright-common": "^1.1.5", "@element-hq/element-web-playwright-common": "^1.1.5",
"@peculiar/webcrypto": "^1.4.3", "@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1", "@playwright/test": "^1.50.1",
@@ -215,11 +215,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.1", "@types/react": "19.1.6",
"@types/react-beautiful-dnd": "^13.0.0", "@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.1.2", "@types/react-dom": "19.1.5",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.15.0", "@types/sanitize-html": "2.16.0",
"@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",
@@ -265,7 +265,7 @@
"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": "^15.0.2", "lint-staged": "^16.0.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",
@@ -294,7 +294,7 @@
"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": "^10.20.0", "testcontainers": "^11.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "5.8.3", "typescript": "5.8.3",
"util": "^0.12.5", "util": "^0.12.5",
@@ -314,5 +314,6 @@
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
} },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@@ -1,5 +1,5 @@
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
index 2272032..18bd20a 100644 index d3318dc..c2b2c77 100644
--- a/node_modules/@types/react/index.d.ts --- a/node_modules/@types/react/index.d.ts
+++ b/node_modules/@types/react/index.d.ts +++ b/node_modules/@types/react/index.d.ts
@@ -134,7 +134,7 @@ declare namespace React { @@ -134,7 +134,7 @@ declare namespace React {
@@ -11,7 +11,7 @@ index 2272032..18bd20a 100644
/** /**
* Created by {@link createRef}, or {@link useRef} when passed `null`. * Created by {@link createRef}, or {@link useRef} when passed `null`.
@@ -941,7 +941,7 @@ declare namespace React { @@ -945,7 +945,7 @@ declare namespace React {
context: unknown; context: unknown;
// Keep in sync with constructor signature of JSXElementConstructor and ComponentClass. // Keep in sync with constructor signature of JSXElementConstructor and ComponentClass.
@@ -20,7 +20,7 @@ index 2272032..18bd20a 100644
// We MUST keep setState() as a unified signature because it allows proper checking of the method return type. // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257 // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
@@ -1113,7 +1113,7 @@ declare namespace React { @@ -1117,7 +1117,7 @@ declare namespace React {
*/ */
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> { interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
// constructor signature must match React.Component // constructor signature must match React.Component

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts"; import { createBot, logIntoElement } from "./utils.ts";
import { type Client } from "../../pages/client.ts"; import { type Client } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts"; import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
@@ -28,21 +28,27 @@ 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("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
// Verify the device by resetting the key (which will create SSSS, and dehydrated device) // Verify the device by resetting the identity key, and then set up recovery (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.getByText("Offline device enabled")).not.toBeVisible(); await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
await app.closeDialog(); await app.closeDialog();
// Verify the device by resetting the key // Reset the identity key
const settings = await app.settings.openUserSettings("Encryption"); const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click(); await settings.getByRole("button", { name: "Verify this device" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click(); await page.getByRole("button", { name: "Proceed with reset" }).click();
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Copy" }).click();
// Set up recovery
await page.getByRole("button", { name: "Set up recovery" }).click();
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Done" }).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); await expectDehydratedDeviceEnabled(app);
@@ -80,7 +86,7 @@ test.describe("Dehydration", () => {
await expectDehydratedDeviceEnabled(app); await expectDehydratedDeviceEnabled(app);
}); });
test("Reset recovery key during login re-creates dehydrated device", async ({ test("Reset identity during login and set up recovery re-creates dehydrated device", async ({
page, page,
homeserver, homeserver,
app, app,
@@ -99,16 +105,26 @@ test.describe("Dehydration", () => {
// Log in our client // Log in our client
await logIntoElement(page, credentials); await logIntoElement(page, credentials);
// Oh no, we forgot our recovery key // Oh no, we forgot our recovery key - reset our identity
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).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();
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password }); // 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 // There should be a brand new dehydrated device
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client); await expectDehydratedDeviceEnabled(app);
expect(dehydratedDeviceIds.length).toBe(1);
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
}); });
test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => { test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => {

View File

@@ -169,8 +169,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Fill the passphrase // Fill the passphrase
const dialog = page.locator(".mx_Dialog"); const dialog = page.locator(".mx_Dialog");
await dialog.locator("input").fill("new passphrase"); await dialog.locator("textarea").fill("new passphrase");
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
@@ -190,10 +190,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Fill the recovery key // Fill the recovery key
const dialog = page.locator(".mx_Dialog"); const dialog = page.locator(".mx_Dialog");
await dialog.getByRole("button", { name: "use your Recovery Key" }).click();
const aliceRecoveryKey = await aliceBotClient.getRecoveryKey(); const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey); await dialog.locator("textarea").fill(aliceRecoveryKey.encodedPrivateKey);
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();

View File

@@ -67,8 +67,9 @@ test.describe("Cryptography", function () {
// Bob has a second, not cross-signed, device // Bob has a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
// Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list // Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
await page.getByRole("button", { name: "Not now" }).click(); await page.getByRole("button", { name: "Dismiss" }).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",

View File

@@ -8,7 +8,8 @@
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils"; import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
import { type Bot } from "../../pages/bot";
test.describe("Key storage out of sync toast", () => { test.describe("Key storage out of sync toast", () => {
let recoveryKey: GeneratedSecretStorageKey; let recoveryKey: GeneratedSecretStorageKey;
@@ -53,3 +54,114 @@ test.describe("Key storage out of sync toast", () => {
).toBeVisible(); ).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("checkbox", { 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("checkbox", { 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();
});
});

View File

@@ -228,8 +228,8 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
await useSecurityKey.click(); await useSecurityKey.click();
} }
// Fill in the recovery key // Fill in the recovery key
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); await page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await page.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click(); await page.getByRole("button", { name: "Done" }).click();
} }
} }
@@ -263,7 +263,7 @@ export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption"); const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click(); await settings.getByRole("button", { name: "Verify this device" }).click();
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click(); await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); await app.page.locator(".mx_Dialog").locator("textarea").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(); await app.settings.closeDialog();
@@ -316,6 +316,25 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
return recoveryKey; return recoveryKey;
} }
/**
* Open the encryption settings and disable key storage (and recovery)
* 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("checkbox", { name: "Allow key storage" });
if (await keyStorageToggle.isChecked()) {
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
await encryptionTab.getByRole("checkbox", { 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`). * Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
* *

View File

@@ -5,8 +5,11 @@
* Please see LICENSE files in the repository root for full details. * 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 { expect, test } from "../../../element-web-test";
import type { Page } from "@playwright/test"; import { SettingLevel } from "../../../../src/settings/SettingLevel";
test.describe("Room list filters and sort", () => { test.describe("Room list filters and sort", () => {
test.use({ test.use({
@@ -18,10 +21,14 @@ test.describe("Room list filters and sort", () => {
labsFlags: ["feature_new_room_list"], labsFlags: ["feature_new_room_list"],
}); });
function getPrimaryFilters(page: Page) { function getPrimaryFilters(page: Page): Locator {
return page.getByRole("listbox", { name: "Room list filters" }); return page.getByRole("listbox", { name: "Room list filters" });
} }
function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}
/** /**
* Get the room list * Get the room list
* @param page * @param page
@@ -35,6 +42,65 @@ test.describe("Room list filters and sort", () => {
await app.closeNotificationToast(); 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("gridcell", { 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.describe("Scroll behaviour", () => {
test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({ test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({
page, page,
@@ -106,6 +172,38 @@ test.describe("Room list filters and sort", () => {
await app.client.evaluate(async (client, favouriteId) => { await app.client.evaluate(async (client, favouriteId) => {
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 }); await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
}, favouriteId); }, 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 }) => { test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
@@ -122,7 +220,7 @@ test.describe("Room list filters and sort", () => {
// only one room should be visible // only one room should be visible
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(2); expect(await roomList.locator("role=gridcell").count()).toBe(4);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png"); await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Favourite" }).click(); await primaryFilters.getByRole("option", { name: "Favourite" }).click();
@@ -131,37 +229,71 @@ test.describe("Room list filters and sort", () => {
await primaryFilters.getByRole("option", { name: "People" }).click(); await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1); await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(2);
await primaryFilters.getByRole("option", { name: "Rooms" }).click(); await primaryFilters.getByRole("option", { name: "Rooms" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(3); await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(5);
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Invites" }).click();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
}); });
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => { test(
"unread filter should only match unread rooms that have a count",
{ tag: "@screenshot" },
async ({ page, app, bot }) => {
const roomListView = getRoomList(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("gridcell", { 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 page.getByRole("option", { name: "Unread" }).click();
// Unread filter should only show unread room and not unread dm!
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
await expect(unreadDm).toBeVisible();
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
},
);
test("should sort the room list alphabetically", async ({ page }) => {
const roomListView = getRoomList(page); const roomListView = getRoomList(page);
// Let's configure unread dm room so that we only get notification for mentions and keywords await getRoomOptionsMenu(page).click();
await app.viewRoomById(unReadDmId); await page.getByRole("menuitemradio", { name: "A-Z" }).click();
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 expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click(); });
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
const roomListView = getRoomList(page);
// Let's make the bot send a new message in both rooms
await bot.sendMessage(unReadDmId, "Hello!"); await bot.sendMessage(unReadDmId, "Hello!");
await bot.sendMessage(unReadRoomId, "Hello!");
// Let's activate the unread filter now await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
await page.getByRole("option", { name: "Unread" }).click();
// Unread filter should only show unread room and not unread dm!
await expect(roomListView.getByRole("gridcell", { name: "Open room unread room" })).toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
}); });
}); });
@@ -184,15 +316,25 @@ test.describe("Room list filters and sort", () => {
}, },
); );
test("should render the placeholder for unread filter", { tag: "@screenshot" }, async ({ page, app, user }) => { [
const primaryFilters = getPrimaryFilters(page); { filter: "Unreads", action: "Show all chats" },
await primaryFilters.getByRole("option", { name: "Unread" }).click(); { 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 primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page); const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png"); await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
await emptyRoomList.getByRole("button", { name: "show all chats" }).click(); await emptyRoomList.getByRole("button", { name: action }).click();
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked(); await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked();
},
);
}); });
["People", "Rooms", "Favourite"].forEach((filter) => { ["People", "Rooms", "Favourite"].forEach((filter) => {

View File

@@ -30,6 +30,9 @@ test.describe("Room list panel", () => {
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
await app.client.createRoom({ name: `room${i}` }); await app.client.createRoom({ name: `room${i}` });
} }
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
}); });
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => { test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
@@ -38,4 +41,10 @@ test.describe("Room list panel", () => {
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible(); await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-panel.png"); await expect(roomListView).toMatchScreenshot("room-list-panel.png");
}); });
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => {
await page.setViewportSize({ width: 575, height: 600 });
const roomListPanel = page.getByTestId("room-list-panel");
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");
});
}); });

View File

@@ -29,6 +29,9 @@ test.describe("Room list", () => {
test.beforeEach(async ({ page, app, user }) => { test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section // The notification toast is displayed above the search section
await app.closeNotificationToast(); await app.closeNotificationToast();
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
}); });
test.describe("Room list", () => { test.describe("Room list", () => {
@@ -43,7 +46,8 @@ test.describe("Room list", () => {
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible(); await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png"); await expect(roomListView).toMatchScreenshot("room-list.png");
await roomListView.hover(); // Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list // Scroll to the end of the room list
await page.mouse.wheel(0, 1000); await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
@@ -105,13 +109,10 @@ test.describe("Room list", () => {
// It should make the room muted // It should make the room muted
await page.getByRole("menuitem", { name: "Mute room" }).click(); await page.getByRole("menuitem", { name: "Mute room" }).click();
// Remove hover on the room list item // Put focus on the room list
await roomListView.hover(); await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
// Scroll to the end of the room list
// Scroll to the bottom of the list await page.mouse.wheel(0, 1000);
await page.getByRole("grid", { name: "Room list" }).evaluate((e) => {
e.scrollTop = e.scrollHeight;
});
// The room decoration should have the muted icon // The room decoration should have the muted icon
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
@@ -129,7 +130,8 @@ test.describe("Room list", () => {
test("should scroll to the current room", async ({ page, app, user }) => { test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page); const roomListView = getRoomList(page);
await roomListView.hover(); // Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list // Scroll to the end of the room list
await page.mouse.wheel(0, 1000); await page.mouse.wheel(0, 1000);
@@ -142,6 +144,98 @@ test.describe("Room list", () => {
await filters.getByRole("option", { name: "People" }).click(); await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
}); });
test.describe("Shortcuts", () => {
test("should select the next room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await page.keyboard.press("Alt+ArrowDown");
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
});
test("should select the previous room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
await page.keyboard.press("Alt+ArrowUp");
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
test("should select the last room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await page.keyboard.press("Alt+ArrowUp");
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
});
test("should select the next unread room", async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
const roomId = await app.client.createRoom({ name: "1 notification" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
await page.keyboard.press("Alt+Shift+ArrowDown");
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
});
});
test.describe("Keyboard navigation", () => {
test("should navigate to the room list", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
// open the room
await room29.click();
// put focus back on the room list item
await room29.click();
await expect(room29).toBeFocused();
await page.keyboard.press("ArrowDown");
await expect(room28).toBeFocused();
await expect(room29).not.toBeFocused();
await page.keyboard.press("ArrowUp");
await expect(room29).toBeFocused();
await expect(room28).not.toBeFocused();
});
test("should navigate to the notification menu", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
const moreButton = room29.getByRole("button", { name: "More options" });
const notificationButton = room29.getByRole("button", { name: "Notification options" });
await room29.click();
// put focus back on the room list item
await room29.click();
await page.keyboard.press("Tab");
await expect(moreButton).toBeFocused();
await page.keyboard.press("Tab");
await expect(notificationButton).toBeFocused();
// Open the menu
await notificationButton.click();
// Wait for the menu to be open
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
"aria-selected",
"true",
);
// Close the menu
await page.keyboard.press("Escape");
// Focus should be back on the room list item
await expect(room29).toBeFocused();
});
});
}); });
test.describe("Avatar decoration", () => { test.describe("Avatar decoration", () => {
@@ -150,6 +244,10 @@ test.describe("Room list", () => {
test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => { test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
// @ts-ignore Visibility enum is not accessible // @ts-ignore Visibility enum is not accessible
await app.client.createRoom({ name: "public room", visibility: "public" }); await app.client.createRoom({ name: "public room", visibility: "public" });
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
const roomListView = getRoomList(page); const roomListView = getRoomList(page);
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" }); const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
@@ -165,6 +263,10 @@ test.describe("Room list", () => {
const roomListView = getRoomList(page); const roomListView = getRoomList(page);
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" }); const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
await expect(videoRoom).toBeVisible(); await expect(videoRoom).toBeVisible();
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png"); await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
}); });
@@ -230,6 +332,26 @@ test.describe("Room list", () => {
await expect(room).toMatchScreenshot("room-list-item-mention.png"); await expect(room).toMatchScreenshot("room-list-item-mention.png");
}); });
test("should render a message preview", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
await page.getByRole("button", { name: "Room Options" }).click();
await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click();
const roomId = await app.client.createRoom({ name: "activity" });
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("gridcell", { name: "activity" });
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
});
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page); const roomListView = getRoomList(page);
@@ -269,8 +391,8 @@ test.describe("Room list", () => {
await room.getByRole("button", { name: "More Options" }).click(); await room.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "mark as unread" }).click(); await page.getByRole("menuitem", { name: "mark as unread" }).click();
// Remove hover on the room list item // focus the user menu to avoid to have hover decoration
await roomListView.hover(); await page.getByRole("button", { name: "User menu" }).focus();
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png"); await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
}); });

View File

@@ -288,6 +288,43 @@ test.describe("Login", () => {
await expect(h1).toBeVisible(); await expect(h1).toBeVisible();
}); });
}); });
test("Can reset identity to become verified", async ({ page, homeserver, request, credentials }) => {
// Log in
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
});
if (!res.ok()) {
console.log(`Uploading dummy keys failed with HTTP status ${res.status}`, await res.json());
throw new Error("Uploading dummy keys failed");
}
await page.goto("/");
await login(page, homeserver, credentials);
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
// Start the reset process
await page.getByRole("button", { name: "Proceed with reset" }).click();
// First try cancelling and restarting
await page.getByRole("button", { name: "Cancel" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
// Then click outside the dialog and restart
await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true });
await page.getByRole("button", { name: "Proceed with reset" }).click();
// Finally we actually continue
await page.getByRole("button", { name: "Continue" }).click();
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Continue" }).click();
// We end up at the Home screen
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
await expect(page.getByRole("heading", { name: "Welcome Dave", exact: true })).toBeVisible();
});
}); });
}); });

View File

@@ -15,6 +15,7 @@ test.describe("Module loading", () => {
test.describe("Example Module", () => { test.describe("Example Module", () => {
test.use({ test.use({
config: { config: {
brand: "TestBrand",
modules: ["/modules/example-module.js"], modules: ["/modules/example-module.js"],
}, },
page: async ({ page }, use) => { page: async ({ page }, use) => {
@@ -25,11 +26,31 @@ test.describe("Module loading", () => {
}, },
}); });
test("should show alert", async ({ page }) => { const testCases = [
const dialogPromise = page.waitForEvent("dialog"); ["en", "TestBrand module loading successful!"],
await page.goto("/"); ["de", "TestBrand-Module erfolgreich geladen!"],
const dialog = await dialogPromise; ];
expect(dialog.message()).toBe("Testing module loading successful!");
}); for (const [lang, message] of testCases) {
test.describe(`language-${lang}`, () => {
test.use({
config: async ({ config }, use) => {
await use({
...config,
setting_defaults: {
language: lang,
},
});
},
});
test("should show alert", async ({ page }) => {
const dialogPromise = page.waitForEvent("dialog");
await page.goto("/");
const dialog = await dialogPromise;
expect(dialog.message()).toBe(message);
});
});
}
}); });
}); });

View File

@@ -37,6 +37,8 @@ export async function registerAccountMas(
await page.getByRole("textbox", { name: "6-digit code" }).fill(code); await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("textbox", { name: "Display Name" }).fill(username);
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Allow access to your account?")).toBeVisible(); await expect(page.getByText("Allow access to your account?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
} }

View File

@@ -88,7 +88,6 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await expect(page.getByText("Welcome")).toBeVisible(); await expect(page.getByText("Welcome")).toBeVisible();
await page.goto("about:blank"); await page.goto("about:blank");
// @ts-expect-error
const result = await mas.manage("kill-sessions", userId); const result = await mas.manage("kill-sessions", userId);
expect(result.output).toContain("Ended 1 active OAuth 2.0 session"); expect(result.output).toContain("Ended 1 active OAuth 2.0 session");

View File

@@ -15,20 +15,34 @@ test.describe("Release announcement", () => {
feature_release_announcement: true, feature_release_announcement: true,
}, },
}, },
labsFlags: ["threadsActivityCentre"], room: async ({ app, user }, use) => {
const roomId = await app.client.createRoom({
name: "Test room",
});
await app.viewRoomById(roomId);
await use({ roomId });
},
}); });
test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => { test(
// The TAC release announcement should be displayed "should display the pinned messages release announcement",
await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); { tag: "@screenshot" },
// Hide the release announcement async ({ page, app, room, util }) => {
await util.markReleaseAnnouncementAsRead("Threads Activity Centre"); await app.toggleRoomInfoPanel();
await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre");
await page.reload(); const name = "All new pinned messages";
// Wait for EW to load
await expect(page.getByRole("navigation", { name: "Spaces" })).toBeVisible(); // The release announcement should be displayed
// Check that once the release announcement has been marked as viewed, it does not appear again await util.assertReleaseAnnouncementIsVisible(name);
await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre"); // Hide the release announcement
}); await util.markReleaseAnnouncementAsRead(name);
await util.assertReleaseAnnouncementIsNotVisible(name);
await page.reload();
await app.toggleRoomInfoPanel();
await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible();
// Check that once the release announcement has been marked as viewed, it does not appear again
await util.assertReleaseAnnouncementIsNotVisible(name);
},
);
}); });

View File

@@ -11,6 +11,7 @@ import { type Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils"; import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { Bot } from "../../pages/bot";
const ROOM_NAME = "Test room"; const ROOM_NAME = "Test room";
const ROOM_NAME_LONG = const ROOM_NAME_LONG =
@@ -21,20 +22,23 @@ const ROOM_NAME_LONG =
"officia deserunt mollit anim id est laborum."; "officia deserunt mollit anim id est laborum.";
const SPACE_NAME = "Test space"; const SPACE_NAME = "Test space";
const NAME = "Alice"; const NAME = "Alice";
const LONG_NAME = "Bob long long long long long long long long long long long long long long long name";
const ROOM_ADDRESS_LONG = const ROOM_ADDRESS_LONG =
"loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua"; "loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua";
function getMemberTileByName(page: Page, name: string): Locator { function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_MemberTileView, [title="${name}"]`); return page.locator(".mx_MemberListView .mx_MemberTileView_name").filter({ hasText: name });
} }
test.describe("RightPanel", () => { test.describe("RightPanel", () => {
let testRoomId: string;
test.use({ test.use({
displayName: NAME, displayName: NAME,
}); });
test.beforeEach(async ({ app, user }) => { test.beforeEach(async ({ app, user }) => {
await app.client.createRoom({ name: ROOM_NAME }); testRoomId = await app.client.createRoom({ name: ROOM_NAME });
await app.client.createSpace({ name: SPACE_NAME }); await app.client.createSpace({ name: SPACE_NAME });
}); });
@@ -77,10 +81,12 @@ test.describe("RightPanel", () => {
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png"); await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png");
}); });
test("should handle clicking add widgets", async ({ page, app }) => { test("should handle clicking add widgets", { tag: "@screenshot" }, async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME); await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Extensions" }).click(); await page.getByRole("menuitem", { name: "Extensions" }).click();
await expect(page.getByTestId("right-panel")).toMatchScreenshot("with-extensions.png");
await page.getByRole("button", { name: "Add extensions" }).click(); await page.getByRole("button", { name: "Add extensions" }).click();
await expect(page.locator(".mx_IntegrationManager")).toBeVisible(); await expect(page.locator(".mx_IntegrationManager")).toBeVisible();
}); });
@@ -134,6 +140,37 @@ test.describe("RightPanel", () => {
await page.getByLabel("Room info").nth(1).click(); await page.getByLabel("Room info").nth(1).click();
await checkRoomSummaryCard(page, ROOM_NAME); await checkRoomSummaryCard(page, ROOM_NAME);
}); });
test(
"should handle viewing long room member name",
{ tag: "@screenshot" },
async ({ page, homeserver, app }) => {
const bobLongName = new Bot(page, homeserver, { displayName: LONG_NAME });
await bobLongName.prepareClient();
await app.client.inviteUser(testRoomId, bobLongName.credentials.userId);
await bobLongName.joinRoom(testRoomId);
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
await expect(page.locator(".mx_MemberListView")).toBeVisible();
await getMemberTileByName(page, LONG_NAME).click();
await expect(page.locator(".mx_UserInfo")).toBeVisible();
await expect(page.locator(".mx_UserInfo_profile").getByText(LONG_NAME)).toBeVisible();
await expect(page.locator(".mx_UserInfo")).toMatchScreenshot("with-long-name.png", {
mask: [page.locator(".mx_UserInfo_profile_mxid")],
css: `
/* Use monospace font for consistent mask width */
.mx_UserInfo_profile_mxid {
font-family: Inconsolata !important;
}
`,
});
},
);
test.describe("room reporting", () => { test.describe("room reporting", () => {
test.skip(isDendrite, "Dendrite does not implement room reporting"); test.skip(isDendrite, "Dendrite does not implement room reporting");
test("should handle reporting a room", { tag: "@screenshot" }, async ({ page, app }) => { test("should handle reporting a room", { tag: "@screenshot" }, async ({ page, app }) => {

View File

@@ -19,126 +19,162 @@ import {
test.describe("Encryption tab", () => { test.describe("Encryption tab", () => {
test.use({ displayName: "Alice" }); test.use({ displayName: "Alice" });
let recoveryKey: GeneratedSecretStorageKey; test.describe("when encryption is set up", () => {
let expectedBackupVersion: string; let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => { test.beforeEach(async ({ page, homeserver, credentials }) => {
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key // The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
const res = await createBot(page, homeserver, credentials); const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey; recoveryKey = res.recoveryKey;
expectedBackupVersion = res.expectedBackupVersion; expectedBackupVersion = res.expectedBackupVersion;
}); });
test( test(
"should show a 'Verify this device' button if the device is unverified", "should show a 'Verify this device' button if the device is unverified",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, app, util }) => { async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab(); const dialog = await util.openEncryptionTab();
const content = util.getEncryptionTabContent(); const content = util.getEncryptionTabContent();
// The user's device is in an unverified state, therefore the only option available to them here is to verify it // The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" }); const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible(); await expect(verifyButton).toBeVisible();
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png"); await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click(); await verifyButton.click();
await util.verifyDevice(recoveryKey); await util.verifyDevice(recoveryKey);
await expect(content).toMatchScreenshot("default-tab.png", { await expect(content).toMatchScreenshot("default-tab.png", {
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")], mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
}); });
// 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(app, expectedBackupVersion, true);
}, },
); );
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB. // Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
// //
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. // This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
// We simulate this case by deleting the cached secrets in the indexedDB. // We simulate this case by deleting the cached secrets in the indexedDB.
test( test(
"should prompt to enter the recovery key when the secrets are not cached locally", "should prompt to enter the recovery key when the secrets are not cached locally",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, app, util }) => { async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets
await deleteCachedSecrets(page);
await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-tab.png", {
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
});
// 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(app, expectedBackupVersion, true);
},
);
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
page,
app,
util,
}) => {
await verifySession(app, recoveryKey.encodedPrivateKey); await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets // We need to delete the cached secrets
await deleteCachedSecrets(page); await deleteCachedSecrets(page);
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
await util.openEncryptionTab(); await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent(); const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" }); await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
// Fill the recovery key // The user is prompted to reset their identity
await util.enterRecoveryKey(recoveryKey); await expect(
await expect(dialog).toMatchScreenshot("default-tab.png", { dialog.getByText("Forgot your recovery key? Youll need to reset your identity."),
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")], ).toBeVisible();
}); });
// Check that our device is now cross-signed test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await checkDeviceIsCrossSigned(app); await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
// Check that the current device is connected to key backup await page.getByRole("checkbox", { name: "Allow key storage" }).click();
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
},
);
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({ await expect(
page, page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
app, ).toBeVisible();
util,
}) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets
await deleteCachedSecrets(page);
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
await util.openEncryptionTab();
const dialog = util.getEncryptionTabContent();
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
// The user is prompted to reset their identity const deleteRequestPromises = [
await expect(dialog.getByText("Forgot your recovery key? Youll need to reset your identity.")).toBeVisible(); page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
];
await page.getByRole("button", { name: "Delete key storage" }).click();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
for (const prom of deleteRequestPromises) {
const request = await prom;
expect(request.method()).toBe("PUT");
expect(request.postData()).toBe(JSON.stringify({}));
}
});
}); });
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => { test.describe("when encryption is not set up", () => {
await verifySession(app, recoveryKey.encodedPrivateKey); test("'Verify this device' allows us to become verified", async ({
await util.openEncryptionTab(); page,
user,
credentials,
app,
}, workerInfo) => {
const settings = await app.settings.openUserSettings("Encryption");
await page.getByRole("checkbox", { name: "Allow key storage" }).click(); // Initially, our device is not verified
await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible();
await expect( // We will reset our identity
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }), await settings.getByRole("button", { name: "Verify this device" }).click();
).toBeVisible(); await page.getByRole("button", { name: "Proceed with reset" }).click();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png"); // First try cancelling and restarting
await page.getByRole("button", { name: "Cancel" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
const deleteRequestPromises = [ // Then click outside the dialog and restart
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")), await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true });
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")), await page.getByRole("button", { name: "Proceed with reset" }).click();
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
];
await page.getByRole("button", { name: "Delete key storage" }).click(); // Finally we actually continue
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked(); // Now we are verified, so we see the Key storage toggle
await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible();
for (const prom of deleteRequestPromises) { });
const request = await prom;
expect(request.method()).toBe("PUT");
expect(request.postData()).toBe(JSON.stringify({}));
}
}); });
}); });

View File

@@ -0,0 +1,34 @@
/*
* 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("share from URL", () => {
test.use({
displayName: "Alice",
room: async ({ app }, use) => {
const roomId = await app.client.createRoom({ name: "A test room" });
await use({ roomId });
},
});
test("should share message when users navigates to share URL", async ({ page, user, room, app }) => {
await page.goto("/#/share?msg=Hello+world");
// The forward message dialog doesn't update as new infomation arrives via sync, which means sometimes
// this is just says, "Empty room". For the same reason, we can't reliably write a test for loading the
// app straight away with a /#/share url as the room doesn't appear until the client syncs.]
// Ideally we should fix the forward dialog to update and eliminate races, until then, there is only one
// room so we click the first button.
await page.getByRole("listitem" /*, { name: "A test room" }*/).getByRole("button", { name: "Send" }).click();
await page.keyboard.press("Escape");
await app.viewRoomByName("A test room");
const lastMessage = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
await expect(lastMessage).toBeVisible();
const lastMessageText = await lastMessage.locator(".mx_EventTile_body").innerText();
await expect(lastMessageText).toBe("Hello world");
});
});

View File

@@ -19,7 +19,6 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
test.use({ test.use({
displayName: "Alice", displayName: "Alice",
botCreateOpts: { displayName: "Other User" }, botCreateOpts: { displayName: "Other User" },
labsFlags: ["threadsActivityCentre"],
}); });
test( test(

View File

@@ -0,0 +1,139 @@
/*
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 * as fs from "node:fs";
import { type EventType, type MsgType, type RoomJoinRulesEventContent } from "matrix-js-sdk/src/types";
import { test, expect } from "../../element-web-test";
const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png");
test.describe("Media preview settings", () => {
test.use({
displayName: "Alan",
botCreateOpts: {
displayName: "Bob",
},
room: async ({ app, page, homeserver, bot, user }, use) => {
const mxc = (await bot.uploadContent(MEDIA_FILE, { name: "image.png", type: "image/png" })).content_uri;
const roomId = await bot.createRoom({
name: "Test room",
invite: [user.userId],
initial_state: [{ type: "m.room.avatar", content: { url: mxc }, state_key: "" }],
});
await bot.sendEvent(roomId, null, "m.room.message" as EventType, {
msgtype: "m.image" as MsgType,
body: "image.png",
url: mxc,
});
await use({ roomId });
},
});
test("should be able to hide avatars of inviters", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
let settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Hide avatars of room and inviter").click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await expect(
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
).toMatchScreenshot("invite-no-avatar.png", {
// Hide the mxid, which is not stable.
css: `
.mx_RoomPreviewBar_inviter_mxid {
display: none !important;
}
`,
});
await expect(
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
).toMatchScreenshot("invite-room-tree-no-avatar.png");
// And then go back to being visible
settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Hide avatars of room and inviter").click();
await app.closeDialog();
await page.goto("#/home");
await app.viewRoomById(room.roomId);
await expect(
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
).toMatchScreenshot("invite-with-avatar.png", {
// Hide the mxid, which is not stable.
css: `
.mx_RoomPreviewBar_inviter_mxid {
display: none !important;
}
`,
});
await expect(
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
).toMatchScreenshot("invite-room-tree-with-avatar.png");
});
test("should be able to hide media in rooms globally", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
await expect(page.getByText("Show image")).toBeVisible();
});
test("should be able to hide media in non-private rooms globally", async ({ page, app, room, user, bot }) => {
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
join_rule: "public",
});
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByLabel("In private rooms").click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
await expect(page.getByText("Show image")).toBeVisible();
for (const joinRule of ["invite", "knock", "restricted"] as RoomJoinRulesEventContent["join_rule"][]) {
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
join_rule: joinRule,
} satisfies RoomJoinRulesEventContent);
await expect(page.getByText("Show image")).not.toBeVisible();
}
});
test("should be able to show media in rooms globally", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
await expect(page.getByText("Show image")).not.toBeVisible();
});
test("should be able to hide media in an individual room", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
const roomSettings = await app.settings.openRoomSettings("General");
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
await app.closeDialog();
await expect(page.getByText("Show image")).toBeVisible();
});
test("should be able to show media in an individual room", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
const roomSettings = await app.settings.openRoomSettings("General");
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
await app.closeDialog();
await expect(page.getByText("Show image")).not.toBeVisible();
});
});

View File

@@ -6,11 +6,19 @@ Please see LICENSE files in the repository root for full details.
*/ */
export default class ExampleModule { export default class ExampleModule {
static moduleApiVersion = "^0.1.0"; static moduleApiVersion = "^1.0.0";
constructor(api) { constructor(api) {
this.api = api; this.api = api;
this.api.i18n.register({
key: {
en: "%(brand)s module loading successful!",
de: "%(brand)s-Module erfolgreich geladen!",
},
});
} }
async load() { async load() {
alert("Testing module loading successful!"); const brand = this.api.config.get("brand");
alert(this.api.i18n.translate("key", { brand }));
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 KiB

After

Width:  |  Height:  |  Size: 956 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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