diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5a762fe224..4d173ab222 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,12 +4,13 @@ /pnpm-lock.yaml @element-hq/element-web-team /apps/web/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers -/apps/web/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers /apps/web/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/async-components/dialogs/security/ @element-hq/element-crypto-web-reviewers /apps/web/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers -/apps/web/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers /apps/web/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers -/apps/web/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers /apps/web/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers /apps/web/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers /apps/web/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml index a64bc3241b..7a7265a2ae 100644 --- a/.github/actions/download-verify-element-tarball/action.yml +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -11,7 +11,7 @@ runs: using: composite steps: - name: Download release tarball - uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1 + uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1 with: tag: ${{ inputs.tag }} fileName: element-*.tar.gz* @@ -31,7 +31,9 @@ runs: - name: Move webapp to out-file-path shell: bash - run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }} + run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp "$OUT_PATH" + env: + OUT_PATH: ${{ inputs.out-file-path }} - name: Clean up temp directory shell: bash diff --git a/.github/actions/setup-playwright/action.yml b/.github/actions/setup-playwright/action.yml new file mode 100644 index 0000000000..1276191d2b --- /dev/null +++ b/.github/actions/setup-playwright/action.yml @@ -0,0 +1,49 @@ +name: Setup playwright +description: Installs playwright browsers and sets up a cache +inputs: + needs-webkit: + description: Whether to install the additional dependencies for webkit + required: false + default: "false" + write-cache: + description: Whether to write the cache back + required: true +runs: + using: composite + steps: + - name: Calculate cache key + id: key + run: | + PW_VERSION=$(pnpm --silent -- playwright --version | awk '{print $2}') + echo "key=${PREFIX}-playwright-${PW_VERSION}" >> $GITHUB_OUTPUT + shell: bash + env: + PREFIX: ${{ runner.os }}-${{ runner.arch }} + + - name: Cache playwright binaries + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + if: inputs.write-cache == 'true' + id: cache + with: + path: ~/.cache/ms-playwright + key: ${{ steps.key.outputs.key }} + + # When running in merge queue only restore the cache, never write it + - name: Restore playwright binaries cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + if: inputs.write-cache != 'true' + id: cache-restore + with: + path: ~/.cache/ms-playwright + key: ${{ steps.key.outputs.key }} + + - name: Install Playwright browsers + if: (steps.cache.outputs.cache-hit || steps.cache-restore.outputs.cache-hit) != 'true' + shell: bash + run: pnpm playwright install --with-deps + + # Some WebKit dependencies seem to lay outside the cache and will need to be installed separately + - name: Install system dependencies for WebKit + if: inputs.needs-webkit == 'true' && (steps.cache.outputs.cache-hit || steps.cache-restore.outputs.cache-hit) == 'true' + shell: bash + run: pnpm playwright install-deps webkit diff --git a/.github/renovate.json b/.github/renovate.json index ded20f15d7..928cfd4ad0 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,22 +2,45 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>matrix-org/renovate-config-element-web"], "postUpdateOptions": ["pnpmDedupe"], + "stopUpdatingLabel": "X-Blocked", "packageRules": [ { + "description": "Group all testcontainers docker digests", "groupName": "testcontainers docker digests", - "groupSlug": "{{manager}}-docker-digests", - "matchManagers": ["custom.regex"], + "groupSlug": "testcontainers-docker", + "matchDepTypes": ["testcontainers-docker"], "matchPackageNames": ["*"] + }, + { + "description": "Separate updates to overrides from other groups", + "matchDepTypes": ["pnpm.overrides"], + "groupSlug": null + }, + { + "description": "Disable any major updates to overrides as this almost always is wrong", + "matchDepTypes": ["pnpm.overrides"], + "matchUpdateTypes": ["major"], + "enabled": false } ], "customManagers": [ { + "description": "Update testcontainers docker digests", "customType": "regex", "datasourceTemplate": "docker", "versioningTemplate": "loose", - "description": "Update testcontainers docker digests", "managerFilePatterns": ["**/testcontainers/*.ts"], - "matchStrings": ["\\s+\"(?[^@]+):(?[^@]+)@(?sha256:[a-f0-9]+)\""] + "matchStrings": ["\\s+\"(?[^@]+):(?[^@]+)@(?sha256:[a-f0-9]+)\""], + "depTypeTemplate": "testcontainers-docker" + }, + { + "description": "Update element-desktop hakDependencies", + "customType": "jsonata", + "managerFilePatterns": ["/(^|/)package\\.json$/"], + "fileFormat": "json", + "matchStrings": ["hakDependencies.$each(function($v, $k) { { 'packageName': $k, 'currentValue': $v } })"], + "datasourceTemplate": "npm", + "depTypeTemplate": "hak" } ] } diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index e13c537d0e..c1ecb609f8 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -18,8 +18,6 @@ on: push: # We do not build on push to develop as the merge_group check handles that branches: [staging, master] - repository_dispatch: - types: [element-web-notify] # support triggering from other workflows workflow_call: @@ -56,6 +54,8 @@ jobs: outputs: num-runners: ${{ env.NUM_RUNNERS }} runners-matrix: ${{ steps.runner-vars.outputs.matrix }} + # Skip pull_request runs on renovate PRs to speed up CI time, delegating to the full run in merge queue + skip: ${{ inputs.skip || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -64,7 +64,7 @@ jobs: persist-credentials: false - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" node-version: "lts/*" @@ -86,7 +86,7 @@ jobs: run: VERSION=$(scripts/get-version-from-git.sh) pnpm run build - name: Upload Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: webapp path: apps/web/webapp @@ -94,7 +94,7 @@ jobs: - name: Calculate runner variables id: runner-vars - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const numRunners = parseInt(process.env.NUM_RUNNERS, 10); @@ -104,7 +104,7 @@ jobs: playwright_ew: name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build_ew.outputs.num-runners }}" needs: build_ew - if: inputs.skip != true + if: needs.build_ew.outputs.skip == 'false' runs-on: ubuntu-24.04 permissions: actions: read @@ -146,7 +146,7 @@ jobs: path: apps/web/webapp - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" cache-dependency-path: pnpm-lock.yaml @@ -155,33 +155,17 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Get installed Playwright version - id: playwright - run: echo "version=$(pnpm --silent -- playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT - - - name: Cache playwright binaries - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 - id: playwright-cache + - name: Setup playwright + uses: ./.github/actions/setup-playwright with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }} - - - name: Install Playwright browsers - if: steps.playwright-cache.outputs.cache-hit != 'true' - working-directory: apps/web - run: pnpm playwright install --with-deps --no-shell - - - name: Install system dependencies for WebKit - # Some WebKit dependencies seem to lay outside the cache and will need to be installed separately - if: matrix.project == 'WebKit' && steps.playwright-cache.outputs.cache-hit == 'true' - working-directory: apps/web - run: pnpm playwright install-deps webkit + needs-webkit: ${{ matrix.project == 'WebKit' }} + write-cache: ${{ github.event_name != 'merge_group' }} # We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else - name: Run Playwright tests working-directory: apps/web run: | - pnpm playwright test \ + pnpm test:playwright \ --shard "$SHARD" \ --project="${{ matrix.project }}" \ ${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }} @@ -190,7 +174,7 @@ jobs: - name: Upload blob report to GitHub Actions Artifacts if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: blob-report-${{ matrix.project }}-${{ matrix.runner }} path: apps/web/blob-report @@ -200,28 +184,30 @@ jobs: downstream-modules: name: Downstream Playwright tests [element-modules] needs: build_ew - if: inputs.skip != true && github.event_name == 'merge_group' + if: needs.build_ew.outputs.skip == 'false' && github.event_name == 'merge_group' uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main # zizmor: ignore[unpinned-uses] with: webapp-artifact: webapp + reporter: blob prepare_ed: name: "Prepare Element Desktop" uses: ./.github/workflows/build_desktop_prepare.yaml needs: build_ew - if: inputs.skip != true + if: needs.build_ew.outputs.skip == 'false' permissions: contents: read with: config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }} - version: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'develop' || '' }} + version: ${{ case((github.event.pull_request.base.ref || github.ref_name) == 'develop' || github.event_name == 'merge_group', 'develop', '') }} webapp-artifact: webapp build_ed_windows: needs: prepare_ed name: "Desktop Windows" uses: ./.github/workflows/build_desktop_windows.yaml - if: inputs.skip != true + # Skip Windows builds on PRs, as the Linux amd64 build is enough of a smoke test and includes the screenshot tests + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') strategy: matrix: arch: [x64, ia32, arm64] @@ -233,17 +219,19 @@ jobs: needs: prepare_ed name: "Desktop Linux" uses: ./.github/workflows/build_desktop_linux.yaml - if: inputs.skip != true strategy: matrix: sqlcipher: [system, static] arch: [amd64, arm64] runAllTests: - ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }} - # We ship static sqlcipher builds, so delegate testing the system builds to the merge queue exclude: + # We ship static sqlcipher builds, so delegate testing the system builds to the merge queue - runAllTests: false sqlcipher: system + # Additionally skip arm64 system builds on PRs, as the amd64 test is enough for a smoke test and includes the screenshot tests + - runAllTests: false + arch: arm64 with: sqlcipher: ${{ matrix.sqlcipher }} arch: ${{ matrix.arch }} @@ -253,15 +241,19 @@ jobs: needs: prepare_ed name: "Desktop macOS" uses: ./.github/workflows/build_desktop_macos.yaml - if: inputs.skip != true + # Skip macOS builds on PRs, as the Linux amd64 build is enough of a smoke test and includes the screenshot tests + # and we have a very low limit of concurrent macos runners (5) across the Github org. + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') with: blob_report: true complete: name: end-to-end-tests needs: + - build_ew - playwright_ew - downstream-modules + - prepare_ed - build_ed_windows - build_ed_linux - build_ed_macos @@ -269,25 +261,25 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - if: inputs.skip != true + if: needs.build_ew.outputs.skip == 'false' with: persist-credentials: false repository: element-hq/element-web - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - if: inputs.skip != true - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - if: inputs.skip != true + if: needs.build_ew.outputs.skip == 'false' + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + if: needs.build_ew.outputs.skip == 'false' with: cache: "pnpm" node-version: "lts/*" - name: Install dependencies - if: inputs.skip != true + if: needs.build_ew.outputs.skip == 'false' run: pnpm install --frozen-lockfile - name: Download blob reports from GitHub Actions Artifacts - if: inputs.skip != true + if: needs.build_ew.outputs.skip == 'false' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: blob-report-* @@ -295,7 +287,7 @@ jobs: merge-multiple: true - name: Merge into HTML Report - if: inputs.skip != true + if: needs.build_ew.outputs.skip == 'false' run: | pnpm playwright merge-reports \ --config=playwright-merge.config.ts \ @@ -307,8 +299,8 @@ jobs: # Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected - name: Upload HTML report - if: always() && inputs.skip != true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + if: always() && needs.build_ew.outputs.skip == 'false' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: html-report path: playwright-report diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a78e4f25f8..5f93a94516 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: persist-credentials: false - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: # Disable cache on Windows as it is slower than not caching # https://github.com/actions/setup-node/issues/975 @@ -69,7 +69,7 @@ jobs: run: VERSION=$(scripts/get-version-from-git.sh) pnpm run build - name: Upload Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: webapp-${{ matrix.image }} path: apps/web/webapp diff --git a/.github/workflows/build_debian.yaml b/.github/workflows/build_debian.yaml index c924bb8949..24fe492e7c 100644 --- a/.github/workflows/build_debian.yaml +++ b/.github/workflows/build_debian.yaml @@ -69,7 +69,7 @@ jobs: dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: element-web.deb path: apps/web/element-web.deb diff --git a/.github/workflows/build_desktop_and_deploy.yaml b/.github/workflows/build_desktop_and_deploy.yaml index 0455b7e37c..5611b2d4fd 100644 --- a/.github/workflows/build_desktop_and_deploy.yaml +++ b/.github/workflows/build_desktop_and_deploy.yaml @@ -212,7 +212,7 @@ jobs: - name: Stash packages.element.io if: needs.prepare.outputs.deploy == 'false' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: packages.element.io path: packages.element.io @@ -250,7 +250,7 @@ jobs: - name: Stash debs if: needs.prepare.outputs.deploy == 'false' && needs.linux.result == 'success' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: debs path: | @@ -289,7 +289,7 @@ jobs: id-token: write # This is required for requesting the JWT steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6 with: role-to-assume: arn:aws:iam::264135176173:role/Push-ElementDesktop-MSI role-session-name: githubaction-run-${{ github.run_id }} diff --git a/.github/workflows/build_desktop_linux.yaml b/.github/workflows/build_desktop_linux.yaml index d045cc3d7e..436a5194ed 100644 --- a/.github/workflows/build_desktop_linux.yaml +++ b/.github/workflows/build_desktop_linux.yaml @@ -104,14 +104,14 @@ jobs: - name: Cache .hak id: cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion', 'apps/desktop/dockerbuild/*') }} path: | apps/desktop/.hak - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: apps/desktop/.node-version cache: "pnpm" @@ -126,14 +126,14 @@ jobs: - name: "Get modified files" id: changed_files if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' && github.repository == 'element-hq/element-web' - uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46 + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47 with: files: | apps/desktop/dockerbuild/** # This allows contributors to test changes to the dockerbuild image within a pull request - name: Build docker image - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 if: steps.changed_files.outputs.any_modified == 'true' with: file: apps/desktop/dockerbuild/Dockerfile @@ -216,7 +216,7 @@ jobs: # We exclude *-unpacked as it loses permissions and the tarball contains it with correct permissions - name: Upload Artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} path: | diff --git a/.github/workflows/build_desktop_macos.yaml b/.github/workflows/build_desktop_macos.yaml index bc5455b5a4..a32f674cfe 100644 --- a/.github/workflows/build_desktop_macos.yaml +++ b/.github/workflows/build_desktop_macos.yaml @@ -90,7 +90,7 @@ jobs: - name: Cache .hak id: cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: key: ${{ runner.os }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion') }} path: | @@ -114,7 +114,7 @@ jobs: - run: sudo pip3 install pyobjc-framework-Quartz - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: apps/desktop/.node-version cache: "pnpm" @@ -194,7 +194,7 @@ jobs: # We exclude mac-universal as the unpacked app takes forever to upload and zip and dmg already contains it - name: Upload Artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ inputs.artifact-prefix }}macos path: | diff --git a/.github/workflows/build_desktop_prepare.yaml b/.github/workflows/build_desktop_prepare.yaml index fe6c5a2624..2420e907cc 100644 --- a/.github/workflows/build_desktop_prepare.yaml +++ b/.github/workflows/build_desktop_prepare.yaml @@ -59,7 +59,7 @@ jobs: repository: element-hq/element-web - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: apps/desktop/.node-version cache: "pnpm" @@ -185,7 +185,7 @@ jobs: env: NIGHTLY_VERSION: ${{ steps.versions.outputs.nightly }} - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: desktop-prepare retention-days: 1 diff --git a/.github/workflows/build_desktop_test.yaml b/.github/workflows/build_desktop_test.yaml index ba347fa5b3..1ec6d7bf73 100644 --- a/.github/workflows/build_desktop_test.yaml +++ b/.github/workflows/build_desktop_test.yaml @@ -38,11 +38,11 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - repository: ${{ github.repository == 'element-hq/element-web-pro' && 'element-hq/element-web' || github.repository }} + repository: element-hq/element-web persist-credentials: false - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: apps/desktop/.node-version cache: "pnpm" @@ -97,10 +97,11 @@ jobs: PW_TAG: ${{ inputs.project }} ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }} ARGS: ${{ inputs.args }} + DEBUG: pw:browser - name: Upload blob report if: always() && inputs.blob_report - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: blob-report-${{ inputs.artifact }} path: apps/desktop/blob-report @@ -109,7 +110,7 @@ jobs: - name: Upload HTML report if: always() && inputs.blob_report == false - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ inputs.artifact }}-test path: apps/desktop/playwright-report diff --git a/.github/workflows/build_desktop_windows.yaml b/.github/workflows/build_desktop_windows.yaml index ebad3763f0..2cbaef9c8f 100644 --- a/.github/workflows/build_desktop_windows.yaml +++ b/.github/workflows/build_desktop_windows.yaml @@ -121,7 +121,7 @@ jobs: - name: Cache .hak id: cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion') }} path: | @@ -153,7 +153,7 @@ jobs: TARGET: ${{ steps.config.outputs.target }} - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: apps/desktop/.node-version cache: "pnpm" @@ -274,7 +274,7 @@ jobs: | ForEach-Object -Process {. $env:SIGNTOOL_PATH verify /pa $_.FullName; if(!$?) { throw }} - name: Upload Artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }} path: | diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml index 12821aab1a..246363b1df 100644 --- a/.github/workflows/build_develop.yml +++ b/.github/workflows/build_develop.yml @@ -33,7 +33,7 @@ jobs: persist-credentials: false - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" node-version: "lts/*" @@ -60,7 +60,7 @@ jobs: - run: mv dist/element-*.tar.gz dist/develop.tar.gz working-directory: apps/web - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: webapp path: apps/web/dist/develop.tar.gz @@ -111,7 +111,7 @@ jobs: running-workflow-name: "Build & Deploy develop.element.io" repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 10 - check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload|Netlify).)*$ + check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload|Netlify|Report).)*$ # We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier # as the expires after 24h and requires auth to download. diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index e00296abf1..46800f02f0 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: Install Cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Set up QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4 @@ -41,7 +41,7 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version-file: package.json cache: "pnpm" @@ -50,7 +50,7 @@ jobs: run: "pnpm install --frozen-lockfile" - name: Login to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index e276754640..97726eb6ef 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -26,7 +26,7 @@ jobs: persist-credentials: false - name: Install Cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 if: github.event_name != 'pull_request' - name: Set up QEMU @@ -39,7 +39,7 @@ jobs: - name: Build and load id: test-build - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 with: context: . file: apps/web/Dockerfile @@ -97,14 +97,14 @@ jobs: latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }} - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 if: github.event_name != 'pull_request' with: registry: ghcr.io @@ -140,7 +140,7 @@ jobs: services/web-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; - name: Login to oci.element.io Registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 if: github.event_name != 'pull_request' with: registry: oci-push.vpn.infra.element.io @@ -149,7 +149,7 @@ jobs: - name: Build and push id: build-and-push - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 if: github.event_name != 'pull_request' with: context: . diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bf24169461..9439989919 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 with: package_json_file: package.json - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" cache-dependency-path: pnpm-lock.yaml @@ -36,7 +36,7 @@ jobs: run: pnpm run docs:build - name: Upload artifact - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: ./docs/.vitepress/dist diff --git a/.github/workflows/issue_closed.yml b/.github/workflows/issue_closed.yml index 375c2e7184..e42d54dc65 100644 --- a/.github/workflows/issue_closed.yml +++ b/.github/workflows/issue_closed.yml @@ -10,7 +10,7 @@ jobs: name: Tidy closed issues runs-on: ubuntu-24.04 steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 id: main with: # 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 name: Close duplicate as Not Planned if: steps.main.outputs.closeAsNotPlanned with: diff --git a/.github/workflows/shared-component-publish.yaml b/.github/workflows/npm-publish.yaml similarity index 62% rename from .github/workflows/shared-component-publish.yaml rename to .github/workflows/npm-publish.yaml index c728c303d5..55a1f37ed7 100644 --- a/.github/workflows/shared-component-publish.yaml +++ b/.github/workflows/npm-publish.yaml @@ -1,6 +1,16 @@ -name: Publish shared component npm package +name: Publish npm package +run-name: Publish ${{ inputs.package }} on: - workflow_dispatch: {} + workflow_dispatch: + inputs: + package: + description: Which package to release + required: true + type: choice + options: + - playwright-common + - shared-components + - module-api concurrency: release jobs: @@ -19,7 +29,7 @@ jobs: - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: 🔧 Set up node environment - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" node-version-file: ".node-version" @@ -29,10 +39,9 @@ jobs: - name: Update npm run: npm install -g npm@latest - # Need to setup element web too as it needs the translations - - name: 🛠️ Setup EW + - name: 🛠️ Install dependencies run: pnpm install --frozen-lockfile - name: 🚀 Publish to npm - working-directory: packages/shared-components + working-directory: packages/${{ inputs.package }} run: npm publish --access public --provenance diff --git a/.github/workflows/pull_request_base_branch.yaml b/.github/workflows/pull_request_base_branch.yaml index e79c37783b..32c79071ef 100644 --- a/.github/workflows/pull_request_base_branch.yaml +++ b/.github/workflows/pull_request_base_branch.yaml @@ -8,7 +8,7 @@ jobs: name: Check PR base branch runs-on: ubuntu-24.04 steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const baseBranch = context.payload.pull_request.base.ref; diff --git a/.github/workflows/shared-component-storybook-build.yml b/.github/workflows/shared-component-storybook-build.yml index 0a19e215dd..13b16a239e 100644 --- a/.github/workflows/shared-component-storybook-build.yml +++ b/.github/workflows/shared-component-storybook-build.yml @@ -18,7 +18,7 @@ jobs: - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: 🔧 Pnpm cache - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" node-version-file: package.json @@ -31,7 +31,7 @@ jobs: working-directory: packages/shared-components run: pnpm build:storybook - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: shared-components-storybook path: packages/shared-components/storybook-static diff --git a/.github/workflows/shared-component-storybook-publish.yaml b/.github/workflows/shared-component-storybook-publish.yaml index 6193c5af74..1a365c6b6b 100644 --- a/.github/workflows/shared-component-storybook-publish.yaml +++ b/.github/workflows/shared-component-storybook-publish.yaml @@ -26,7 +26,7 @@ jobs: path: storybook-static - name: 🚀 Deploy to Cloudflare Pages - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 + uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3 with: apiToken: ${{ secrets.CF_PAGES_TOKEN }} accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }} diff --git a/.github/workflows/shared-component-visual-tests-netlify.yaml b/.github/workflows/shared-component-visual-tests-netlify.yaml index 1f9ae76826..e4b830406d 100644 --- a/.github/workflows/shared-component-visual-tests-netlify.yaml +++ b/.github/workflows/shared-component-visual-tests-netlify.yaml @@ -2,7 +2,9 @@ # It uploads the received images and diffs to netlify, printing the URLs to the console name: Upload Shared Component Visual Test Diffs on: - workflow_run: + # Privilege escalation necessary to deploy to Netlify + # 🚨 We must not execute any checked out code here. + workflow_run: # zizmor: ignore[dangerous-triggers] workflows: ["Shared Component Visual Tests"] types: - completed diff --git a/.github/workflows/shared-component-visual-tests.yaml b/.github/workflows/shared-component-visual-tests.yaml index 06b2d11b1c..f9d0e34fa8 100644 --- a/.github/workflows/shared-component-visual-tests.yaml +++ b/.github/workflows/shared-component-visual-tests.yaml @@ -27,7 +27,7 @@ jobs: repository: element-hq/element-web - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" node-version: "lts/*" @@ -36,22 +36,10 @@ jobs: working-directory: packages/shared-components run: pnpm install --frozen-lockfile - - name: Get installed Playwright version - working-directory: packages/shared-components - id: playwright - run: echo "version=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT - - - name: Cache playwright binaries - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 - id: playwright-cache + - name: Setup playwright + uses: ./.github/actions/setup-playwright with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell - - - name: Install Playwright browsers - working-directory: packages/shared-components - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: "pnpm playwright install --with-deps --only-shell" + write-cache: ${{ github.event_name != 'merge_group' }} - name: Run Visual tests working-directory: packages/shared-components @@ -65,7 +53,7 @@ jobs: - name: Upload received images & diffs if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: received-images path: packages/shared-components/__vis__/linux diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 73efd48ba3..e934f05ad1 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,6 +1,8 @@ name: SonarQube on: - workflow_run: + # Privilege escalation necessary to call upon SonarCloud + # 🚨 We must not execute any checked out code here. + workflow_run: # zizmor: ignore[dangerous-triggers] workflows: ["Tests"] types: - completed diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index f3052ff373..895ea83dc2 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -5,8 +5,6 @@ on: branches: [develop, master] merge_group: types: [checks_requested] - repository_dispatch: - types: [element-web-notify] concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true @@ -56,7 +54,7 @@ jobs: persist-credentials: false - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 if: matrix.install != '' with: cache: "pnpm" @@ -89,7 +87,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 i18n: strategy: @@ -105,7 +103,7 @@ jobs: voip|element_call error|invalid_json error|misconfigured - welcome_to_element + welcome|title_element devtools|settings|elementCallUrl labs|sliding_sync_description settings|voip|noise_suppression_description @@ -127,7 +125,9 @@ jobs: # Dummy job to simplify branch protections ci: name: Static Analysis - needs: [lint, i18n] + needs: [lint, i18n, zizmor] + if: always() runs-on: ubuntu-24.04 steps: - - run: echo "Ok" + - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: exit 1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b9ffa23c99..8a237b6950 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,8 +5,6 @@ on: types: [checks_requested] push: branches: [develop, master] - repository_dispatch: - types: [element-web-notify] workflow_call: inputs: disable_coverage: @@ -47,7 +45,7 @@ jobs: - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: pnpm cache - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: "lts/*" cache: "pnpm" @@ -58,7 +56,7 @@ jobs: JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }} - name: Jest Cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: /tmp/jest_cache key: ${{ hashFiles('**/pnpm-lock.yaml') }} @@ -93,7 +91,7 @@ jobs: - name: Upload Artifact if: env.ENABLE_COVERAGE == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage-${{ matrix.runner }} path: | @@ -102,7 +100,7 @@ jobs: complete: name: jest-tests - needs: [jest_ew, vitest_sc] + needs: [jest_ew, vitest] if: always() runs-on: ubuntu-24.04 permissions: @@ -122,8 +120,13 @@ jobs: sha: ${{ github.sha }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - vitest_sc: - name: Vitest (Shared Components) + vitest: + name: Vitest + strategy: + matrix: + package: + - shared-components + - module-api runs-on: ubuntu-24.04 steps: - name: Checkout code @@ -134,49 +137,42 @@ jobs: - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: pnpm cache - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: "lts/*" cache: "pnpm" - - name: Install Shared Component Deps - working-directory: "packages/shared-components" + - name: Install Deps run: "pnpm install" - name: Cache storybook & vitest - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | - packages/shared-components/node_modules/.cache - packages/shared-components/node_modules/.vite/vitest + packages/${{ matrix.package }}/node_modules/.cache + packages/${{ matrix.package }}/node_modules/.vite/vitest key: ${{ hashFiles('pnpm-lock.yaml') }} - - name: Get installed Playwright version - working-directory: packages/shared-components - id: playwright - run: echo "version=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT - - - name: Cache playwright binaries - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 - id: playwright-cache + - name: Setup playwright + uses: ./.github/actions/setup-playwright + if: matrix.package == 'shared-components' with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell - - - name: Install Playwright browsers - working-directory: packages/shared-components - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: "pnpm playwright install --with-deps --only-shell" + write-cache: ${{ github.event_name != 'merge_group' }} - name: Run tests - working-directory: "packages/shared-components" + working-directory: "packages/${{ matrix.package }}" run: pnpm test:unit --coverage=$ENABLE_COVERAGE + # Dump the disk usage on failure, because this job seems to fail with disk fills sometimes + - name: df + run: df + if: ${{ failure() }} + - name: Upload Artifact if: env.ENABLE_COVERAGE == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: coverage-sharedcomponents + name: coverage-${{ matrix.package }} path: | - packages/shared-components/coverage - !packages/shared-components/coverage/lcov-report + packages/${{ matrix.package }}/coverage + !packages/${{ matrix.package }}/coverage/lcov-report diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 496cfd53df..582207dc14 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -27,7 +27,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') || contains(github.event.issue.labels.*.name, 'A-Element-Call') steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | github.rest.issues.addLabels({ @@ -44,7 +44,7 @@ jobs: contains(github.event.issue.labels.*.name, 'good first issue') || contains(github.event.issue.labels.*.name, 'Hacktoberfest') steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | github.rest.issues.addLabels({ diff --git a/.github/workflows/triage-unlabelled.yml b/.github/workflows/triage-unlabelled.yml index 71396be804..04f312ab32 100644 --- a/.github/workflows/triage-unlabelled.yml +++ b/.github/workflows/triage-unlabelled.yml @@ -43,7 +43,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A-Element-Call')) && contains(github.event.issue.labels.*.name, 'Z-Labs') steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | github.rest.issues.removeLabel({ diff --git a/.github/workflows/update-jitsi.yml b/.github/workflows/update-jitsi.yml index 2e52b08cbf..7d56d6d418 100644 --- a/.github/workflows/update-jitsi.yml +++ b/.github/workflows/update-jitsi.yml @@ -14,7 +14,7 @@ jobs: persist-credentials: false - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: cache: "pnpm" node-version: "lts/*" @@ -27,7 +27,7 @@ jobs: run: "pnpm vendor:jitsi" - name: Create Pull Request - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} branch: actions/jitsi-update diff --git a/.github/workflows/update-topics.yaml b/.github/workflows/update-topics.yaml index c1fb78e3b8..698c8da804 100644 --- a/.github/workflows/update-topics.yaml +++ b/.github/workflows/update-topics.yaml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-24.04 environment: Matrix steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: HS_URL: ${{ secrets.BETABOT_HS_URL }} LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }} diff --git a/.prettierignore b/.prettierignore index ca5fe9afd8..e0a9e4fc57 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,7 +14,8 @@ webpack-stats.json .vscode/ .env coverage -# Auto-generated file +# Auto-generated files +*.api.md /apps/web/src/modules.ts /apps/web/src/modules.js src/i18n/strings @@ -49,6 +50,7 @@ CHANGELOG.md /apps/desktop/dist/ /apps/desktop/build/ /apps/desktop/dockerbuild/ +/apps/desktop/deploys/ /apps/desktop/lib/ /apps/desktop/webapp /apps/desktop/playwright/html-report diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f628628ce..615e496938 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,21 @@ As for your PR description, it should include these things: Please **_do not use force push_** in your PRs. Doing so means we can't see what has changed. We use squash merge to get a "clean" git history. +### Adding a new feature or enhancement + +To make a great product with a great user experience, all the small efforts need to go in the same direction and be aligned and consistent with each other. + +Before making your contribution, please consider the following: + +- One product can’t do everything well. Element is focusing on private end-to-end encrypted messaging and voice - this can either be for consumers (e.g. friends and family) or for professional teams and organizations. Public forums and other types of chats without E2EE remain supported but are not the primary use case in case UX compromises need to be made. +- There are 3 platforms - Web/Desktop, [Android](https://github.com/element-hq/element-x-android) and [iOS](https://github.com/element-hq/element-x-ios). These platforms need to have feature parity and design consistency. For some features, supporting all platforms is a must have, in some cases exceptions can be made to have it on one platform only. +- To make sure your idea fits both from a design/solution and use case perspective, please open a new issue (or find an existing issue) in [element-meta](https://github.com/element-hq/element-meta/issues) repository describing the use case and how you plan to tackle it. Do not just describe what feature is missing, explain why the users need it with a couple of real life examples from the field. + - In case of an existing issue, please comment that you're planning to contribute. If you create a new issue, please specify that in the issue. In such a case we will try to review the issue ASAP and provide you with initial feedback so you can be confident if and at which conditions your contributions will be accepted. + +Once we know that you want to contribute and have confirmed that the new feature is overall aligned with the product direction, the designers of the core team will help you with the designs and any other type of guidance when it comes to the user experience. We will try to unblock you as quickly as we can, but it may not be instant. Having a clear understanding of the use case and the impact of the feature will help us with the prioritization and faster responses. + +Only once all of the above is met should you open a PR with your proposed changes. + ### Changelogs There's no need to manually add Changelog entries: we use information in the diff --git a/apps/desktop/.node-version b/apps/desktop/.node-version index 8e35034890..5bf4400f22 100644 --- a/apps/desktop/.node-version +++ b/apps/desktop/.node-version @@ -1 +1 @@ -24.14.1 +24.15.0 diff --git a/apps/desktop/dockerbuild/Dockerfile b/apps/desktop/dockerbuild/Dockerfile index 0acea0e60d..f9521745ba 100644 --- a/apps/desktop/dockerbuild/Dockerfile +++ b/apps/desktop/dockerbuild/Dockerfile @@ -1,7 +1,7 @@ # Docker image to facilitate building Element Desktop's native bits using a glibc version (2.31) # with broader compatibility, down to Debian bullseye & Ubuntu focal. -FROM rust:bullseye@sha256:bc19574c121fe10c1bc68fc2b1ea9b420d87d047a0c50fb1622b282199700cee +FROM rust:bullseye@sha256:949b0903defbfc4e374dc85f947b153859e9ee0104e425cd9a74d94474a9a335 ENV DEBIAN_FRONTEND=noninteractive diff --git a/apps/desktop/hak/tsconfig.json b/apps/desktop/hak/tsconfig.json index f547523e48..b762dda71a 100644 --- a/apps/desktop/hak/tsconfig.json +++ b/apps/desktop/hak/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "moduleResolution": "node", + "moduleResolution": "node16", + "module": "Node16", "esModuleInterop": true, "target": "es2022", "sourceMap": false, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a4f9c68437..158bb1a284 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -28,7 +28,7 @@ "mkdirs": "mkdirp packages deploys", "fetch": "pnpm run mkdirs && node scripts/fetch-package.ts", "asar-webapp": "asar p webapp webapp.asar", - "start": "pnpm run build:ts && pnpm run build:res && electron .", + "start": "nx start", "lint": "pnpm lint:types && pnpm lint:js", "lint:js": "eslint --max-warnings 0 src hak playwright scripts", "lint:js-fix": "eslint --fix --max-warnings 0 src hak playwright scripts && prettier --log-level=warn --write .", @@ -39,22 +39,19 @@ "lint:types:hak": "tsc --noEmit -p hak/tsconfig.json", "build:native": "pnpm run hak", "build:native:universal": "pnpm run hak --target x86_64-apple-darwin fetchandbuild && pnpm run hak --target aarch64-apple-darwin fetchandbuild && pnpm run hak --target x86_64-apple-darwin --target aarch64-apple-darwin copyandlink", - "build:32": "pnpm run build:ts && pnpm run build:res && electron-builder --ia32", - "build:64": "pnpm run build:ts && pnpm run build:res && electron-builder --x64", - "build:universal": "pnpm run build:ts && pnpm run build:res && electron-builder --universal", - "build": "pnpm run build:ts && pnpm run build:res && electron-builder", - "build:ts": "tsc", - "build:res": "node scripts/copy-res.ts", + "build:32": "nx build --ia32", + "build:64": "nx build --x64", + "build:universal": "nx build --universal", + "build": "nx build --", "docker:setup": "docker build --platform linux/amd64 -t element-desktop-dockerbuild -f dockerbuild/Dockerfile .", "docker:build:native": "scripts/in-docker.sh pnpm run hak", "docker:build": "scripts/in-docker.sh pnpm run build", "docker:install": "scripts/in-docker.sh pnpm install", "clean": "rimraf webapp.asar dist packages deploys lib", "hak": "node scripts/hak/index.ts", - "test": "playwright test", - "test:open": "pnpm test --ui", - "test:screenshots:build": "docker build playwright -t element-desktop-playwright --platform linux/amd64", - "test:screenshots:run": "docker run --rm --network host -v $(pwd):/work/element-desktop -v element-desktop-playwright:/work/element-desktop/node_modules -v /var/run/docker.sock:/var/run/docker.sock --platform linux/amd64 -it element-desktop-playwright", + "test:playwright": "nx test:playwright --", + "test:playwright:open": "nx test:playwright -- --ui", + "test:playwright:screenshots": "nx test:playwright:screenshots --", "sane-postinstall": "electron-builder install-app-deps" }, "dependencies": { @@ -65,13 +62,14 @@ "electron-window-state": "^5.0.3", "minimist": "^1.2.6", "png-to-ico": "^3.0.0", - "uuid": "^13.0.0" + "uuid": "^14.0.0" }, "devDependencies": { "@babel/core": "^7.18.10", "@babel/preset-env": "^7.18.10", "@babel/preset-typescript": "^7.18.6", - "@electron/asar": "4.1.2", + "@electron/asar": "4.2.0", + "@electron/fuses": "^2.1.1", "@playwright/test": "catalog:", "@stylistic/eslint-plugin": "^5.0.0", "@types/auto-launch": "^5.0.1", @@ -81,12 +79,12 @@ "@types/pacote": "^11.1.1", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", - "app-builder-lib": "26.8.2", + "app-builder-lib": "26.9.0", "chokidar": "^5.0.0", "detect-libc": "^2.0.0", - "electron": "41.1.0", - "electron-builder": "26.8.2", - "electron-builder-squirrel-windows": "26.8.2", + "electron": "41.2.2", + "electron-builder": "26.9.0", + "electron-builder-squirrel-windows": "26.9.0", "electron-devtools-installer": "^4.0.0", "eslint": "^8.26.0", "eslint-config-google": "^0.14.0", @@ -102,10 +100,13 @@ "prettier": "^3.0.0", "rimraf": "^6.0.0", "tar": "^7.5.8", - "typescript": "5.9.3" + "typescript": "6.0.3" }, "hakDependencies": { - "matrix-seshat": "^4.0.1" + "matrix-seshat": "4.2.0" }, - "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", + "nx": { + "includedScripts": [] + } } diff --git a/apps/desktop/playwright/Dockerfile b/apps/desktop/playwright/Dockerfile index dabf89dd46..212eeb3438 100644 --- a/apps/desktop/playwright/Dockerfile +++ b/apps/desktop/playwright/Dockerfile @@ -1,13 +1,19 @@ FROM mcr.microsoft.com/playwright:v1.59.1-jammy@sha256:8a0360d39d1973be506dd59002904a774f6d697d4946c94063b3fd006461c8ff -WORKDIR /work/element-desktop +WORKDIR /work -RUN apt-get update && apt-get -y install xvfb dbus-x11 && apt-get purge -y --auto-remove && rm -rf /var/lib/apt/lists/* +RUN apt-get update && \ + apt-get -y install xvfb dbus-x11 && \ + apt-get purge -y --auto-remove && \ + rm -rf /var/lib/apt/lists/* && \ + corepack enable -# Create node_modules & dist dirs so that the volumes have the correct permissions -RUN mkdir node_modules dist && chown 1000:1000 node_modules dist +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +ENV GITHUB_ACTIONS=1 +ENV DEBUG=pw:browser +# switch to node user USER 1000:1000 -COPY docker-entrypoint.sh /opt/docker-entrypoint.sh +COPY apps/desktop/playwright/docker-entrypoint.sh /opt/docker-entrypoint.sh ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"] diff --git a/apps/desktop/playwright/docker-entrypoint.sh b/apps/desktop/playwright/docker-entrypoint.sh index 68deb4af9a..fd846cb2fa 100644 --- a/apps/desktop/playwright/docker-entrypoint.sh +++ b/apps/desktop/playwright/docker-entrypoint.sh @@ -8,11 +8,5 @@ sleep 2 export DISPLAY=:99 -pnpm install --frozen-lockfile -pnpm build -l --dir - PLAYWRIGHT_HTML_OPEN=never ELEMENT_DESKTOP_EXECUTABLE="./dist/linux-unpacked/element-desktop" \ - npx playwright test --update-snapshots --reporter line,html "$1" - -# Clean up -rm -R core qemu_* || exit 0 + exec pnpm -C apps/desktop exec playwright test --update-snapshots --reporter line,html "$1" diff --git a/apps/desktop/playwright/element-desktop-test.ts b/apps/desktop/playwright/element-desktop-test.ts index 743be8da6f..b2fa3c983e 100644 --- a/apps/desktop/playwright/element-desktop-test.ts +++ b/apps/desktop/playwright/element-desktop-test.ts @@ -17,14 +17,14 @@ import { PassThrough } from "node:stream"; * A PassThrough stream that captures all data written to it. */ class CapturedPassThrough extends PassThrough { - private _chunks = []; + private _chunks: any[] = []; public constructor() { super(); super.on("data", this.onData); } - private onData = (chunk): void => { + private onData = (chunk: any): void => { this._chunks.push(chunk); }; @@ -69,7 +69,13 @@ export const test = base.extend({ const args = ["--profile-dir", tmpDir, ...extraArgs]; if (process.env.GITHUB_ACTIONS) { + args.push("--disable-gpu"); + if (process.platform === "linux") { + if (process.getuid() === 0) { + args.push("--no-sandbox"); + } + // GitHub Actions hosted runner lacks dbus and a compatible keyring, so we need to force plaintext storage args.push("--storage-mode", "force-plaintext"); } else if (process.platform === "darwin") { diff --git a/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png b/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png index 7bcf260d52..baec548402 100644 Binary files a/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png and b/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png differ diff --git a/apps/desktop/playwright/tsconfig.json b/apps/desktop/playwright/tsconfig.json index 4c9756442b..931ff18075 100644 --- a/apps/desktop/playwright/tsconfig.json +++ b/apps/desktop/playwright/tsconfig.json @@ -1,11 +1,12 @@ { "compilerOptions": { "resolveJsonModule": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "esModuleInterop": true, "target": "es2022", - "module": "es2022", - "lib": ["es2022", "dom"], + "module": "ESNext", + "lib": ["es2024", "dom", "dom.iterable"], + "strictNullChecks": false, "types": ["node"] }, "include": ["**/*.ts"] diff --git a/apps/desktop/project.json b/apps/desktop/project.json index cb43a99d61..ae1bd1453e 100644 --- a/apps/desktop/project.json +++ b/apps/desktop/project.json @@ -19,6 +19,65 @@ "tags": ["type=ref,event=branch"] } } + }, + "build:ts": { + "cache": true, + "command": "tsc", + "inputs": ["src", "{projectRoot}/tsconfig.json"], + "outputs": ["{projectRoot}/lib/*.js", "{projectRoot}/lib/*.d.ts"], + "options": { "cwd": "apps/desktop" } + }, + "build:res": { + "cache": true, + "command": "node scripts/copy-res.ts", + "inputs": ["{projectRoot}/i18n"], + "outputs": ["{projectRoot}/lib/i18n"], + "options": { "cwd": "apps/desktop" } + }, + "build": { + "cache": true, + "command": "pnpm exec electron-builder", + "inputs": [ + "src", + "{projectRoot}/.hak/hakModules", + "{projectRoot}/electron-builder.json", + "{projectRoot}/webapp.asar" + ], + "outputs": ["{projectRoot}/dist"], + "options": { "cwd": "apps/desktop" }, + "dependsOn": ["build:*"] + }, + "start": { + "command": "electron .", + "options": { "cwd": "apps/desktop" }, + "dependsOn": ["build:*"] + }, + "test:playwright": { + "command": "playwright test", + "options": { "cwd": "apps/desktop" } + }, + "test:playwright:screenshots:build-app": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "pnpm run build -l --x64 --dir --publish=never", + "pnpm exec electron-fuses write --app ./dist/linux-unpacked/element-desktop EnableNodeCliInspectArguments=on" + ], + "parallel": false, + "cwd": "apps/desktop" + }, + "dependsOn": ["build:*"] + }, + "test:playwright:screenshots:build-docker": { + "cache": true, + "command": "docker build -f playwright/Dockerfile -t element-desktop-playwright --platform linux/amd64 ../..", + "inputs": ["{projectRoot}/playwright/Dockerfile", "{projectRoot}/playwright/docker-entrypoint.sh"], + "options": { "cwd": "apps/desktop" } + }, + "test:playwright:screenshots": { + "command": "docker run --rm --network host -v $(pwd)/../../:/work/ --platform linux/amd64 -it element-desktop-playwright", + "options": { "cwd": "apps/desktop" }, + "dependsOn": ["test:playwright:screenshots:*"] } } } diff --git a/apps/desktop/src/i18n/strings/zh_Hans.json b/apps/desktop/src/i18n/strings/zh_Hans.json index 448412456d..d9b6be5832 100644 --- a/apps/desktop/src/i18n/strings/zh_Hans.json +++ b/apps/desktop/src/i18n/strings/zh_Hans.json @@ -22,7 +22,9 @@ "about": "关于", "brand_help": "%(brand)s帮助", "help": "帮助", - "preferences": "偏好" + "no": "否", + "preferences": "偏好", + "yes": "是" }, "confirm_quit": "你确定要退出吗?", "edit_menu": { @@ -30,9 +32,20 @@ "speech_start_speaking": "开始讲话", "speech_stop_speaking": "停止讲话" }, + "eol": { + "no_more_updates": "你正在使用的 macOS 版本不受支持。请升级以获取 %(brand)s 更新。", + "title": "不受支持的系统", + "warning": "你正在使用的 macOS 版本不受支持。请升级系统以确保 %(brand)s 能保持运行。" + }, "file_menu": { "label": "文件" }, + "icon_overlay": { + "description_error": "错误", + "description_notifications": { + "other": "你有 %(count)s 条未读通知。" + } + }, "menu": { "hide": "隐藏", "hide_others": "隐藏其他", @@ -49,6 +62,21 @@ "save_image_as_error_description": "图片保存失败", "save_image_as_error_title": "图片保存失败" }, + "store": { + "error": { + "backend_changed": "清除数据并重新加载?", + "backend_changed_detail": "无法从系统密钥环访问密钥,该密钥似乎已被更改。", + "backend_changed_title": "数据库加载失败", + "backend_no_encryption": "你的系统支持密钥环,但加密功能不可用。", + "backend_no_encryption_detail": "Electron 检测到你的密钥环 %(backend)s 的加密不可用。请确保已安装该密钥环。若已安装请重启设备后重试。也可选择允许 %(brand)s 使用弱加密方式。", + "backend_no_encryption_title": "不支持加密", + "unsupported_keyring": "你的系统存在不受支持的密钥环,这意味着无法打开数据库。", + "unsupported_keyring_detail": "Electron 的密钥环检测未找到受支持的后端。你可以尝试通过命令行参数启动 %(brand)s 来手动配置后端,此操作仅需执行一次。详情请参阅:%(link)s。", + "unsupported_keyring_title": "不受支持的系统", + "unsupported_keyring_use_basic_text": "使用弱加密", + "unsupported_keyring_use_plaintext": "不使用加密" + } + }, "view_menu": { "actual_size": "实际大小", "toggle_developer_tools": "切换开发者工具", diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index ca8d04950d..2cc7e22fdb 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,8 +1,8 @@ -# syntax=docker.io/docker/dockerfile:1.22-labs@sha256:4c116b618ed48404d579b5467127b20986f2a6b29e4b9be2fee841f632db6a86 +# syntax=docker.io/docker/dockerfile:1.23-labs@sha256:7eca9451d94f9b8ad22e44988b92d595d3e4d65163794237949a8c3413fbed5d # Context must be the root of the monorepo # Builder -FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:27e462f5db2402700867dfa8ec35e3a68b127fdf61b505db0dd6ab98c38284bb AS builder +FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:d2059a9c157c9f70739736979fa3635008bf3ca74560b30930dc181228bc427f AS builder # Support custom branch of the js-sdk. This also helps us build images of element-web develop. ARG USE_CUSTOM_SDKS=false @@ -25,7 +25,7 @@ RUN --mount=type=bind,source=.git,target=/src/.git /src/scripts/docker-package.s RUN cp /src/apps/web/config.sample.json /src/apps/web/webapp/config.json # App -FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:b5831ee7f7aa827cbae87df4a30a642f62c747d8525f5674365389f3adab278d +FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:360465db60105a4cbf5215cd9e5a2ba40ef956978dd94f99707e9674050e38ea # Need root user to install packages & manipulate the usr directory USER root diff --git a/apps/web/Dockerfile.dockerignore b/apps/web/Dockerfile.dockerignore index 403d667eaa..f43268b142 100644 --- a/apps/web/Dockerfile.dockerignore +++ b/apps/web/Dockerfile.dockerignore @@ -10,6 +10,7 @@ **/.pnpm-store **/tsconfig.node.tsbuildinfo **/*.md +!**/*.api.md **/*.rst .idea/ @@ -22,6 +23,8 @@ apps/web/webpack-stats.json apps/web/playwright/ apps/web/webapp/ apps/web/debian/ +apps/desktop +!apps/desktop/package.json packages/shared-components/__vis__/ packages/shared-components/storybook-static/ diff --git a/apps/web/package.json b/apps/web/package.json index 98b6f8c876..d6d8c9b17b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,15 +30,15 @@ "lint:types": "nx lint:types", "lint:style": "stylelint \"res/css/**/*.pcss\"", "test": "nx test:unit", - "test:playwright": "playwright test", - "test:playwright:open": "pnpm test:playwright --ui", - "test:playwright:screenshots": "playwright-screenshots-experimental pnpm playwright test --update-snapshots --project=Chrome --grep @screenshot", + "test:playwright": "nx test:playwright --", + "test:playwright:open": "nx test:playwright -- --ui", + "test:playwright:screenshots": "nx test:playwright:screenshots --", "coverage": "pnpm test --coverage", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@element-hq/element-web-module-api": "catalog:", + "@element-hq/element-web-module-api": "workspace:*", "@element-hq/web-shared-components": "workspace:*", "@fontsource/fira-code": "^5", "@fontsource/inter": "catalog:", @@ -72,7 +72,7 @@ "glob-to-regexp": "^0.4.1", "highlight.js": "^11.3.1", "html-entities": "^2.0.0", - "html-react-parser": "^5.2.2", + "html-react-parser": "^6.0.0", "is-ip": "^5.0.0", "js-xxhash": "^5.0.0", "jsrsasign": "^11.0.0", @@ -89,7 +89,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.364.7", + "posthog-js": "1.369.3", "qrcode": "1.5.4", "re-resizable": "6.11.2", "react": "catalog:", @@ -101,10 +101,9 @@ "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", - "sanitize-html": "2.17.2", + "sanitize-html": "2.17.3", "tar-js": "^0.3.0", "ua-parser-js": "1.0.40", - "uuid": "^13.0.0", "what-input": "^5.2.10" }, "devDependencies": { @@ -125,10 +124,9 @@ "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", - "@casualbot/jest-sonar-reporter": "2.5.0", - "@element-hq/element-call-embedded": "0.18.0", - "@element-hq/element-web-playwright-common": "catalog:", - "@element-hq/element-web-playwright-common-local": "workspace:*", + "@casualbot/jest-sonar-reporter": "2.6.0", + "@element-hq/element-call-embedded": "0.19.1", + "@element-hq/element-web-playwright-common": "workspace:*", "@fetch-mock/jest": "^0.2.20", "@jest/globals": "^30.2.0", "@peculiar/webcrypto": "^1.4.3", @@ -206,17 +204,17 @@ "mini-css-extract-plugin": "2.10.2", "modernizr": "^3.12.0", "playwright-core": "catalog:", - "postcss": "8.5.8", + "postcss": "8.5.10", "postcss-easings": "4.0.0", "postcss-hexrgba": "2.1.0", "postcss-import": "16.1.1", "postcss-loader": "8.2.1", "postcss-mixins": "12.1.2", "postcss-nested": "7.0.2", - "postcss-preset-env": "11.2.0", + "postcss-preset-env": "11.2.1", "postcss-scss": "4.0.9", "postcss-simple-vars": "7.0.1", - "prettier": "3.8.1", + "prettier": "3.8.3", "process": "^0.11.10", "raw-loader": "^4.0.2", "semver": "^7.5.2", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index f6d15f4e79..6120557148 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -91,7 +91,7 @@ export default defineConfig<{}, WorkerOptions>({ trace: "on-first-retry", }, webServer: { - command: process.env.CI ? "npx serve -p 8080 -L ./webapp" : "pnpm start", + command: process.env.CI ? "npx serve -p 8080 -L ./webapp" : "nx --outputStyle stream start", url: `${baseURL}/config.json`, reuseExistingServer: true, timeout: (process.env.CI ? 30 : 120) * 1000, diff --git a/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts b/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts index 960b6a6692..d5679bc017 100644 --- a/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts +++ b/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts @@ -20,7 +20,7 @@ test.use({ test("Shows the welcome page by default", async ({ page }) => { await page.goto("/"); - await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible(); await expect(page.getByRole("link", { name: "Sign in" })).toBeVisible(); }); diff --git a/apps/web/playwright/e2e/audio-player/audio-player.spec.ts b/apps/web/playwright/e2e/audio-player/audio-player.spec.ts index f042b07993..e0b707e9cd 100644 --- a/apps/web/playwright/e2e/audio-player/audio-player.spec.ts +++ b/apps/web/playwright/e2e/audio-player/audio-player.spec.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. */ import type { Locator, Page } from "@playwright/test"; -import { test, expect } from "../../element-web-test"; +import { test, expect, type ExtendedToMatchScreenshotOptions } from "../../element-web-test"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import { Layout } from "../../../src/settings/enums/Layout"; import { type ElementAppPage } from "../../pages/ElementAppPage"; @@ -94,7 +94,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { // Assert that rendering of the player settled and the play button is visible before taking a snapshot await checkPlayerVisibility(ircTile); - const screenshotOptions = { + const screenshotOptions: ExtendedToMatchScreenshotOptions = { css: ` /* The timestamp is of inconsistent width depending on the time the test runs at */ .mx_MessageTimestamp { @@ -120,7 +120,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { }; // Take a snapshot of mx_EventTile_last on IRC layout - screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox(); + screenshotOptions.clip = (await page.locator(".mx_EventTile_last").boundingBox()) ?? undefined; await scrollToBottomOfTimeline(page); await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-irc-layout.png`, screenshotOptions); @@ -129,7 +129,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { const groupTile = page.locator(".mx_EventTile_last[data-layout='group']"); await groupTile.locator(".mx_MessageTimestamp").click(); await checkPlayerVisibility(groupTile); - screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox(); + screenshotOptions.clip = (await page.locator(".mx_EventTile_last").boundingBox()) ?? undefined; await scrollToBottomOfTimeline(page); await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-group-layout.png`, screenshotOptions); @@ -138,7 +138,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { const bubbleTile = page.locator(".mx_EventTile_last[data-layout='bubble']"); await bubbleTile.locator(".mx_MessageTimestamp").click(); await checkPlayerVisibility(bubbleTile); - screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox(); + screenshotOptions.clip = (await page.locator(".mx_EventTile_last").boundingBox()) ?? undefined; await scrollToBottomOfTimeline(page); await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-bubble-layout.png`, screenshotOptions); }; diff --git a/apps/web/playwright/e2e/crypto/crypto.spec.ts b/apps/web/playwright/e2e/crypto/crypto.spec.ts index d03fa1454e..a5dfd32755 100644 --- a/apps/web/playwright/e2e/crypto/crypto.spec.ts +++ b/apps/web/playwright/e2e/crypto/crypto.spec.ts @@ -27,6 +27,9 @@ const startDMWithBob = async (page: Page, bob: Bot) => { await page.getByRole("option", { name: bob.credentials.displayName }).click(); await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible(); await page.getByRole("button", { name: "Go" }).click(); + + await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); }; const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => { @@ -44,7 +47,7 @@ const bobJoin = async (page: Page, bob: Bot) => { const bobRooms = cli.getRooms(); if (!bobRooms.length) { await new Promise((resolve) => { - const onMembership = (_event) => { + const onMembership = () => { cli.off(window.matrixcs.RoomMemberEvent.Membership, onMembership); resolve(); }; diff --git a/apps/web/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/apps/web/playwright/e2e/crypto/decryption-failure-messages.spec.ts index 728b3bf620..1dc04ee905 100644 --- a/apps/web/playwright/e2e/crypto/decryption-failure-messages.spec.ts +++ b/apps/web/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import type { EmittedEvents, Preset } from "matrix-js-sdk/src/matrix"; +import type { Preset, RoomMemberEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { expect, test } from "../../element-web-test"; import { createRoom, @@ -122,7 +122,7 @@ test.describe("Cryptography", function () { const roomId = await bob.evaluate( async (client, { alice }) => { const encryptionStatePromise = new Promise((resolve) => { - client.on("RoomState.events" as EmittedEvents, (event, _state, _lastStateEvent) => { + client.on("RoomState.events" as RoomStateEvent.Events, (event, _state, _lastStateEvent) => { if (event.getType() === "m.room.encryption") { resolve(); } @@ -253,11 +253,14 @@ test.describe("Cryptography", function () { // invite Alice const inviteAlicePromise = new Promise((resolve) => { - client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => { - if (member.userId === alice.userId && member.membership === "invite") { - resolve(); - } - }); + client.on( + "RoomMember.membership" as RoomMemberEvent.Membership, + (_event, member, _oldMembership?) => { + if (member.userId === alice.userId && member.membership === "invite") { + resolve(); + } + }, + ); }); await client.invite(roomId, alice.userId); // wait for the invite to come back so that we encrypt to Alice @@ -271,11 +274,14 @@ test.describe("Cryptography", function () { // kick Alice const kickAlicePromise = new Promise((resolve) => { - client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => { - if (member.userId === alice.userId && member.membership === "leave") { - resolve(); - } - }); + client.on( + "RoomMember.membership" as RoomMemberEvent.Membership, + (_event, member, _oldMembership?) => { + if (member.userId === alice.userId && member.membership === "leave") { + resolve(); + } + }, + ); }); await client.kick(roomId, alice.userId); await kickAlicePromise; diff --git a/apps/web/playwright/e2e/crypto/dehydration.spec.ts b/apps/web/playwright/e2e/crypto/dehydration.spec.ts index 2707e2d45d..ccb98e89df 100644 --- a/apps/web/playwright/e2e/crypto/dehydration.spec.ts +++ b/apps/web/playwright/e2e/crypto/dehydration.spec.ts @@ -166,13 +166,9 @@ async function getDehydratedDeviceIds(client: Client): Promise { return await client.evaluate(async (client) => { const userId = client.getUserId(); const devices = await client.getCrypto().getUserDeviceInfo([userId]); - return Array.from( - devices - .get(userId) - .values() - .filter((d) => d.dehydrated) - .map((d) => d.deviceId), - ); + return Array.from(devices.get(userId).values()) + .filter((d) => d.dehydrated) + .map((d) => d.deviceId); }); } diff --git a/apps/web/playwright/e2e/crypto/device-verification.spec.ts b/apps/web/playwright/e2e/crypto/device-verification.spec.ts index 1a1731e6ae..07fa4ed9d8 100644 --- a/apps/web/playwright/e2e/crypto/device-verification.spec.ts +++ b/apps/web/playwright/e2e/crypto/device-verification.spec.ts @@ -21,7 +21,6 @@ import { waitForVerificationRequest, } from "./utils"; import { type Bot } from "../../pages/bot"; -import { Toasts } from "../../pages/toasts.ts"; import type { ElementAppPage } from "../../pages/ElementAppPage.ts"; test.describe("Device verification", { tag: "@no-webkit" }, () => { @@ -82,7 +81,11 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { ); // Regression test for https://github.com/element-hq/element-web/issues/29110 - test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => { + test("No toast after verification, even if the secrets take a while to arrive", async ({ + page, + credentials, + toasts, + }) => { // Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens // when we are in an encrypted room. await aliceBotClient.createRoom({ @@ -121,7 +124,6 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { await infoDialog.getByRole("button", { name: "Got it" }).click(); // There should be no toast (other than the notifications one) - const toasts = new Toasts(page); await toasts.rejectToast("Notifications"); await toasts.assertNoToasts(); diff --git a/apps/web/playwright/e2e/crypto/history-sharing.spec.ts b/apps/web/playwright/e2e/crypto/history-sharing.spec.ts index 3974ba79c5..29676036ba 100644 --- a/apps/web/playwright/e2e/crypto/history-sharing.spec.ts +++ b/apps/web/playwright/e2e/crypto/history-sharing.spec.ts @@ -49,7 +49,7 @@ test.describe("History sharing", function () { await sendMessageInCurrentRoom(alicePage, "A message from Alice"); // Send the invite to Bob - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -105,7 +105,7 @@ test.describe("History sharing", function () { // Alice invites Bob, and Bob accepts const roomId = await aliceElementApp.getCurrentRoomIdFromUrl(); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); await bobPage.getByRole("option", { name: "TestRoom" }).click(); await bobPage.getByRole("button", { name: "Accept" }).click(); @@ -143,7 +143,7 @@ test.describe("History sharing", function () { await sendMessageInCurrentRoom(bobPage, "Message3: 'shared' visibility, but Bob thinks it is still 'joined'"); // Alice now invites Charlie - await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId, { confirmUnknownUser: true }); await charliePage.getByRole("option", { name: "TestRoom" }).click(); await charliePage.getByRole("button", { name: "Accept" }).click(); diff --git a/apps/web/playwright/e2e/crypto/toasts.spec.ts b/apps/web/playwright/e2e/crypto/toasts.spec.ts index 876000009b..9ce1b8a5ae 100644 --- a/apps/web/playwright/e2e/crypto/toasts.spec.ts +++ b/apps/web/playwright/e2e/crypto/toasts.spec.ts @@ -43,11 +43,7 @@ test.describe("Key storage out of sync toast", () => { }); test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => { - // We need to wait for there to be two toasts as the wait below won't work in isolation: - // playwright only evaluates the 'first()' call initially, not subsequent times it checks, so - // it would always be checking the same toast, even if another one is now the first. - await expect(page.getByRole("alert")).toHaveCount(2); - await expect(page.getByRole("alert").first()).toMatchScreenshot( + await expect(page.getByRole("alert").filter({ hasText: "Your key storage is out of sync." })).toMatchScreenshot( "key-storage-out-of-sync-toast.png", screenshotOptions, ); diff --git a/apps/web/playwright/e2e/crypto/utils.ts b/apps/web/playwright/e2e/crypto/utils.ts index 56378358ea..479ae12a78 100644 --- a/apps/web/playwright/e2e/crypto/utils.ts +++ b/apps/web/playwright/e2e/crypto/utils.ts @@ -579,8 +579,8 @@ export async function deleteCachedSecrets(page: Page) { await page.evaluate(async () => { const removeCachedSecrets = new Promise((resolve) => { const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto"); - request.onsuccess = (event: Event & { target: { result: IDBDatabase } }) => { - const db = event.target.result; + request.onsuccess = function (this: IDBRequest) { + const db = this.result as IDBDatabase; const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity"); request.onsuccess = () => { db.close(); diff --git a/apps/web/playwright/e2e/invite/invite-dialog.spec.ts b/apps/web/playwright/e2e/invite/invite-dialog.spec.ts index 42588f37c7..60fb6bf4af 100644 --- a/apps/web/playwright/e2e/invite/invite-dialog.spec.ts +++ b/apps/web/playwright/e2e/invite/invite-dialog.spec.ts @@ -9,6 +9,15 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; +/** + * CSS which will hide the mxid in the user list of the "unknown users" confirmation dialog. This is useful because the + * MXID is not stable and the screenshot tests will otherwise fail. + * + * Ideally RichItem would give us a way to do this that doesn't involve gnarly CSS. + */ +const UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS = + '[data-testid="userlist"] li > span:nth-last-child(1) { display: none }'; + test.describe("Invite dialog", function () { test.use({ displayName: "Hanako", @@ -62,6 +71,15 @@ test.describe("Invite dialog", function () { // Invite the bot await other.getByRole("button", { name: "Invite" }).click(); + // Expect a confirmation dialog, screenshot, and dismiss + await expect( + page.locator(".mx_Dialog").getByRole("heading", { name: "Invite new contacts to this room?" }), + ).toBeVisible(); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-invite-new-contact.png", { + css: UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS, + }); + await page.locator(".mx_Dialog").getByRole("button", { name: "Invite" }).click(); + // Assert that the invite dialog disappears await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); @@ -104,6 +122,15 @@ test.describe("Invite dialog", function () { // Open a direct message UI await other.getByRole("button", { name: "Go" }).click(); + // Expect a confirmation dialog, screenshot, and dismiss + await expect( + page.locator(".mx_Dialog").getByRole("heading", { name: "Start a chat with this new contact?" }), + ).toBeVisible(); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-chat-with-new-contact.png", { + css: UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS, + }); + await page.locator(".mx_Dialog").getByRole("button", { name: "Continue" }).click(); + // Assert that the invite dialog disappears await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts new file mode 100644 index 0000000000..01146ef0dd --- /dev/null +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts @@ -0,0 +1,267 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type Page } from "@playwright/test"; + +import { expect, test } from "../../../element-web-test"; +import { getRoomList, getRoomListHeader, getSectionHeader } from "./utils"; + +test.describe("Room list custom sections", () => { + test.use({ + displayName: "Alice", + labsFlags: ["feature_new_room_list", "feature_room_list_sections"], + botCreateOpts: { + displayName: "BotBob", + autoAcceptInvites: true, + }, + }); + + /** + * Create a custom section via the header compose menu and dialog. + * @param page + * @param sectionName The name of the section to create + */ + async function createCustomSection(page: Page, sectionName: string): Promise { + const composeMenu = getRoomListHeader(page).getByRole("button", { name: "New conversation" }); + await composeMenu.click(); + await page.getByRole("menuitem", { name: "New section" }).click(); + + // Fill in the section name in the dialog + const dialog = page.getByRole("dialog", { name: "Create a section" }); + await expect(dialog).toBeVisible(); + await dialog.getByRole("textbox", { name: "Section name" }).fill(sectionName); + await dialog.getByRole("button", { name: "Create section" }).click(); + + // Wait for the dialog to close + await expect(dialog).not.toBeVisible(); + } + + test.beforeEach(async ({ page, app, user }) => { + // The notification toast is displayed above the search section + await app.closeNotificationToast(); + + // Focus the user menu to avoid hover decoration + await page.getByRole("button", { name: "User menu" }).focus(); + }); + + test.describe("Section creation", () => { + test("should create a custom section via the header compose menu", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + + await createCustomSection(page, "Work"); + + // The custom section header should be visible (even though it is empty) + await expect(getSectionHeader(page, "Work")).toBeVisible(); + // The Chats section should also be visible + await expect(getSectionHeader(page, "Chats")).toBeVisible(); + }); + + test("should show 'Section created' toast after creating a section", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + + await createCustomSection(page, "Personal"); + + // The "Section created" toast should appear + await expect(page.getByText("Section created")).toBeVisible(); + }); + + test("should create a custom section via the room option menu", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + + const roomList = getRoomList(page); + const roomItem = roomList.getByRole("option", { name: "Open room my room" }); + await expect(roomItem).toBeVisible(); + + // Open the More Options menu + await roomItem.hover(); + await roomItem.getByRole("button", { name: "More Options" }).click(); + + // Open the "Move to" submenu + await page.getByRole("menuitem", { name: "Move to" }).hover(); + + // Click on "New section" + await page.getByRole("menuitem", { name: "New section" }).click(); + + // Fill in the section name in the dialog + const dialog = page.getByRole("dialog", { name: "Create a section" }); + await expect(dialog).toBeVisible(); + await dialog.getByRole("textbox", { name: "Section name" }).fill("Projects"); + await dialog.getByRole("button", { name: "Create section" }).click(); + + // Wait for the dialog to close + await expect(dialog).not.toBeVisible(); + + // The custom section should be created + await expect(getSectionHeader(page, "Projects")).toBeVisible(); + }); + + test("should cancel section creation when dialog is dismissed", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + + const composeMenu = getRoomListHeader(page).getByRole("button", { name: "New conversation" }); + await composeMenu.click(); + await page.getByRole("menuitem", { name: "New section" }).click(); + + // The dialog should appear + const dialog = page.getByRole("dialog", { name: "Create a section" }); + await expect(dialog).toBeVisible(); + + // Cancel the dialog + await dialog.getByRole("button", { name: "Cancel" }).click(); + + // The dialog should close + await expect(dialog).not.toBeVisible(); + + // No custom section should be created - should remain a flat list + await expect(getSectionHeader(page, "Chats")).not.toBeVisible(); + }); + + test("should create multiple custom sections", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + + await createCustomSection(page, "Work"); + await createCustomSection(page, "Personal"); + + // Both custom sections should be visible + await expect(getSectionHeader(page, "Work")).toBeVisible(); + await expect(getSectionHeader(page, "Personal")).toBeVisible(); + await expect(getSectionHeader(page, "Chats")).toBeVisible(); + }); + }); + + test.describe("Custom section display", () => { + test("should show empty custom sections", async ({ page, app }) => { + // Create a room so the Chats section has something + await app.client.createRoom({ name: "my room" }); + + await createCustomSection(page, "Empty Section"); + + // The custom section should be visible even with no rooms + await expect(getSectionHeader(page, "Empty Section")).toBeVisible(); + // The room should still be in the Chats section + const roomList = getRoomList(page); + await expect(roomList.getByRole("row", { name: "Open room my room" })).toBeVisible(); + }); + + test("should display custom sections between Favourites and Chats", async ({ page, app }) => { + // Create a favourite room + const favouriteId = await app.client.createRoom({ name: "favourite room" }); + await app.client.evaluate(async (client, roomId) => { + await client.setRoomTag(roomId, "m.favourite"); + }, favouriteId); + + // Create a low priority room + const lowPrioId = await app.client.createRoom({ name: "low prio room" }); + await app.client.evaluate(async (client, roomId) => { + await client.setRoomTag(roomId, "m.lowpriority"); + }, lowPrioId); + + // Create a regular room + await app.client.createRoom({ name: "regular room" }); + + // Create a custom section + await createCustomSection(page, "Work"); + + // All section headers should be visible + await expect(getSectionHeader(page, "Favourites")).toBeVisible(); + await expect(getSectionHeader(page, "Work")).toBeVisible(); + // Should be expanded by default + await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "true"); + await expect(getSectionHeader(page, "Chats")).toBeVisible(); + await expect(getSectionHeader(page, "Low Priority")).toBeVisible(); + }); + }); + + test.describe("Adding a room to a custom section", () => { + /** + * Asserts a room is nested under a specific section using the treegrid aria-level hierarchy. + * Section header rows sit at aria-level=1; room rows nested within a section sit at aria-level=2. + * Verifies that the closest preceding aria-level=1 row is the expected section header. + */ + async function assertRoomInSection(page: Page, sectionName: string, roomName: string): Promise { + const roomList = getRoomList(page); + const roomRow = roomList.getByRole("row", { name: `Open room ${roomName}` }); + // Room row must be at aria-level=2 (i.e. inside a section) + await expect(roomRow).toHaveAttribute("aria-level", "2"); + // The closest preceding aria-level=1 row must be the expected section header. + // XPath preceding:: axis returns nodes before the context in document order; [1] picks the nearest one. + const closestSectionHeader = roomRow.locator(`xpath=preceding::*[@role="row" and @aria-level="1"][1]`); + await expect(closestSectionHeader).toContainText(sectionName); + } + + test("should add a room to a custom section via the More Options menu", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + await createCustomSection(page, "Work"); + + const roomList = getRoomList(page); + + // Room starts in Chats section (aria-level=2) + const roomItem = roomList.getByRole("row", { name: "Open room my room" }); + await expect(roomItem).toBeVisible(); + + // Open More Options and move to the Work section + await roomItem.hover(); + await roomItem.getByRole("button", { name: "More Options" }).click(); + await page.getByRole("menuitem", { name: "Move to" }).hover(); + await page.getByRole("menuitem", { name: "Work" }).click(); + + // Room should now be nested under the Work section header (aria-level=1 → aria-level=2) + await assertRoomInSection(page, "Work", "my room"); + }); + + test( + "should show 'Chat moved' toast when adding a room to a custom section", + { tag: "@screenshot" }, + async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + await createCustomSection(page, "Work"); + + const roomList = getRoomList(page); + const roomItem = roomList.getByRole("row", { name: "Open room my room" }); + + await roomItem.hover(); + await roomItem.getByRole("button", { name: "More Options" }).click(); + await page.getByRole("menuitem", { name: "Move to" }).hover(); + await page.getByRole("menuitem", { name: "Work" }).click(); + + // The "Chat moved" toast should appear + await expect(page.getByText("Chat moved")).toBeVisible(); + + // Remove focus outline from the room item before taking the screenshot + await page.getByRole("button", { name: "User menu" }).focus(); + + await expect(roomList).toMatchScreenshot("room-list-sections-chat-moved-toast.png"); + }, + ); + + test("should remove a room from a custom section when toggling the same section", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + await createCustomSection(page, "Work"); + + const roomList = getRoomList(page); + + // Move to Work section and verify placement via aria-level + let roomItem = roomList.getByRole("row", { name: "Open room my room" }); + await roomItem.hover(); + await roomItem.getByRole("button", { name: "More Options" }).click(); + await page.getByRole("menuitem", { name: "Move to" }).hover(); + await page.getByRole("menuitem", { name: "Work" }).click(); + + await assertRoomInSection(page, "Work", "my room"); + + // Toggle off by selecting the same section again + roomItem = roomList.getByRole("row", { name: "Open room my room" }); + await roomItem.hover(); + await roomItem.getByRole("button", { name: "More Options" }).click(); + await page.getByRole("menuitem", { name: "Move to" }).hover(); + await page.getByRole("menuitem", { name: "Work" }).click(); + + // Room is back in the Chats section + await assertRoomInSection(page, "Chats", "my room"); + }); + }); +}); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 99ffed3ff8..7356a2e6bf 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -6,10 +6,11 @@ */ import { type Visibility } from "matrix-js-sdk/src/matrix"; -import { type Locator, type Page } from "@playwright/test"; +import { type Page } from "@playwright/test"; import { expect, test } from "../../../element-web-test"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import { getFilterCollapseButton, getFilterExpandButton, getPrimaryFilters, getRoomOptionsMenu } from "./utils"; test.describe("Room list filters and sort", () => { test.use({ @@ -21,22 +22,6 @@ test.describe("Room list filters and sort", () => { labsFlags: ["feature_new_room_list"], }); - function getPrimaryFilters(page: Page): Locator { - return page.getByTestId("primary-filters"); - } - - function getRoomOptionsMenu(page: Page): Locator { - return page.getByRole("button", { name: "Room Options" }); - } - - function getFilterExpandButton(page: Page): Locator { - return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" }); - } - - function getFilterCollapseButton(page: Page): Locator { - return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" }); - } - /** * Get the room list * @param page diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts index 96e0ca8597..1ffa49429d 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-header.spec.ts @@ -6,21 +6,13 @@ */ import { test, expect } from "../../../element-web-test"; -import type { Page } from "@playwright/test"; +import { getHeaderSection } from "./utils"; test.describe("Header section of the room list", () => { test.use({ labsFlags: ["feature_new_room_list"], }); - /** - * Get the header section of the room list - * @param page - */ - function getHeaderSection(page: Page) { - return page.getByTestId("room-list-header"); - } - test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts index bc1387cbce..ad87eac89b 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts @@ -5,23 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -import { type Page } from "@playwright/test"; - import { test, expect } from "../../../element-web-test"; +import { getRoomListView } from "./utils"; test.describe("Room list panel", () => { test.use({ labsFlags: ["feature_new_room_list"], }); - /** - * Get the room list view - * @param page - */ - function getRoomListView(page: Page) { - return page.getByRole("navigation", { name: "Room list" }); - } - test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-search.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-search.spec.ts index 028503f622..37263d9b67 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-search.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-search.spec.ts @@ -5,23 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -import { type Page } from "@playwright/test"; - import { test, expect } from "../../../element-web-test"; +import { getSearchSection } from "./utils"; test.describe("Search section of the room list", () => { test.use({ labsFlags: ["feature_new_room_list"], }); - /** - * Get the search section of the room list - * @param page - */ - function getSearchSection(page: Page) { - return page.getByRole("search"); - } - test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts index 9bc9bbe2b0..80651c409f 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -import { type Locator, type Page } from "@playwright/test"; - import { expect, test } from "../../../element-web-test"; +import { getPrimaryFilters, getRoomList, getSectionHeader } from "./utils"; test.describe("Room list sections", () => { test.use({ @@ -19,34 +18,6 @@ test.describe("Room list sections", () => { }, }); - /** - * Get the room list - * @param page - */ - function getRoomList(page: Page): Locator { - return page.getByTestId("room-list"); - } - - /** - * Get the primary filters - * @param page - */ - function getPrimaryFilters(page: Page): Locator { - return page.getByTestId("primary-filters"); - } - - /** - * Get a section header toggle button by section name - * @param page - * @param sectionName The display name of the section (e.g. "Favourites", "Chats", "Low Priority") - * @param isUnread Whether to look for the unread version of the section header - */ - function getSectionHeader(page: Page, sectionName: string, isUnread = false): Locator { - return getRoomList(page).getByRole("gridcell", { - name: isUnread ? `Toggle ${sectionName} section with unread room(s)` : `Toggle ${sectionName} section`, - }); - } - test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 5907ad6d97..30dd461041 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -10,6 +10,7 @@ import { type Page } from "@playwright/test"; import { expect, test } from "../../../element-web-test"; import { type Bot } from "../../../pages/bot"; import { type ElementAppPage } from "../../../pages/ElementAppPage"; +import { getRoomList } from "./utils"; test.describe("Room list", () => { test.use({ @@ -20,14 +21,6 @@ test.describe("Room list", () => { }, }); - /** - * Get the room list - * @param page - */ - function getRoomList(page: Page) { - return page.getByTestId("room-list"); - } - test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); @@ -328,11 +321,11 @@ test.describe("Room list", () => { const roomListView = getRoomList(page); const videoRoom = roomListView.getByRole("option", { name: "video room" }); + await expect(videoRoom).toHaveAttribute("aria-selected", "true"); // wait for room list update // 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).toMatchScreenshot("room-list-item-video.png"); }); }); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/utils.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/utils.ts new file mode 100644 index 0000000000..523b268c80 --- /dev/null +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/utils.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type Locator, type Page } from "@playwright/test"; + +/** + * Get the room list + * @param page + */ +export function getRoomList(page: Page): Locator { + return page.getByTestId("room-list"); +} + +/** + * Get the room list header + * @param page + */ +export function getRoomListHeader(page: Page): Locator { + return page.getByTestId("room-list-header"); +} + +/** + * Get a section header toggle button by section name + * @param page + * @param sectionName The display name of the section + * @param isUnread Whether to look for the unread version of the section header + */ +export function getSectionHeader(page: Page, sectionName: string, isUnread = false): Locator { + return getRoomList(page).getByRole("gridcell", { + name: isUnread ? `Toggle ${sectionName} section with unread room(s)` : `Toggle ${sectionName} section`, + }); +} + +/** + * Get the primary filters container + * @param page + */ +export function getPrimaryFilters(page: Page): Locator { + return page.getByTestId("primary-filters"); +} + +/** + * Get the room options menu button in the room list header + * @param page + */ +export function getRoomOptionsMenu(page: Page): Locator { + return page.getByRole("button", { name: "Room Options" }); +} + +/** + * Get the filter list expand button in the room list header + * @param page + */ +export function getFilterExpandButton(page: Page): Locator { + return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" }); +} + +/** + * Get the filter list collapse button in the room list header + * @param page + */ +export function getFilterCollapseButton(page: Page): Locator { + return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" }); +} + +/** + * Get the header section of the room list + * @param page + */ +export function getHeaderSection(page: Page) { + return page.getByTestId("room-list-header"); +} + +/** + * Get the room list view + * @param page + */ +export function getRoomListView(page: Page) { + return page.getByRole("navigation", { name: "Room list" }); +} + +/** + * Get the search section of the room list + * @param page + */ +export function getSearchSection(page: Page) { + return page.getByRole("search"); +} diff --git a/apps/web/playwright/e2e/login/login-consent.spec.ts b/apps/web/playwright/e2e/login/login-consent.spec.ts index 19e095f50a..be8b9669a2 100644 --- a/apps/web/playwright/e2e/login/login-consent.spec.ts +++ b/apps/web/playwright/e2e/login/login-consent.spec.ts @@ -126,7 +126,7 @@ test.describe("Login", () => { await page.goto("/"); // Should give us the welcome page initially - await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible(); // Start the login process await expect(axe).toHaveNoViolations(); diff --git a/apps/web/playwright/e2e/messages/messages.spec.ts b/apps/web/playwright/e2e/messages/messages.spec.ts index 67af9edb42..2115dc3394 100644 --- a/apps/web/playwright/e2e/messages/messages.spec.ts +++ b/apps/web/playwright/e2e/messages/messages.spec.ts @@ -252,6 +252,7 @@ test.describe("Message url previews", () => { "og:title": "A simple site", "og:description": "And with a brief description", "og:image": mxc, + "og:image:alt": "The riot logo", }, }); }); diff --git a/apps/web/playwright/e2e/oidc/oidc-native.spec.ts b/apps/web/playwright/e2e/oidc/oidc-native.spec.ts index 81964c4e64..6d6c4612c1 100644 --- a/apps/web/playwright/e2e/oidc/oidc-native.spec.ts +++ b/apps/web/playwright/e2e/oidc/oidc-native.spec.ts @@ -148,7 +148,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { const userId = `alice_${testInfo.testId}`; await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!"); - await expect(page.getByText("Welcome")).toBeVisible(); + // richvdh: This takes several seconds to happen on a dev instance + await expect(page.getByText("Welcome")).toBeVisible({ timeout: 10000 }); // Log out await page.getByRole("button", { name: "User menu" }).click(); @@ -162,11 +163,14 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { // Log in again await page.goto("/#/login"); + await expect(page.getByText("Sign in")).toBeVisible(); await page.getByRole("button", { name: "Continue" }).click(); + await expect(page.getByText("Continue to Element?")).toBeVisible(); await page.getByRole("button", { name: "Continue" }).click(); // We should be being warned that we need to verify (but we can't) - await expect(page.getByText("Confirm your digital identity")).toBeVisible(); + // richvdh: Again, Element takes several seconds to load on a dev instance + await expect(page.getByText("Confirm your digital identity")).toBeVisible({ timeout: 10000 }); // And there should be no way to close this prompt await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible(); diff --git a/apps/web/playwright/e2e/read-receipts/read-receipts.spec.ts b/apps/web/playwright/e2e/read-receipts/read-receipts.spec.ts index b1142bae59..9e38cd4d71 100644 --- a/apps/web/playwright/e2e/read-receipts/read-receipts.spec.ts +++ b/apps/web/playwright/e2e/read-receipts/read-receipts.spec.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import type { JSHandle } from "@playwright/test"; -import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix"; +import type { MatrixEvent, ISendEventResponse, ReceiptType, RelationType } from "matrix-js-sdk/src/matrix"; import { expect } from "../../element-web-test"; import { type ElementAppPage } from "../../pages/ElementAppPage"; import { type Bot } from "../../pages/bot"; @@ -47,7 +47,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => { getId: () => eventResponse.event_id, threadRootId, getTs: () => 1, - isRelation: (relType) => { + isRelation: (relType: RelationType) => { return !relType || relType === "m.thread"; }, } as any as MatrixEvent; diff --git a/apps/web/playwright/e2e/room-directory/room-directory.spec.ts b/apps/web/playwright/e2e/room-directory/room-directory.spec.ts index 741fde3505..6eea5abce7 100644 --- a/apps/web/playwright/e2e/room-directory/room-directory.spec.ts +++ b/apps/web/playwright/e2e/room-directory/room-directory.spec.ts @@ -48,9 +48,9 @@ test.describe("Room Directory", () => { await app.closeDialog(); const resp = await bot.publicRooms({}); - expect(resp.total_room_count_estimate).toEqual(1); - expect(resp.chunk).toHaveLength(1); - expect(resp.chunk[0].room_id).toEqual(roomId); + expect(resp.total_room_count_estimate).toBeGreaterThanOrEqual(1); + expect(resp.chunk).toHaveLength(resp.total_room_count_estimate); + expect(resp.chunk.find((r) => r.room_id === roomId)).toBeTruthy(); }, ); diff --git a/apps/web/playwright/e2e/room/create-room.spec.ts b/apps/web/playwright/e2e/room/create-room.spec.ts index 554f972c7d..fd699dcec8 100644 --- a/apps/web/playwright/e2e/room/create-room.spec.ts +++ b/apps/web/playwright/e2e/room/create-room.spec.ts @@ -57,6 +57,9 @@ test.describe("Create Room", () => { await page.getByRole("button", { name: "Go" }).click(); + await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + await expect(page.getByText("Encryption enabled")).toBeVisible(); await expect(page.getByText("Send your first message to")).toBeVisible(); diff --git a/apps/web/playwright/e2e/room/room-status-bar.spec.ts b/apps/web/playwright/e2e/room/room-status-bar.spec.ts index 78d5c49a30..dc11333753 100644 --- a/apps/web/playwright/e2e/room/room-status-bar.spec.ts +++ b/apps/web/playwright/e2e/room/room-status-bar.spec.ts @@ -163,6 +163,10 @@ test.describe("Room Status Bar", () => { ).toBeVisible(); await other.getByRole("option", { name: "Alice" }).click(); await other.getByRole("button", { name: "Go" }).click(); + + await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + // Send a message to invite the bots const composer = app.getComposerField(); await composer.fill("Hello"); diff --git a/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts b/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts index 6c20af2d9a..a46e6eef36 100644 --- a/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts +++ b/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts @@ -33,7 +33,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Create the room and invite bob await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -72,7 +72,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Create the room and invite bob await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -115,7 +115,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Create the room and invite bob await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite and dismisses the warnings. await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -149,7 +149,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Alice creates the room and invite Bob. await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite. await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -214,7 +214,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Alice creates the room and invite Bob. await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite. await bobPage.getByRole("option", { name: "TestRoom" }).click(); diff --git a/apps/web/playwright/e2e/timeline/timeline.spec.ts b/apps/web/playwright/e2e/timeline/timeline.spec.ts index 8a1c51b45f..f468baf776 100644 --- a/apps/web/playwright/e2e/timeline/timeline.spec.ts +++ b/apps/web/playwright/e2e/timeline/timeline.spec.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import type { Locator, Page } from "@playwright/test"; -import type { ISendEventResponse, EventType, MsgType } from "matrix-js-sdk/src/matrix"; +import type { ISendEventResponse, EventType, MsgType, IContent } from "matrix-js-sdk/src/matrix"; import { test, expect } from "../../element-web-test"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import { Layout } from "../../../src/settings/enums/Layout"; @@ -50,11 +50,9 @@ const expectAvatar = async (cli: Client, e: Locator, avatarUrl: string): Promise }; const sendEvent = async (client: Client, roomId: string, html = false): Promise => { - const content = { + const content: IContent = { msgtype: "m.text" as MsgType, body: "Message", - format: undefined, - formatted_body: undefined, }; if (html) { content.format = "org.matrix.custom.html"; diff --git a/apps/web/playwright/e2e/utils.ts b/apps/web/playwright/e2e/utils.ts index 49561ed1b4..6c30bf1856 100644 --- a/apps/web/playwright/e2e/utils.ts +++ b/apps/web/playwright/e2e/utils.ts @@ -42,7 +42,7 @@ export async function waitForRoom( return new Promise((resolve) => { const room = matrixClient.getRoom(roomId); - if (window[predicateId](room)) { + if ((window)[predicateId](room)) { resolve(room); return; } @@ -50,7 +50,7 @@ export async function waitForRoom( function onEvent(ev: MatrixEvent) { if (ev.getRoomId() !== roomId) return; - if (window[predicateId](room)) { + if ((window)[predicateId](room)) { matrixClient.removeListener("event" as ClientEvent, onEvent); resolve(room); } diff --git a/apps/web/playwright/element-web-test.ts b/apps/web/playwright/element-web-test.ts index f7c6f5b8e2..62e6515f62 100644 --- a/apps/web/playwright/element-web-test.ts +++ b/apps/web/playwright/element-web-test.ts @@ -24,7 +24,6 @@ import type { IConfigOptions } from "../src/IConfigOptions"; import { type Credentials } from "./plugins/homeserver"; import { ElementAppPage } from "./pages/ElementAppPage"; import { Crypto } from "./pages/crypto"; -import { Toasts } from "./pages/toasts"; import { Bot, type CreateBotOpts } from "./pages/bot"; import { Webserver } from "./plugins/webserver"; import { type WorkerOptions, type Services, test as base } from "./services"; @@ -52,7 +51,6 @@ export interface TestFixtures extends BaseTestFixtures { crypto: Crypto; room?: { roomId: string }; - toasts: Toasts; uut?: Locator; // Unit Under Test, useful place to refer a prepared locator botCreateOpts: CreateBotOpts; bot: Bot; @@ -92,9 +90,6 @@ export const test = base.extend({ crypto: async ({ page, homeserver, request }, use) => { await use(new Crypto(page, homeserver, request)); }, - toasts: async ({ page }, use) => { - await use(new Toasts(page)); - }, botCreateOpts: {}, bot: async ({ page, homeserver, botCreateOpts, user }, use, testInfo) => { @@ -112,7 +107,7 @@ export const test = base.extend({ }, }); -interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions { +export interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions { includeDialogBackground?: boolean; showTooltips?: boolean; timeout?: number; diff --git a/apps/web/playwright/pages/ElementAppPage.ts b/apps/web/playwright/pages/ElementAppPage.ts index e5a1aab31c..1d28507010 100644 --- a/apps/web/playwright/pages/ElementAppPage.ts +++ b/apps/web/playwright/pages/ElementAppPage.ts @@ -233,15 +233,30 @@ export class ElementAppPage { * Open the room info panel, and use it to send an invite to the given user. * * @param userId - The user to invite to the room. + * @param options - Options object */ - public async inviteUserToCurrentRoom(userId: string): Promise { + public async inviteUserToCurrentRoom( + userId: string, + options?: { + /** If true, expect and acknowledge "Confirm inviting new users" page */ + confirmUnknownUser?: boolean; + }, + ): Promise { const rightPanel = await this.openRoomInfoPanel(); await rightPanel.getByRole("menuitem", { name: "Invite" }).click(); - const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input"); + const dialogLocator = this.page.getByRole("dialog"); + const input = dialogLocator.getByTestId("invite-dialog-input"); await input.fill(userId); await input.press("Enter"); - await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click(); + await dialogLocator.getByRole("button", { name: "Invite" }).click(); + + if (options?.confirmUnknownUser) { + await expect( + dialogLocator.getByRole("heading", { name: "Invite new contacts to this room?" }), + ).toBeVisible(); + await dialogLocator.getByRole("button", { name: "Invite" }).click(); + } } /** diff --git a/apps/web/playwright/pages/client.ts b/apps/web/playwright/pages/client.ts index 76f2733820..39275085f7 100644 --- a/apps/web/playwright/pages/client.ts +++ b/apps/web/playwright/pages/client.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { type JSHandle, type Page } from "@playwright/test"; -import { type PageFunctionOn } from "playwright-core/types/structs"; +import { type ElementHandle } from "playwright-core"; import { Network } from "./network"; import type { @@ -30,6 +30,34 @@ import type { import type { RoomMessageEventContent } from "matrix-js-sdk/src/types"; import { type CredentialsOptionalAccessToken } from "./bot"; +/** Types cribbed from playwright-core/types/structs as they are not importable */ +export type NoHandles = Arg extends JSHandle + ? never + : Arg extends object + ? { [Key in keyof Arg]: NoHandles } + : Arg; +export type Unboxed = + Arg extends ElementHandle + ? T + : Arg extends JSHandle + ? T + : Arg extends NoHandles + ? Arg + : Arg extends [infer A0] + ? [Unboxed] + : Arg extends [infer A0, infer A1] + ? [Unboxed, Unboxed] + : Arg extends [infer A0, infer A1, infer A2] + ? [Unboxed, Unboxed, Unboxed] + : Arg extends [infer A0, infer A1, infer A2, infer A3] + ? [Unboxed, Unboxed, Unboxed, Unboxed] + : Arg extends Array + ? Array> + : Arg extends object + ? { [Key in keyof Arg]: Unboxed } + : Arg; +export type PageFunctionOn = string | ((on: On, arg2: Unboxed) => R | Promise); + export class Client { public network: Network; protected client: JSHandle; diff --git a/apps/web/playwright/pages/toasts.ts b/apps/web/playwright/pages/toasts.ts deleted file mode 100644 index 80ee3c9f26..0000000000 --- a/apps/web/playwright/pages/toasts.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { type Page, expect, type Locator } from "@playwright/test"; - -export class Toasts { - public constructor(private readonly page: Page) {} - - /** - * Assert that a toast with the given title exists, and return it - * - * @param expectedTitle - Expected title of the toast - * @param timeout Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. - * @returns the Locator for the matching toast - */ - public async getToast(expectedTitle: string, timeout?: number): Promise { - const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first(); - await expect(toast).toBeVisible({ timeout }); - return toast; - } - - /** - * Assert that no toasts exist - */ - public async assertNoToasts(): Promise { - await expect(this.page.locator(".mx_Toast_toast")).not.toBeVisible(); - } - - /** - * Accept a toast with the given title, only works for the first toast in the stack - * - * @param expectedTitle - Expected title of the toast - */ - public async acceptToast(expectedTitle: string): Promise { - const toast = await this.getToast(expectedTitle); - await toast.locator('.mx_Toast_buttons button[data-kind="primary"]').click(); - } - - /** - * Reject a toast with the given title, only works for the first toast in the stack - * - * @param expectedTitle - Expected title of the toast - */ - public async rejectToast(expectedTitle: string): Promise { - const toast = await this.getToast(expectedTitle); - await toast.locator('.mx_Toast_buttons button[data-kind="secondary"]').click(); - } -} diff --git a/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png b/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png index dcdd2cb0f7..904e23c35a 100644 Binary files a/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png and b/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 3be7831755..0a99a4cf86 100644 Binary files a/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png b/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png index 78ff359a99..f2e5ab1350 100644 Binary files a/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png and b/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png b/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png index b84cdd4e8f..adea2eac4e 100644 Binary files a/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png and b/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png b/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png index 7aeac7a9a9..01ad3384c1 100644 Binary files a/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png and b/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png differ diff --git a/apps/web/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png b/apps/web/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png index f764311250..771157a11a 100644 Binary files a/apps/web/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png and b/apps/web/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png differ diff --git a/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-chat-with-new-contact-linux.png b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-chat-with-new-contact-linux.png new file mode 100644 index 0000000000..4cdb24a173 Binary files /dev/null and b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-chat-with-new-contact-linux.png differ diff --git a/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-invite-new-contact-linux.png b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-invite-new-contact-linux.png new file mode 100644 index 0000000000..cf961af67a Binary files /dev/null and b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-invite-new-contact-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-custom-sections.spec.ts/room-list-sections-chat-moved-toast-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-custom-sections.spec.ts/room-list-sections-chat-moved-toast-linux.png new file mode 100644 index 0000000000..4da4571cbc Binary files /dev/null and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-custom-sections.spec.ts/room-list-sections-chat-moved-toast-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-collapsed-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-collapsed-linux.png index 6ebcef8532..1f5246ceaf 100644 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-collapsed-linux.png and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-collapsed-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-linux.png index bd61342d60..28f00a9614 100644 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-linux.png and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-sections.spec.ts/room-list-sections-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index d651340ace..77345fe6fe 100644 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-basic-linux.png b/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-basic-linux.png index 6d004c8da2..5651405d53 100644 Binary files a/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-basic-linux.png and b/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-basic-linux.png differ diff --git a/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-with-thumb-linux.png b/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-with-thumb-linux.png index aa36fca9e9..fa149ef399 100644 Binary files a/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-with-thumb-linux.png and b/apps/web/playwright/snapshots/messages/messages.spec.ts/preview-with-thumb-linux.png differ diff --git a/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index ada2b8a60e..7fb7e011bb 100644 Binary files a/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index 98de99bf98..15da4c7c02 100644 Binary files a/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png index a42cb394d7..a6b0afc57d 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png index a9f601e822..2fa939ff85 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png index be1a42d63f..f340588e3e 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png index 9108514219..5700565252 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png index 87d1cc85a4..bd69b42ab1 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index 48625def3a..e5b81c1973 100644 Binary files a/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/apps/web/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/notifications/notifications-settings-2-tab.spec.ts/standard-notifications-2-settings-linux.png b/apps/web/playwright/snapshots/settings/notifications/notifications-settings-2-tab.spec.ts/standard-notifications-2-settings-linux.png index 8487b5a381..79a9efe5cf 100644 Binary files a/apps/web/playwright/snapshots/settings/notifications/notifications-settings-2-tab.spec.ts/standard-notifications-2-settings-linux.png and b/apps/web/playwright/snapshots/settings/notifications/notifications-settings-2-tab.spec.ts/standard-notifications-2-settings-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/notifications/notifications-settings-tab.spec.ts/standard-notification-settings-linux.png b/apps/web/playwright/snapshots/settings/notifications/notifications-settings-tab.spec.ts/standard-notification-settings-linux.png index 21198098a4..efec129b55 100644 Binary files a/apps/web/playwright/snapshots/settings/notifications/notifications-settings-tab.spec.ts/standard-notification-settings-linux.png and b/apps/web/playwright/snapshots/settings/notifications/notifications-settings-tab.spec.ts/standard-notification-settings-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 2a998720e9..2519cee3ef 100644 Binary files a/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/security-user-settings-tab.spec.ts/security-settings-tab-linux.png b/apps/web/playwright/snapshots/settings/security-user-settings-tab.spec.ts/security-settings-tab-linux.png index 5248968436..c12d1a8808 100644 Binary files a/apps/web/playwright/snapshots/settings/security-user-settings-tab.spec.ts/security-settings-tab-linux.png and b/apps/web/playwright/snapshots/settings/security-user-settings-tab.spec.ts/security-settings-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/spaces/spaces.spec.ts/space-visibility-settings-linux.png b/apps/web/playwright/snapshots/spaces/spaces.spec.ts/space-visibility-settings-linux.png index a1d376d62f..1551195fd5 100644 Binary files a/apps/web/playwright/snapshots/spaces/spaces.spec.ts/space-visibility-settings-linux.png and b/apps/web/playwright/snapshots/spaces/spaces.spec.ts/space-visibility-settings-linux.png differ diff --git a/apps/web/playwright/testcontainers/dendrite.ts b/apps/web/playwright/testcontainers/dendrite.ts index 68a356027e..03e34856a9 100644 --- a/apps/web/playwright/testcontainers/dendrite.ts +++ b/apps/web/playwright/testcontainers/dendrite.ts @@ -21,7 +21,7 @@ const DEFAULT_CONFIG = { global: { server_name: "localhost", private_key: "matrix_key.pem", - old_private_keys: null, + old_private_keys: null as any, key_validity_period: "168h0m0s", cache: { max_size_estimated: "1gb", @@ -47,7 +47,7 @@ const DEFAULT_CONFIG = { room_name: "Server Alerts", }, jetstream: { - addresses: null, + addresses: null as any, disable_tls_validation: false, storage_path: "./", topic_prefix: "Dendrite", @@ -67,7 +67,7 @@ const DEFAULT_CONFIG = { }, app_service_api: { disable_tls_validation: false, - config_files: null, + config_files: null as any, }, client_api: { registration_disabled: false, @@ -79,14 +79,14 @@ const DEFAULT_CONFIG = { recaptcha_bypass_secret: "", turn: { turn_user_lifetime: "5m", - turn_uris: null, + turn_uris: null as any, turn_shared_secret: "", }, rate_limiting: { enabled: true, threshold: 20, cooloff_ms: 500, - exempt_user_ids: null, + exempt_user_ids: null as any, }, }, federation_api: { @@ -140,7 +140,7 @@ const DEFAULT_CONFIG = { }, }, mscs: { - mscs: null, + mscs: null as any, database: { connection_string: "file:dendrite-msc.db", }, @@ -157,7 +157,7 @@ const DEFAULT_CONFIG = { }, user_api: { bcrypt_cost: 10, - auto_join_rooms: null, + auto_join_rooms: null as any, account_database: { connection_string: "file:dendrite-userapi.db", }, @@ -183,12 +183,12 @@ const DEFAULT_CONFIG = { serviceName: "", disabled: false, rpc_metrics: false, - tags: [], - sampler: null, - reporter: null, - headers: null, - baggage_restrictions: null, - throttler: null, + tags: [] as any[], + sampler: null as any, + reporter: null as any, + headers: null as any, + baggage_restrictions: null as any, + throttler: null as any, }, }, logging: [ diff --git a/apps/web/playwright/testcontainers/mas.ts b/apps/web/playwright/testcontainers/mas.ts index 45752fc10e..85b727189f 100644 --- a/apps/web/playwright/testcontainers/mas.ts +++ b/apps/web/playwright/testcontainers/mas.ts @@ -11,7 +11,7 @@ import { } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js"; const DOCKER_IMAGE = - "ghcr.io/element-hq/matrix-authentication-service:main@sha256:f54c2214354ec3294694a525523debb5f38c8580c1a5afc8cdec0f8372374ef3"; + "ghcr.io/element-hq/matrix-authentication-service:main@sha256:034500c4797287bfcdc4d13304e89ac65ce44a0fa33664836aea6e42c33535fb"; /** * MatrixAuthenticationServiceContainer which freezes the docker digest to diff --git a/apps/web/playwright/testcontainers/synapse.ts b/apps/web/playwright/testcontainers/synapse.ts index f67d51659a..5fb1c27bfd 100644 --- a/apps/web/playwright/testcontainers/synapse.ts +++ b/apps/web/playwright/testcontainers/synapse.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js"; const DOCKER_IMAGE = - "ghcr.io/element-hq/synapse:develop@sha256:73fe964d854412cd905f4f0e668b2a9d10edc86891036e03656714de0311f6f6"; + "ghcr.io/element-hq/synapse:develop@sha256:b2fec2c9460f5b297a3a4ce78037902590240a1978301ed1d4bc97918c451041"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/apps/web/playwright/tsconfig.json b/apps/web/playwright/tsconfig.json index 968d903fa4..4f3f437a9a 100644 --- a/apps/web/playwright/tsconfig.json +++ b/apps/web/playwright/tsconfig.json @@ -2,12 +2,14 @@ "compilerOptions": { "target": "es2022", "jsx": "react", - "lib": ["ESNext", "es2022", "dom", "dom.iterable"], + "lib": ["es2024", "dom", "dom.iterable"], "resolveJsonModule": true, "esModuleInterop": true, - "moduleResolution": "node", - "module": "es2022", + "moduleResolution": "bundler", + "module": "ESNext", "allowImportingTsExtensions": true, + "strictNullChecks": false, + "noImplicitAny": false, "types": ["node"] }, "include": [ diff --git a/apps/web/project.json b/apps/web/project.json index 95f9a6828d..abb5fc4757 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -44,15 +44,23 @@ "parallel": false, "cwd": "apps/web" }, - "dependsOn": ["^build"] + "dependsOn": ["^build", "^build:playwright"] }, "test:unit": { - "executor": "@nx/jest:jest", - "options": { - "jestConfig": "{projectRoot}/jest.config.ts", - "cwd": "apps/web" - }, + // We avoid the jest executor because it doesn't seem to give any benefit, and it mangles the summary of failing tests. + "command": "jest", + "options": { "cwd": "apps/web" }, "dependsOn": ["^build"] + }, + "test:playwright": { + "command": "playwright test", + "options": { "cwd": "apps/web" }, + "dependsOn": ["^build:playwright"] + }, + "test:playwright:screenshots": { + "command": "playwright-screenshots playwright test --update-snapshots --project=Chrome --grep @screenshot", + "options": { "cwd": "apps/web" }, + "dependsOn": ["^build:playwright"] } } } diff --git a/apps/web/res/css/_common.pcss b/apps/web/res/css/_common.pcss index 4cadbe71c6..d55cb07606 100644 --- a/apps/web/res/css/_common.pcss +++ b/apps/web/res/css/_common.pcss @@ -598,6 +598,7 @@ legend { .mx_AccessSecretStorageDialog button, .mx_InviteDialog_section button, .mx_InviteDialog_editor button, + .mx_UnknownIdentityUsersWarningDialog button, [class|="maplibregl"] ), .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton), @@ -625,7 +626,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ):last-child { margin-right: 0px; } @@ -641,7 +643,8 @@ legend { .mx_ShareDialog button, .mx_EncryptionUserSettingsTab button, .mx_InviteDialog_section button, - .mx_InviteDialog_editor button + .mx_InviteDialog_editor button, + .mx_UnknownIdentityUsersWarningDialog button ):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus, @@ -659,7 +662,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); @@ -678,7 +682,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); @@ -701,7 +706,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):disabled, @@ -834,18 +840,6 @@ legend { } } -@define-mixin ButtonResetDefault { - appearance: none; - background: none; - border: none; - padding: 0; - margin: 0; - font-size: inherit; - font-family: inherit; - line-height: inherit; - cursor: pointer; -} - @define-mixin LegacyCallButton { box-sizing: border-box; font-weight: var(--cpd-font-weight-semibold); diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index 1197cbabe6..bac006d7f5 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -68,8 +68,8 @@ @import "./structures/_LeftPanel.pcss"; @import "./structures/_MainSplit.pcss"; @import "./structures/_MatrixChat.pcss"; -@import "./structures/_MessagePanel.pcss"; @import "./structures/_NonUrgentToastContainer.pcss"; +@import "./structures/_PictureInPictureDragger.pcss"; @import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_RightPanel.pcss"; @import "./structures/_RoomSearch.pcss"; @@ -105,6 +105,7 @@ @import "./views/auth/_AuthPage.pcss"; @import "./views/auth/_CompleteSecurityBody.pcss"; @import "./views/auth/_CountryDropdown.pcss"; +@import "./views/auth/_DefaultWelcome.pcss"; @import "./views/auth/_InteractiveAuthEntryComponents.pcss"; @import "./views/auth/_LanguageSelector.pcss"; @import "./views/auth/_LoginWithQR.pcss"; @@ -128,6 +129,7 @@ @import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss"; @import "./views/dialogs/_ConfirmUserActionDialog.pcss"; @import "./views/dialogs/_CreateRoomDialog.pcss"; +@import "./views/dialogs/_CreateSectionDialog.pcss"; @import "./views/dialogs/_CreateSubspaceDialog.pcss"; @import "./views/dialogs/_Crypto.pcss"; @import "./views/dialogs/_DeactivateAccountDialog.pcss"; @@ -169,6 +171,7 @@ @import "./views/dialogs/_UserSettingsDialog.pcss"; @import "./views/dialogs/_VerifyEMailDialog.pcss"; @import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss"; +@import "./views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss"; @import "./views/dialogs/security/_AccessSecretStorageDialog.pcss"; @import "./views/dialogs/security/_CreateCrossSigningDialog.pcss"; @import "./views/dialogs/security/_CreateSecretStorageDialog.pcss"; @@ -375,7 +378,6 @@ @import "./views/voip/_DialPad.pcss"; @import "./views/voip/_DialPadContextMenu.pcss"; @import "./views/voip/_DialPadModal.pcss"; -@import "./views/voip/_LegacyCallPreview.pcss"; @import "./views/voip/_LegacyCallView.pcss"; @import "./views/voip/_LegacyCallViewForRoom.pcss"; @import "./views/voip/_LegacyCallViewHeader.pcss"; diff --git a/apps/web/res/css/components/views/location/_ShareDialogButtons.pcss b/apps/web/res/css/components/views/location/_ShareDialogButtons.pcss index d0c419accb..1c1d379f89 100644 --- a/apps/web/res/css/components/views/location/_ShareDialogButtons.pcss +++ b/apps/web/res/css/components/views/location/_ShareDialogButtons.pcss @@ -13,8 +13,7 @@ Please see LICENSE files in the repository root for full details. top: 0; } -.mx_ShareDialogButtons_button { - @mixin ButtonResetDefault; +button.mx_ShareDialogButtons_button { height: 24px; width: 24px; border-radius: 50%; diff --git a/apps/web/res/css/structures/_MessagePanel.pcss b/apps/web/res/css/structures/_MessagePanel.pcss deleted file mode 100644 index fb2830bce7..0000000000 --- a/apps/web/res/css/structures/_MessagePanel.pcss +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_MessagePanel_myReadMarker { - height: 0; - margin: 0; - padding: 0; - border: 0; - - hr { - border-top: solid 1px $accent; - border-bottom: solid 1px $accent; - margin-top: 0; - position: relative; - top: -1px; - z-index: 1; - will-change: width; - transition: - width 400ms easeinsine 1s, - opacity 400ms easeinsine 1s; - width: 99%; - opacity: 1; - } -} diff --git a/apps/web/res/css/structures/_PictureInPictureDragger.pcss b/apps/web/res/css/structures/_PictureInPictureDragger.pcss new file mode 100644 index 0000000000..d6effd3b20 --- /dev/null +++ b/apps/web/res/css/structures/_PictureInPictureDragger.pcss @@ -0,0 +1,20 @@ +/* +Copyright 2026 Element Creations 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. +*/ + +.mx_PictureInPictureDragger { + cursor: grab; + user-select: none; + left: 0; + position: fixed; + top: 0; + /* Display above any widget elements */ + z-index: 102; +} + +.mx_PictureInPictureDragger:active { + cursor: grabbing; +} diff --git a/apps/web/res/css/views/auth/_DefaultWelcome.pcss b/apps/web/res/css/views/auth/_DefaultWelcome.pcss new file mode 100644 index 0000000000..08183e77b1 --- /dev/null +++ b/apps/web/res/css/views/auth/_DefaultWelcome.pcss @@ -0,0 +1,43 @@ +/* +Copyright 2026 Element Creations 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. +*/ + +.mx_DefaultWelcome { + text-align: center; + + .mx_DefaultWelcome_logo img { + height: 48px; + aspect-ratio: auto; + display: block; + margin: 0 auto; + } + + h1 { + margin: var(--cpd-space-4x) 0 var(--cpd-space-2x); + } + + p { + color: var(--cpd-color-text-secondary); + margin-top: var(--cpd-space-2x); + } + + .mx_DefaultWelcome_buttons { + margin: var(--cpd-space-6x) 0 var(--cpd-space-1x); + padding-bottom: var(--cpd-space-4x); + border-bottom: 1px solid var(--cpd-color-separator-primary); + + a { + width: 380px; + margin-bottom: var(--cpd-space-4x); + } + } +} + +.mx_WelcomePage_registrationDisabled { + .mx_DefaultWelcome_buttons_register { + display: none; + } +} diff --git a/apps/web/res/css/views/auth/_Welcome.pcss b/apps/web/res/css/views/auth/_Welcome.pcss index 50a91aa767..12598f3293 100644 --- a/apps/web/res/css/views/auth/_Welcome.pcss +++ b/apps/web/res/css/views/auth/_Welcome.pcss @@ -9,6 +9,10 @@ Please see LICENSE files in the repository root for full details. display: flex; flex-direction: column; align-items: center; + background-color: var(--cpd-color-bg-canvas-default); + box-sizing: border-box; + padding: var(--cpd-space-11x) var(--cpd-space-12x) var(--cpd-space-4x); + &.mx_WelcomePage_registrationDisabled { .mx_ButtonCreateAccount { display: none; @@ -18,7 +22,7 @@ Please see LICENSE files in the repository root for full details. .mx_Welcome .mx_AuthBody_language { width: 160px; - margin-bottom: 10px; + margin: var(--cpd-space-1x) 0; } /* Invert image colours in dark mode. */ diff --git a/apps/web/res/css/views/dialogs/_CreateSectionDialog.pcss b/apps/web/res/css/views/dialogs/_CreateSectionDialog.pcss new file mode 100644 index 0000000000..7c941be39f --- /dev/null +++ b/apps/web/res/css/views/dialogs/_CreateSectionDialog.pcss @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +.mx_CreateSectionDialog { + color: var(--cpd-color-text-primary); + + &.mx_Dialog_fixedWidth { + /* 576px coming from Figma and remove external padding */ + max-width: calc(576px - var(--cpd-space-20x)); + } + + .mx_CreateSectionDialog_content { + min-height: 346px; + } + + .mx_CreateSectionDialog_form { + width: 100%; + } +} diff --git a/apps/web/res/css/views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss b/apps/web/res/css/views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss new file mode 100644 index 0000000000..085d9dfa5d --- /dev/null +++ b/apps/web/res/css/views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss @@ -0,0 +1,45 @@ +/* + Copyright 2026 Element Creations 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. +*/ + +.mx_UnknownIdentityUsersWarningDialog { + display: flex; + flex-direction: column; + height: 600px; /* Consistency with InviteDialog */ +} + +.mx_UnknownIdentityUsersWarningDialog_headerContainer { + /* Centre the PageHeader component horizontally */ + display: flex; + justify-content: center; + + /* Styling for the regular text inside the header */ + font: var(--cpd-font-body-lg-regular); + + /* Space before the list */ + padding-bottom: var(--cpd-space-6x); +} + +.mx_UnknownIdentityUsersWarningDialog_userList { + width: 100%; + overflow: auto; + + /* Fill available vertical space, but don't allow it to shrink to less than 60px (about the height of a single tile) */ + flex: 1 0 60px; + + /* Remove browser default ul padding/margin */ + padding: 0; + margin: 0; +} + +.mx_UnknownIdentityUsersWarningDialog_buttons { + display: flex; + gap: var(--cpd-space-4x); + + > button { + flex: 1; + } +} diff --git a/apps/web/res/css/views/elements/_AccessibleButton.pcss b/apps/web/res/css/views/elements/_AccessibleButton.pcss index fafe75c642..e79a858d49 100644 --- a/apps/web/res/css/views/elements/_AccessibleButton.pcss +++ b/apps/web/res/css/views/elements/_AccessibleButton.pcss @@ -9,6 +9,19 @@ Please see LICENSE files in the repository root for full details. .mx_AccessibleButton { cursor: pointer; + &:where(button) { + /* Clear default button styling */ + appearance: none; + background: none; + border: none; + padding: 0; + margin: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; + box-sizing: content-box; + } + &.mx_AccessibleButton_disabled { cursor: not-allowed; diff --git a/apps/web/res/css/views/elements/_CopyableText.pcss b/apps/web/res/css/views/elements/_CopyableText.pcss index 6219055085..e5cf1c51da 100644 --- a/apps/web/res/css/views/elements/_CopyableText.pcss +++ b/apps/web/res/css/views/elements/_CopyableText.pcss @@ -28,7 +28,6 @@ Please see LICENSE files in the repository root for full details. /* using em here to adapt to the local font size */ width: 1em; height: 1em; - cursor: pointer; padding-left: 12px; padding-right: 10px; display: block; diff --git a/apps/web/res/css/views/rooms/_EventTile.pcss b/apps/web/res/css/views/rooms/_EventTile.pcss index 5bea427961..63d1bdaee2 100644 --- a/apps/web/res/css/views/rooms/_EventTile.pcss +++ b/apps/web/res/css/views/rooms/_EventTile.pcss @@ -1378,6 +1378,10 @@ $left-gutter: 64px; display: flex; } +.mx_EventTile_annotatedInline { + display: inline-flex; +} + .mx_EventTile_footer { display: flex; gap: var(--cpd-space-2x); diff --git a/apps/web/res/css/views/settings/_NotificationSettings2.pcss b/apps/web/res/css/views/settings/_NotificationSettings2.pcss index db439f0706..8a41322615 100644 --- a/apps/web/res/css/views/settings/_NotificationSettings2.pcss +++ b/apps/web/res/css/views/settings/_NotificationSettings2.pcss @@ -12,12 +12,6 @@ Please see LICENSE files in the repository root for full details. gap: 32px; display: flex; flex-direction: column; - - > form { - gap: 32px; - display: flex; - flex-direction: column; - } } .mx_SettingsSubsection_description { diff --git a/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss b/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss index 92a392950f..c92e454e05 100644 --- a/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss +++ b/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss @@ -14,12 +14,13 @@ Please see LICENSE files in the repository root for full details. color: $links; } - form:not(.mx_EncryptionUserSettingsTab form) { + form { display: flex; flex-direction: column; - gap: $spacing-8; + gap: var(--cpd-space-3x); flex-grow: 1; } + // never want full width buttons // event when other content is 100% width .mx_AccessibleButton { diff --git a/apps/web/res/css/views/voip/_LegacyCallPreview.pcss b/apps/web/res/css/views/voip/_LegacyCallPreview.pcss deleted file mode 100644 index 3a8bf5af9f..0000000000 --- a/apps/web/res/css/views/voip/_LegacyCallPreview.pcss +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021 Šimon Brandner - -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. -*/ - -.mx_LegacyCallPreview { - align-items: flex-end; - display: flex; - flex-direction: column; - gap: $spacing-16; - left: 0; - position: fixed; - top: 0; - /* Display above any widget elements */ - z-index: 102; - - .mx_VideoFeed_remote.mx_VideoFeed_voice { - min-height: 150px; - } - - .mx_VideoFeed_local { - border-radius: 8px; - overflow: hidden; - } -} diff --git a/apps/web/res/welcome.html b/apps/web/res/welcome.html deleted file mode 100644 index f1cf3911f4..0000000000 --- a/apps/web/res/welcome.html +++ /dev/null @@ -1,191 +0,0 @@ - - -
- - - -

_t("welcome_to_element")

- -

_t("powered_by_matrix_with_logo")

- -
diff --git a/apps/web/res/welcome/images/icon-create-account.svg b/apps/web/res/welcome/images/icon-create-account.svg deleted file mode 100644 index 7bbef7f632..0000000000 --- a/apps/web/res/welcome/images/icon-create-account.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/web/res/welcome/images/icon-help.svg b/apps/web/res/welcome/images/icon-help.svg deleted file mode 100644 index dc96f8e0cf..0000000000 --- a/apps/web/res/welcome/images/icon-help.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/apps/web/res/welcome/images/icon-room-directory.svg b/apps/web/res/welcome/images/icon-room-directory.svg deleted file mode 100644 index 3786ce1153..0000000000 --- a/apps/web/res/welcome/images/icon-room-directory.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/web/res/welcome/images/icon-sign-in.svg b/apps/web/res/welcome/images/icon-sign-in.svg deleted file mode 100644 index 9bc2fefa3f..0000000000 --- a/apps/web/res/welcome/images/icon-sign-in.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/web/src/@types/common.ts b/apps/web/src/@types/common.ts index 4c8c707c4a..bfb4f64c16 100644 --- a/apps/web/src/@types/common.ts +++ b/apps/web/src/@types/common.ts @@ -52,3 +52,13 @@ export type AtLeastOne }> = Partial & U[k export type Assignable = { [Key in keyof Object]: Object[Key] extends Item ? Key : never; }[keyof Object]; + +/** + * Like `Partial` but for applied to all nested objects. + * Based on https://dev.to/perennialautodidact/adventures-in-typescript-deeppartial-2f2a + */ +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; diff --git a/apps/web/src/@types/css.d.ts b/apps/web/src/@types/css.d.ts new file mode 100644 index 0000000000..a37139aa72 --- /dev/null +++ b/apps/web/src/@types/css.d.ts @@ -0,0 +1,8 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +declare module "*.css"; diff --git a/apps/web/src/@types/global.d.ts b/apps/web/src/@types/global.d.ts index f6b9ff81f9..99cd6d4ba4 100644 --- a/apps/web/src/@types/global.d.ts +++ b/apps/web/src/@types/global.d.ts @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. // eslint-disable-next-line no-restricted-imports import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first -import "@types/modernizr"; import type { ModuleLoader } from "@element-hq/element-web-module-api"; import type { logger } from "matrix-js-sdk/src/logger"; @@ -186,14 +185,6 @@ declare global { readonly port: MessagePort; } - /** - * In future, browsers will support focusVisible option. - * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible - */ - interface FocusOptions { - focusVisible: boolean; - } - // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 function registerProcessor( name: string, diff --git a/apps/web/src/@types/pcss.d.ts b/apps/web/src/@types/pcss.d.ts new file mode 100644 index 0000000000..64680d80cf --- /dev/null +++ b/apps/web/src/@types/pcss.d.ts @@ -0,0 +1,8 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +declare module "*.pcss"; diff --git a/apps/web/src/IConfigOptions.ts b/apps/web/src/IConfigOptions.ts index fd39e2f8df..a3a6a32c91 100644 --- a/apps/web/src/IConfigOptions.ts +++ b/apps/web/src/IConfigOptions.ts @@ -52,8 +52,9 @@ export interface IConfigOptions { disable_3pid_login?: boolean; brand: string; - branding?: { - welcome_background_url?: string | string[]; // chosen at random if array + branding: { + welcome_background_url: string | string[]; // chosen at random if array + logo_link_url: string; auth_header_logo_url?: string; auth_footer_links?: { text: string; url: string }[]; }; diff --git a/apps/web/src/Lifecycle.ts b/apps/web/src/Lifecycle.ts index 78c6cc2de6..32d113adc8 100644 --- a/apps/web/src/Lifecycle.ts +++ b/apps/web/src/Lifecycle.ts @@ -18,7 +18,6 @@ import { decodeBase64, } from "matrix-js-sdk/src/matrix"; import { type AESEncryptedSecretStoragePayload } from "matrix-js-sdk/src/types"; -import { type QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { type IMatrixClientCreds, MatrixClientPeg, type MatrixClientPegAssignOpts } from "./MatrixClientPeg"; @@ -81,6 +80,7 @@ import { } from "./utils/tokens/tokens"; import { TokenRefresher } from "./utils/oidc/TokenRefresher"; import { checkBrowserSupport } from "./SupportedBrowser"; +import { type URLParams } from "./vector/url_utils.ts"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -148,7 +148,7 @@ interface ILoadSessionOpts { guestIsUrl?: string; ignoreGuest?: boolean; defaultDeviceDisplayName?: string; - fragmentQueryParams?: QueryDict; + urlParams?: URLParams; abortSignal?: AbortSignal; } @@ -187,7 +187,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise let enableGuest = opts.enableGuest || false; const guestHsUrl = opts.guestHsUrl; const guestIsUrl = opts.guestIsUrl; - const fragmentQueryParams = opts.fragmentQueryParams || {}; + const urlParams = opts.urlParams; const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; if (enableGuest && !guestHsUrl) { @@ -195,12 +195,12 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise enableGuest = false; } - if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) { + if (enableGuest && guestHsUrl && urlParams?.guest?.guest_user_id && urlParams?.guest?.guest_access_token) { logger.log("Using guest access credentials"); await doSetLoggedIn( { - userId: fragmentQueryParams.guest_user_id as string, - accessToken: fragmentQueryParams.guest_access_token as string, + userId: urlParams.guest.guest_user_id, + accessToken: urlParams.guest.guest_access_token, homeserverUrl: guestHsUrl, identityServerUrl: guestIsUrl, guest: true, @@ -264,38 +264,43 @@ export async function getStoredSessionOwner(): Promise<[string, boolean] | [null * If query string includes OIDC authorization code flow parameters attempt to login using oidc flow * Else, we may be returning from SSO - attempt token login * - * @param {Object} queryParams string->string map of the - * query-parameters extracted from the real query-string of the starting - * URI. + * @param urlParams the parameters read in at app load time from the url * - * @param {string} defaultDeviceDisplayName - * @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again" + * @param defaultDeviceDisplayName + * @param fragmentAfterLogin path to go to after a successful login, only used for "Try again" * - * @returns {Promise} promise which resolves to true if we completed the delegated auth login + * @returns promise which resolves to true if we completed the delegated auth login * else false */ export async function attemptDelegatedAuthLogin( - queryParams: QueryDict, + urlParams: URLParams, defaultDeviceDisplayName?: string, fragmentAfterLogin?: string, ): Promise { - if (queryParams.code && queryParams.state) { - console.log("We have OIDC params - attempting OIDC login"); - return attemptOidcNativeLogin(queryParams); + if (urlParams.oidc_fragment) { + return attemptOidcNativeLogin(urlParams.oidc_fragment, "fragment"); + } else if (urlParams.oidc_query) { + return attemptOidcNativeLogin(urlParams.oidc_query, "query"); } - return attemptTokenLogin(queryParams, defaultDeviceDisplayName, fragmentAfterLogin); + return attemptTokenLogin(urlParams["legacy_sso"], defaultDeviceDisplayName, fragmentAfterLogin); } /** * Attempt to login by completing OIDC authorization code flow - * @param queryParams string->string map of the query-parameters extracted from the real query-string of the starting URI. - * @returns Promise that resolves to true when login succceeded, else false + * @param urlParams subset of app-load url parameters relating to oidc auth + * @param responseMode - the response_mode used in the auth request + * @returns Promise that resolves to true when login succeeded, else false */ -async function attemptOidcNativeLogin(queryParams: QueryDict): Promise { +async function attemptOidcNativeLogin( + urlParams: NonNullable, + responseMode: "fragment" | "query", +): Promise { + console.log("We have OIDC params - attempting OIDC login"); + try { const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idToken, clientId, issuer } = - await completeOidcLogin(queryParams); + await completeOidcLogin(urlParams, responseMode); const { user_id: userId, @@ -354,22 +359,20 @@ async function getUserIdFromAccessToken( } /** - * @param {QueryDict} queryParams string->string map of the - * query-parameters extracted from the real query-string of the starting - * URI. + @param urlParams subset of app-load url parameters relating to legacy sso auth * - * @param {string} defaultDeviceDisplayName - * @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again" + * @param defaultDeviceDisplayName + * @param fragmentAfterLogin path to go to after a successful login, only used for "Try again" * - * @returns {Promise} promise which resolves to true if we completed the token + * @returns promise which resolves to true if we completed the token * login, else false */ export function attemptTokenLogin( - queryParams: QueryDict, + urlParams: URLParams["legacy_sso"], defaultDeviceDisplayName?: string, fragmentAfterLogin?: string, ): Promise { - if (!queryParams.loginToken) { + if (!urlParams?.loginToken) { return Promise.resolve(false); } @@ -384,7 +387,7 @@ export function attemptTokenLogin( } return sendLoginRequest(homeserver, identityServer, "m.login.token", { - token: queryParams.loginToken as string, + token: urlParams.loginToken, initial_device_display_name: defaultDeviceDisplayName, }) .then(async function (creds) { @@ -1040,7 +1043,7 @@ export function isLoggingOut(): boolean { * By the time this method is called, we have successfully logged in if necessary, and the client has been set up with * the access token. * - * Emits {@link Acction.WillStartClient} before starting the client, and {@link Action.ClientStarted} when the client has + * Emits {@link Action.WillStartClient} before starting the client, and {@link Action.ClientStarted} when the client has * been started. * * @param client the matrix client to start diff --git a/apps/web/src/Notifier.ts b/apps/web/src/Notifier.ts index dbe890909e..b0a2ce5b42 100644 --- a/apps/web/src/Notifier.ts +++ b/apps/web/src/Notifier.ts @@ -81,6 +81,59 @@ const msgTypeHandlers: Record string | null> = { }, }; +/** + * Extracts plain text from a message body, replacing any spoilered content + * with '[Spoiler]' to prevent spoilers in desktop notifications. + */ +function getNotificationBodyWithoutSpoilers(ev: MatrixEvent): string { + const content = ev.getContent(); + const plainBody = content.body ?? ""; + const formattedBody = content.formatted_body; + + if (typeof formattedBody !== "string" || !formattedBody.length) { + return plainBody; + } + + /** Recursively walks HTML tree to hide spoilers. */ + function replaceSpoilers(node: Node): Node { + if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof Element)) { + return node; + } + + if (node.hasAttribute("data-mx-spoiler")) { + const e = document.createElement("span"); + e.appendChild(document.createTextNode("[Spoiler]")); + return e; + } + + for (const childNode of node.childNodes) { + node.replaceChild(replaceSpoilers(childNode), childNode); + } + + return node; + } + + try { + // Dev note: ideally we would reuse more of the existing rendering stack + // rather than re-parsing and updating the generated HTML here. However, + // that rendering stack is currently quite consolidated and cannot + // easily be refactored to allow the call-site to control how spoilers + // are rendered. The problem is that we now need two different output + // formats: + // - The existing format where spoilers are wrapped in html tags + // - The new format where the spoilered text is replaced with [Spoiler] + + const parser = new DOMParser(); + const doc = parser.parseFromString(formattedBody, "text/html"); + + // Use textContent rather than innerHTML/outerHTML since textContent is + // XSS-safe and the input is untrusted. + return replaceSpoilers(doc.body).textContent ?? plainBody; + } catch { + return plainBody; + } +} + export const enum NotifierEvent { NotificationHiddenChange = "notification_hidden_change", } @@ -134,7 +187,7 @@ class NotifierClass extends TypedEventEmitter { - const inviter = new MultiInviter(client, roomId, options); - return { states: await inviter.invite(addresses), inviter }; -} - export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. Modal.createDialog( diff --git a/apps/web/src/SdkConfig.ts b/apps/web/src/SdkConfig.ts index 1a26e4f815..4be64648d8 100644 --- a/apps/web/src/SdkConfig.ts +++ b/apps/web/src/SdkConfig.ts @@ -12,11 +12,16 @@ import { mergeWith } from "lodash"; import { SnakedObject } from "./utils/SnakedObject"; import { type IConfigOptions } from "./IConfigOptions"; import { isObject, objectClone } from "./utils/objects"; -import { type DeepReadonly, type Defaultize } from "./@types/common"; +import { type DeepPartial, type DeepReadonly, type Defaultize } from "./@types/common"; // see element-web config.md for docs, or the IConfigOptions interface for dev docs export const DEFAULTS: DeepReadonly = { brand: "Element", + branding: { + logo_link_url: "https://element.io", + auth_header_logo_url: "themes/element/img/logos/element-logo.svg", + welcome_background_url: "themes/element/img/backgrounds/lake.jpg", + }, help_url: "https://element.io/help", help_encryption_url: "https://element.io/help#encryption", help_key_storage_url: "https://element.io/help#encryption5", @@ -70,7 +75,7 @@ export type ConfigOptions = Defaultize; function mergeConfig( config: DeepReadonly, - changes: DeepReadonly>, + changes: DeepReadonly>, ): DeepReadonly { // return { ...config, ...changes }; return mergeWith(objectClone(config), changes, (objValue, srcValue) => { @@ -136,7 +141,7 @@ export default class SdkConfig { SdkConfig.setInstance(mergeConfig(DEFAULTS, {})); // safe to cast - defaults will be applied } - public static add(cfg: Partial): void { + public static add(cfg: DeepPartial): void { SdkConfig.put(mergeConfig(SdkConfig.get(), cfg)); } } diff --git a/apps/web/src/audio/VoiceRecording.ts b/apps/web/src/audio/VoiceRecording.ts index 705b96375a..44a016b995 100644 --- a/apps/web/src/audio/VoiceRecording.ts +++ b/apps/web/src/audio/VoiceRecording.ts @@ -103,10 +103,14 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private async makeRecorder(): Promise { try { + const requestedDeviceId = MediaDeviceHandler.getAudioInput(); + const deviceIdConstraint = + requestedDeviceId && requestedDeviceId !== "default" ? { deviceId: { exact: requestedDeviceId } } : {}; + this.recorderStream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: CHANNELS, - deviceId: MediaDeviceHandler.getAudioInput(), + ...deviceIdConstraint, autoGainControl: { ideal: MediaDeviceHandler.getAudioAutoGainControl() }, echoCancellation: { ideal: MediaDeviceHandler.getAudioEchoCancellation() }, noiseSuppression: { ideal: MediaDeviceHandler.getAudioNoiseSuppression() }, diff --git a/apps/web/src/branding.ts b/apps/web/src/branding.ts new file mode 100644 index 0000000000..fd231479e6 --- /dev/null +++ b/apps/web/src/branding.ts @@ -0,0 +1,20 @@ +/* +Copyright 2026 Element Creations 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 SdkConfig from "./SdkConfig.ts"; + +const ELEMENT_BRAND = "Element"; + +/** + * Returns whether the app is currently branded. + * This is currently a naive check of whether the `brand` config starts with the substring `Element ` or is the literal `Element`, + * which correctly covers `Element` (release), `Element Nightly` & `Element Pro`. + */ +export const isElementBranded = (): boolean => { + const brand = SdkConfig.get("brand"); + return brand === ELEMENT_BRAND || brand.startsWith(ELEMENT_BRAND + " "); +}; diff --git a/apps/web/src/components/structures/MatrixChat.tsx b/apps/web/src/components/structures/MatrixChat.tsx index ce5bfd4598..14e726e532 100644 --- a/apps/web/src/components/structures/MatrixChat.tsx +++ b/apps/web/src/components/structures/MatrixChat.tsx @@ -20,7 +20,6 @@ import { type SyncStateData, type TimelineEvents, } from "matrix-js-sdk/src/matrix"; -import { type QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { throttle } from "lodash"; import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; @@ -141,6 +140,8 @@ import Markdown from "../../Markdown"; import { LinkedTextConfiguration, sanitizeHtmlParams } from "../../Linkify"; import { isOnlyAdmin } from "../../utils/membership"; import { ModuleApi } from "../../modules/Api.ts"; +import { type IScreen } from "../../vector/routing.ts"; +import { type URLParams } from "../../vector/url_utils.ts"; // legacy export export { default as Views } from "../../Views"; @@ -152,21 +153,14 @@ const AUTH_SCREENS = ["register", "mobile_register", "login", "forgot_password", // re-factoring to be included in this list in future. const ONBOARDING_FLOW_STARTERS = [Action.ViewUserSettings, Action.CreateChat, Action.CreateRoom]; -interface IScreen { - screen: string; - params?: QueryDict; -} - interface IProps { config: ConfigOptions; onNewScreen: (screen: string, replaceLast: boolean) => void; enableGuest?: boolean; - // the queryParams extracted from the [real] query-string of the URI - realQueryParams: QueryDict; - // the initial queryParams extracted from the hash-fragment of the URI - startingFragmentQueryParams?: QueryDict; + // the params extracted from the [real] query-string & fragment of the URI + urlParams: URLParams; // called when we have completed a token login - onTokenLoginCompleted: () => void; + onTokenLoginCompleted: (urlParams: URLParams, fragmentAfterLogin: string) => void; // Represents the screen to display as a result of parsing the initial window.location initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. @@ -227,11 +221,8 @@ interface IState { export default class MatrixChat extends React.PureComponent { public static displayName = "MatrixChat"; - public static defaultProps = { - realQueryParams: {}, - startingFragmentQueryParams: {}, + public static defaultProps: Partial = { config: {}, - onTokenLoginCompleted: (): void => {}, }; private firstSyncComplete = false; @@ -353,18 +344,18 @@ export default class MatrixChat extends React.PureComponent { // Otherwise, the first thing to do is to try the token params in the query-string const delegatedAuthSucceeded = await Lifecycle.attemptDelegatedAuthLogin( - this.props.realQueryParams, + this.props.urlParams, this.props.defaultDeviceDisplayName, this.getFragmentAfterLogin(), ); // remove the loginToken or auth code from the URL regardless if ( - this.props.realQueryParams?.loginToken || - this.props.realQueryParams?.code || - this.props.realQueryParams?.state + !!this.props.urlParams.legacy_sso || + !!this.props.urlParams.oidc_fragment || + !!this.props.urlParams.oidc_query ) { - this.props.onTokenLoginCompleted(); + this.props.onTokenLoginCompleted(this.props.urlParams, this.getFragmentAfterLogin()); } if (delegatedAuthSucceeded) { @@ -421,7 +412,7 @@ export default class MatrixChat extends React.PureComponent { * {@link onWillStartClient} and {@link onClientStarted} will already have been called (but not necessarily * completed). * - * This method either calls {@link onLiggedIn} directly, or switches to {@link Views.E2E_SETUP} or + * This method either calls {@link onLoggedIn} directly, or switches to {@link Views.E2E_SETUP} or * {@link Views.COMPLETE_SECURITY}, which will later call {@link onCompleteSecurityE2eSetupFinished}. */ private async postLoginSetup(): Promise { @@ -592,7 +583,7 @@ export default class MatrixChat extends React.PureComponent { return Promise.resolve() .then(() => { return Lifecycle.loadSession({ - fragmentQueryParams: this.props.startingFragmentQueryParams, + urlParams: this.props.urlParams, enableGuest: this.props.enableGuest, guestHsUrl: this.getServerProperties().serverConfig.hsUrl, guestIsUrl: this.getServerProperties().serverConfig.isUrl, @@ -1835,7 +1826,7 @@ export default class MatrixChat extends React.PureComponent { } } - public showScreen(screen: string, params?: { [key: string]: any }): void { + public showScreen(screen: string, params?: Record): void { logger.debug(`showScreen ${screen}`); const cli = MatrixClientPeg.get(); @@ -2267,14 +2258,14 @@ export default class MatrixChat extends React.PureComponent { onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onServerConfigChange={this.onServerConfigChange} fragmentAfterLogin={fragmentAfterLogin} - defaultUsername={this.props.startingFragmentQueryParams?.defaultUsername as string | undefined} + defaultUsername={this.props.urlParams?.defaults?.defaultUsername} {...this.getServerProperties()} /> ); } else if (this.state.view === Views.SOFT_LOGOUT) { view = ( diff --git a/apps/web/src/components/structures/MessagePanel.tsx b/apps/web/src/components/structures/MessagePanel.tsx index 629b9d3c7a..a38f264aab 100644 --- a/apps/web/src/components/structures/MessagePanel.tsx +++ b/apps/web/src/components/structures/MessagePanel.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, createRef, type ReactNode, type TransitionEvent } from "react"; +import React, { type JSX, createRef, type ReactNode, type TransitionEventHandler } from "react"; import classNames from "classnames"; import { type Room, @@ -20,6 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; import { DateSeparatorView, + ReadMarker, TimelineSeparator, useCreateAutoDisposedViewModel, } from "@element-hq/web-shared-components"; @@ -271,7 +272,7 @@ export default class MessagePanel extends React.Component { private readonly _showHiddenEvents: boolean; private unmounted = false; - private readMarkerNode = createRef(); + private readMarkerNode: HTMLLIElement | null = null; private whoIsTyping = createRef(); public scrollPanel = createRef(); @@ -403,7 +404,7 @@ export default class MessagePanel extends React.Component { // 0: read marker is within the window // +1: read marker is below the window public getReadMarkerPosition(): number | null { - const readMarker = this.readMarkerNode.current; + const readMarker = this.readMarkerNode; const messageWrapper = this.scrollPanel.current?.divScroll; if (!readMarker || !messageWrapper) { @@ -507,29 +508,18 @@ export default class MessagePanel extends React.Component { public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode { if (this.context.timelineRenderingType === TimelineRenderingType.File) return null; - const visible = !isLastEvent && this.props.readMarkerVisible; + const showLine = !isLastEvent && !!this.props.readMarkerVisible; if (this.props.readMarkerEventId === eventId) { - let hr; - // if the read marker comes at the end of the timeline (except - // for local echoes, which are excluded from RMs, because they - // don't have useful event ids), we don't want to show it, but - // we still want to create the
  • for it so that the - // algorithms which depend on its position on the screen aren't - // confused. - if (visible) { - hr =
    ; - } - return ( -
  • - {hr} -
  • + /> ); } else if (this.state.ghostReadMarkers.includes(eventId)) { // We render 'ghost' read markers in the DOM while they @@ -542,28 +532,30 @@ export default class MessagePanel extends React.Component { // case is a little more complex because only some of the items // transition (ie. the read markers do but the event tiles do not) // and TransitionGroup requires that all its children are Transitions. - const hr = ( -
    - ); - // give it a key which depends on the event id. That will ensure that // we get a new DOM node (restarting the animation) when the ghost // moves to a different event. return ( -
  • - {hr} -
  • + ); } return null; } - private collectGhostReadMarker = (node: HTMLElement | null): void => { + private collectReadMarker = (node: HTMLLIElement | null): void => { + this.readMarkerNode = node; + }; + + private collectGhostReadMarker = (node: HTMLHRElement | null): void => { if (node) { // now the element has appeared, change the style which will trigger the CSS transition requestAnimationFrame(() => { @@ -573,7 +565,7 @@ export default class MessagePanel extends React.Component { } }; - private onGhostTransitionEnd = (ev: TransitionEvent): void => { + private onGhostTransitionEnd: TransitionEventHandler = (ev): void => { // we can now clean up the ghost element const finishedEventId = (ev.target as HTMLElement).dataset.eventid; this.setState({ diff --git a/apps/web/src/components/structures/PictureInPictureDragger.tsx b/apps/web/src/components/structures/PictureInPictureDragger.tsx index 2cadc59a7b..d9f472c311 100644 --- a/apps/web/src/components/structures/PictureInPictureDragger.tsx +++ b/apps/web/src/components/structures/PictureInPictureDragger.tsx @@ -37,9 +37,7 @@ interface IChildrenOptions { } interface IProps { - className?: string; children: Array; - draggable: boolean; onDoubleClick?: () => void; onMove?: () => void; } @@ -181,9 +179,6 @@ export default class PictureInPictureDragger extends React.Component { }; private onStartMoving = (event: React.MouseEvent | MouseEvent): void => { - event.preventDefault(); - event.stopPropagation(); - this.mouseHeld = true; this.startingPositionX = event.clientX; this.startingPositionY = event.clientY; @@ -217,9 +212,6 @@ export default class PictureInPictureDragger extends React.Component { private onEndMoving = (event: MouseEvent): void => { if (!this.mouseHeld) return; - event.preventDefault(); - event.stopPropagation(); - this.mouseHeld = false; // Delaying this to the next event loop tick is necessary for click // event cancellation to work @@ -250,7 +242,7 @@ export default class PictureInPictureDragger extends React.Component { return (