diff --git a/.eslintrc.js b/.eslintrc.js index 4bec4e83203..3eefd222062 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,14 @@ module.exports = { ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], "Use Media helper instead to centralise access for customisation.", ), + ...buildRestrictedPropertiesOptions(["window.setImmediate"], "Use setTimeout instead."), + ], + "no-restricted-globals": [ + "error", + { + name: "setImmediate", + message: "Use setTimeout instead.", + }, ], "import/no-duplicates": ["error"], @@ -70,6 +78,11 @@ module.exports = { name: "matrix-react-sdk/", message: "Please use matrix-react-sdk/src/index instead", }, + { + name: "emojibase-regex", + message: + "This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.", + }, ], patterns: [ { @@ -107,13 +120,9 @@ module.exports = { "!matrix-js-sdk/src/extensible_events_v1/InvalidEventError", "!matrix-js-sdk/src/crypto", "!matrix-js-sdk/src/crypto/aes", - "!matrix-js-sdk/src/crypto/olmlib", - "!matrix-js-sdk/src/crypto/crypto", "!matrix-js-sdk/src/crypto/keybackup", - "!matrix-js-sdk/src/crypto/RoomList", "!matrix-js-sdk/src/crypto/deviceinfo", "!matrix-js-sdk/src/crypto/key_passphrase", - "!matrix-js-sdk/src/crypto/CrossSigning", "!matrix-js-sdk/src/crypto/recoverykey", "!matrix-js-sdk/src/crypto/dehydration", "!matrix-js-sdk/src/oidc", @@ -136,6 +145,11 @@ module.exports = { ], message: "Please use matrix-js-sdk/src/matrix instead", }, + { + group: ["emojibase-regex/emoji*"], + message: + "This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.", + }, ], }, ], diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index b663948254b..97d9692a38b 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -56,6 +56,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Fetch layered build id: layered_build @@ -103,7 +104,7 @@ jobs: fail-fast: false matrix: # Run multiple instances in parallel to speed up the tests - runner: [1, 2, 3, 4, 5, 6, 7, 8] + runner: [1, 2, 3, 4, 5, 6] steps: - uses: actions/checkout@v4 with: @@ -121,6 +122,7 @@ jobs: with: cache: "yarn" cache-dependency-path: matrix-react-sdk/yarn.lock + node-version: "lts/*" - name: Install dependencies working-directory: matrix-react-sdk @@ -145,10 +147,8 @@ jobs: run: yarn playwright install --with-deps - name: Run Playwright tests - uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a - with: - run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }} - working-directory: matrix-react-sdk + run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }} + working-directory: matrix-react-sdk - name: Upload blob report to GitHub Actions Artifacts if: always() @@ -174,6 +174,7 @@ jobs: if: inputs.skip != true with: cache: "yarn" + node-version: "lts/*" - name: Install dependencies if: inputs.skip != true @@ -189,13 +190,14 @@ jobs: - name: Merge into HTML Report if: inputs.skip != true - run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts ./all-blob-reports + run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,./playwright/stale-screenshot-reporter.ts ./all-blob-reports env: # Only pass creds to the flaky-reporter on main branch runs - GITHUB_TOKEN: ${{ github.event.workflow_run.head_branch == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }} + GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }} + # Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected - name: Upload HTML report - if: inputs.skip != true + if: always() && inputs.skip != true uses: actions/upload-artifact@v4 with: name: html-report diff --git a/.github/workflows/playwright-image-updates.yaml b/.github/workflows/playwright-image-updates.yaml index 15bea28e0f9..a160b77bcf8 100644 --- a/.github/workflows/playwright-image-updates.yaml +++ b/.github/workflows/playwright-image-updates.yaml @@ -7,7 +7,7 @@ jobs: update: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Update matrixdotorg/synapse image run: | @@ -20,7 +20,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} branch: actions/playwright-image-updates diff --git a/.github/workflows/pull_request_base_branch.yaml b/.github/workflows/pull_request_base_branch.yaml new file mode 100644 index 00000000000..49d7bcef7c7 --- /dev/null +++ b/.github/workflows/pull_request_base_branch.yaml @@ -0,0 +1,16 @@ +name: Pull Request Base Branch +on: + pull_request: + types: [opened, edited, synchronize] +jobs: + check_base_branch: + name: Check PR base branch + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const baseBranch = context.payload.pull_request.base.ref; + if (!['develop', 'staging'].includes(baseBranch) && !baseBranch.startsWith('feat/')) { + core.setFailed(`Invalid base branch: ${baseBranch}`); + } diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 6e225467afa..94ed2c7488d 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -25,6 +25,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Install Deps run: "./scripts/ci/install-deps.sh" @@ -83,6 +84,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" # Does not need branch matching as only analyses this layer - name: Install Deps @@ -100,6 +102,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" # Does not need branch matching as only analyses this layer - name: Install Deps @@ -117,6 +120,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" # Does not need branch matching as only analyses this layer - name: Install Deps @@ -134,6 +138,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Install Deps run: "scripts/ci/layered.sh" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3815c4fb4cc..e8418a95199 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,6 +44,7 @@ jobs: - name: Yarn cache uses: actions/setup-node@v4 with: + node-version: "lts/*" cache: "yarn" - name: Install Deps @@ -115,6 +116,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Run tests run: "./scripts/ci/app-tests.sh" diff --git a/CHANGELOG.md b/CHANGELOG.md index ea499883aac..280d6dd562d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,88 @@ +Changes in [3.104.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.104.0) (2024-07-30) +======================================================================================================= +## ✨ Features + +* Add release announcement for the new room header ([#12802](https://github.com/matrix-org/matrix-react-sdk/pull/12802)). Contributed by @MidhunSureshR. +* Default the room header to on ([#12803](https://github.com/matrix-org/matrix-react-sdk/pull/12803)). Contributed by @MidhunSureshR. +* Update Thread Panel to match latest designs ([#12797](https://github.com/matrix-org/matrix-react-sdk/pull/12797)). Contributed by @t3chguy. +* Close any open modals on logout ([#12777](https://github.com/matrix-org/matrix-react-sdk/pull/12777)). Contributed by @dbkr. +* Iterate design of right panel empty state ([#12796](https://github.com/matrix-org/matrix-react-sdk/pull/12796)). Contributed by @t3chguy. +* Update styling of UserInfo right panel card ([#12788](https://github.com/matrix-org/matrix-react-sdk/pull/12788)). Contributed by @t3chguy. +* Accessibility: Add Landmark navigation ([#12190](https://github.com/matrix-org/matrix-react-sdk/pull/12190)). Contributed by @akirk. +* Let Element Call widget receive m.room.create ([#12710](https://github.com/matrix-org/matrix-react-sdk/pull/12710)). Contributed by @AndrewFerr. +* Let Element Call widget set session memberships ([#12713](https://github.com/matrix-org/matrix-react-sdk/pull/12713)). Contributed by @AndrewFerr. +* Update right panel base card styling to match Compound ([#12768](https://github.com/matrix-org/matrix-react-sdk/pull/12768)). Contributed by @t3chguy. +* Align `widget_build_url_ignore_dm` with call behaviour switch between 1:1 and Widget ([#12760](https://github.com/matrix-org/matrix-react-sdk/pull/12760)). Contributed by @t3chguy. +* Move integrations switch ([#12733](https://github.com/matrix-org/matrix-react-sdk/pull/12733)). Contributed by @dbkr. +* Element-R: Report events with withheld keys separately to Posthog. ([#12755](https://github.com/matrix-org/matrix-react-sdk/pull/12755)). Contributed by @richvdh. + +## 🐛 Bug Fixes + +* Test for lack of WebAssembly support ([#12792](https://github.com/matrix-org/matrix-react-sdk/pull/12792)). Contributed by @dbkr. +* Fix stray 'account' heading ([#12791](https://github.com/matrix-org/matrix-react-sdk/pull/12791)). Contributed by @dbkr. +* Add test for the unsupported browser screen ([#12787](https://github.com/matrix-org/matrix-react-sdk/pull/12787)). Contributed by @dbkr. +* Fix HTML export test ([#12778](https://github.com/matrix-org/matrix-react-sdk/pull/12778)). Contributed by @dbkr. +* Fix HTML export missing a bunch of Compound variables ([#12774](https://github.com/matrix-org/matrix-react-sdk/pull/12774)). Contributed by @t3chguy. +* Fix inability to change accent colour consistently in custom theming ([#12772](https://github.com/matrix-org/matrix-react-sdk/pull/12772)). Contributed by @t3chguy. +* Fix edge case of landing on 3pid email link with registration disabled ([#12771](https://github.com/matrix-org/matrix-react-sdk/pull/12771)). Contributed by @t3chguy. + + +Changes in [3.103.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.103.0) (2024-07-16) +======================================================================================================= +## ✨ Features + +* Add tabs to the right panel ([#12672](https://github.com/matrix-org/matrix-react-sdk/pull/12672)). Contributed by @MidhunSureshR. +* Promote new room header from labs to Beta ([#12739](https://github.com/matrix-org/matrix-react-sdk/pull/12739)). Contributed by @t3chguy. +* Redesign room search interface ([#12677](https://github.com/matrix-org/matrix-react-sdk/pull/12677)). Contributed by @t3chguy. +* Move language settings to 'preferences' ([#12723](https://github.com/matrix-org/matrix-react-sdk/pull/12723)). Contributed by @dbkr. +* New layout selector ui in user settings ([#12676](https://github.com/matrix-org/matrix-react-sdk/pull/12676)). Contributed by @florianduros. +* Prevent Element appearing in system media controls ([#10995](https://github.com/matrix-org/matrix-react-sdk/pull/10995)). Contributed by @SuperKenVery. +* Move the account management button ([#12663](https://github.com/matrix-org/matrix-react-sdk/pull/12663)). Contributed by @dbkr. +* Disable profile controls if the HS doesn't allow them to be set ([#12652](https://github.com/matrix-org/matrix-react-sdk/pull/12652)). Contributed by @dbkr. +* New theme ui in user settings ([#12576](https://github.com/matrix-org/matrix-react-sdk/pull/12576)). Contributed by @florianduros. +* Adjust room header hover transition from 300ms to 200ms ([#12703](https://github.com/matrix-org/matrix-react-sdk/pull/12703)). Contributed by @t3chguy. +* Split out email \& phone number settings to separate components \& move discovery to privacy tab ([#12670](https://github.com/matrix-org/matrix-react-sdk/pull/12670)). Contributed by @dbkr. + +## 🐛 Bug Fixes + +* Fix incoming call toast crash due to audio refactor ([#12737](https://github.com/matrix-org/matrix-react-sdk/pull/12737)). Contributed by @t3chguy. +* Improve new room header accessibility ([#12725](https://github.com/matrix-org/matrix-react-sdk/pull/12725)). Contributed by @t3chguy. +* Fix closing all modals ([#12728](https://github.com/matrix-org/matrix-react-sdk/pull/12728)). Contributed by @dbkr. +* Fix close button on forgot password flow ([#12732](https://github.com/matrix-org/matrix-react-sdk/pull/12732)). Contributed by @dbkr. +* Don't consider textual characters to be emoji ([#12582](https://github.com/matrix-org/matrix-react-sdk/pull/12582)). Contributed by @robintown. +* Clear autocomplete input on selection accept ([#12709](https://github.com/matrix-org/matrix-react-sdk/pull/12709)). Contributed by @dbkr. +* Fix `Match system theme` toggle ([#12719](https://github.com/matrix-org/matrix-react-sdk/pull/12719)). Contributed by @florianduros. + + +Changes in [3.102.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.102.0) (2024-07-08) +======================================================================================================= +## ✨ Features + +* Switch to Rust crypto stack for all logins ([#12630](https://github.com/matrix-org/matrix-react-sdk/pull/12630)). Contributed by @richvdh. +* Hide voip buttons in group rooms in environments with widgets disabled ([#12664](https://github.com/matrix-org/matrix-react-sdk/pull/12664)). Contributed by @t3chguy. +* Minor tweaks to UserSettings dialog ([#12651](https://github.com/matrix-org/matrix-react-sdk/pull/12651)). Contributed by @florianduros. +* Hide voice call button when redundant ([#12639](https://github.com/matrix-org/matrix-react-sdk/pull/12639)). Contributed by @t3chguy. +* Improve accessibility of the room summary card ([#12586](https://github.com/matrix-org/matrix-react-sdk/pull/12586)). Contributed by @t3chguy. +* Show tooltips on narrow tabbed views ([#12624](https://github.com/matrix-org/matrix-react-sdk/pull/12624)). Contributed by @dbkr. +* Update gfm.css to github-markdown-css ([#12613](https://github.com/matrix-org/matrix-react-sdk/pull/12613)). Contributed by @t3chguy. +* Cache e2eStatus to avoid concerning unencrypted flicker when changing rooms ([#12606](https://github.com/matrix-org/matrix-react-sdk/pull/12606)). Contributed by @t3chguy. +* Tweak copy for user verification toast ([#12605](https://github.com/matrix-org/matrix-react-sdk/pull/12605)). Contributed by @t3chguy. +* Support s tags for strikethrough for Matrix v1.10 ([#12604](https://github.com/matrix-org/matrix-react-sdk/pull/12604)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* Remove redundant copy in deactive uia modal ([#12668](https://github.com/matrix-org/matrix-react-sdk/pull/12668)). Contributed by @t3chguy. +* Fix high contrast theme in settings ([#12649](https://github.com/matrix-org/matrix-react-sdk/pull/12649)). Contributed by @florianduros. +* Fix background on live location sharing footer ([#12629](https://github.com/matrix-org/matrix-react-sdk/pull/12629)). Contributed by @t3chguy. +* Remove outdated iframe sandbox attribute ([#12633](https://github.com/matrix-org/matrix-react-sdk/pull/12633)). Contributed by @t3chguy. +* Remove stray setState which caused encryption state shields to flicker ([#12632](https://github.com/matrix-org/matrix-react-sdk/pull/12632)). Contributed by @t3chguy. +* Fix stray background colour on markdown body ([#12628](https://github.com/matrix-org/matrix-react-sdk/pull/12628)). Contributed by @t3chguy. +* Fix widgets not being cleaned up correctly. ([#12616](https://github.com/matrix-org/matrix-react-sdk/pull/12616)). Contributed by @toger5. +* Add in-progress view to display name EditInPlace ([#12609](https://github.com/matrix-org/matrix-react-sdk/pull/12609)). Contributed by @dbkr. +* Fix config override of other settings levels ([#12593](https://github.com/matrix-org/matrix-react-sdk/pull/12593)). Contributed by @langleyd. +* Don't show 'saved' on display name save error ([#12600](https://github.com/matrix-org/matrix-react-sdk/pull/12600)). Contributed by @dbkr. + + Changes in [3.101.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.101.0) (2024-06-18) ======================================================================================================= ## ✨ Features diff --git a/babel.config.js b/babel.config.js index 1c4adcb7f70..a9e4b5c137b 100644 --- a/babel.config.js +++ b/babel.config.js @@ -17,9 +17,9 @@ module.exports = { ], plugins: [ "@babel/plugin-proposal-export-default-from", - "@babel/plugin-proposal-numeric-separator", - "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-object-rest-spread", + "@babel/plugin-transform-numeric-separator", + "@babel/plugin-transform-class-properties", + "@babel/plugin-transform-object-rest-spread", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-runtime", ], diff --git a/jest.config.ts b/jest.config.ts index 182c28f68ae..7293e5b3be9 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -22,7 +22,7 @@ const config: Config = { testEnvironment: "jsdom", testMatch: ["/test/**/*-test.[jt]s?(x)"], globalSetup: "/test/globalSetup.ts", - setupFiles: ["jest-canvas-mock"], + setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"], setupFilesAfterEnv: ["/test/setupTests.ts"], moduleNameMapper: { "\\.(gif|png|ttf|woff2)$": "/__mocks__/imageMock.js", diff --git a/package.json b/package.json index 6e21f438d1d..62ce8627bb2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "matrix-react-sdk", "version": "0.0.0", - "version-matrix": "3.101.0", + "version-matrix": "3.104.0", "description": "SDK for matrix.org using React for Tchap", "author": "DINUM", "repository": { @@ -24,6 +24,9 @@ "package.json", ".stylelintrc.js" ], + "engines": { + "node": ">=20.0.0" + }, "main": "./lib/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", @@ -56,29 +59,30 @@ "test:playwright:open": "yarn test:playwright --ui", "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots:build": "docker build playwright -t matrix-react-sdk-playwright", - "test:playwright:screenshots:run": "docker run --rm --network host -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright", + "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright", "coverage": "yarn test --coverage", "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'" }, "resolutions": { "@types/react-dom": "17.0.25", "@types/react": "17.0.80", - "@types/seedrandom": "3.0.4", + "@types/seedrandom": "3.0.8", "oidc-client-ts": "3.0.1", - "jwt-decode": "4.0.0" + "jwt-decode": "4.0.0", + "@floating-ui/react": "0.26.11", + "@radix-ui/react-id": "1.1.0" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.21.0", + "@matrix-org/analytics-events": "^0.24.0", "@matrix-org/emojibase-bindings": "^1.1.2", - "@matrix-org/matrix-wysiwyg": "2.17.0", - "@matrix-org/olm": "3.2.15", + "@matrix-org/matrix-wysiwyg": "2.37.4", "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", - "@sentry/browser": "^7.0.0", + "@sentry/browser": "^8.0.0", "@testing-library/react-hooks": "^8.0.1", - "@vector-im/compound-design-tokens": "^1.2.0", - "@vector-im/compound-web": "^4.4.1", + "@vector-im/compound-design-tokens": "^1.6.1", + "@vector-im/compound-web": "^5.4.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", @@ -88,19 +92,19 @@ "classnames": "^2.2.6", "commonmark": "^0.31.0", "counterpart": "^0.18.6", + "css-tree": "^2.3.1", "diff-dom": "^5.0.0", "diff-match-patch": "^1.0.5", - "emojibase-regex": "15.3.0", + "emojibase-regex": "15.3.2", "escape-html": "^1.0.3", "file-saver": "^2.0.5", - "filesize": "10.1.2", - "gfm.css": "^1.1.2", + "filesize": "10.1.4", + "github-markdown-css": "^5.5.1", "glob-to-regexp": "^0.4.1", - "graphemer": "^1.4.0", "highlight.js": "^11.3.1", "html-entities": "^2.0.0", "is-ip": "^3.1.0", - "js-xxhash": "^3.0.1", + "js-xxhash": "^4.0.0", "jszip": "^3.7.0", "katex": "^0.16.0", "linkify-element": "4.1.3", @@ -111,7 +115,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "33.1.0", + "matrix-js-sdk": "34.2.0", "matrix-widget-api": "^1.5.0", "memoize-one": "^6.0.0", "minimist": "^1.2.5", @@ -119,8 +123,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.135.2", - "proposal-temporal": "^0.9.0", + "posthog-js": "1.145.0", "qrcode": "1.5.3", "re-resizable": "^6.9.0", "react": "17.0.2", @@ -133,8 +136,9 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "2.13.0", "tar-js": "^0.3.0", + "temporal-polyfill": "^0.2.5", "ua-parser-js": "^1.0.2", - "uuid": "^9.0.0", + "uuid": "^10.0.0", "what-input": "^5.2.10" }, "devDependencies": { @@ -146,10 +150,10 @@ "@babel/eslint-parser": "^7.12.10", "@babel/eslint-plugin": "^7.12.10", "@babel/parser": "^7.12.11", - "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-export-default-from": "^7.12.1", - "@babel/plugin-proposal-numeric-separator": "^7.12.7", - "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-transform-class-properties": "^7.12.1", + "@babel/plugin-transform-numeric-separator": "^7.12.7", + "@babel/plugin-transform-object-rest-spread": "^7.12.1", "@babel/plugin-transform-runtime": "^7.12.10", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", @@ -165,6 +169,7 @@ "@types/commonmark": "^0.27.4", "@types/content-type": "^1.1.5", "@types/counterpart": "^0.18.1", + "@types/css-tree": "^2.3.8", "@types/diff-match-patch": "^1.0.32", "@types/escape-html": "^1.0.1", "@types/express": "^4.17.21", @@ -185,10 +190,10 @@ "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.11.0", "@types/sdp-transform": "^2.4.6", - "@types/seedrandom": "3.0.4", + "@types/seedrandom": "3.0.8", "@types/tar-js": "^0.3.2", "@types/ua-parser-js": "^0.7.36", - "@types/uuid": "^9.0.2", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "axe-core": "4.9.1", @@ -204,11 +209,12 @@ "eslint-plugin-matrix-org": "1.2.1", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unicorn": "^53.0.0", + "eslint-plugin-unicorn": "^54.0.0", "express": "^4.18.2", - "fake-indexeddb": "^5.0.2", + "fake-indexeddb": "^6.0.0", "fetch-mock-jest": "^1.5.1", "fs-extra": "^11.0.0", + "glob": "^11.0.0", "jest": "^29.6.2", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.6.2", @@ -219,15 +225,17 @@ "matrix-web-i18n": "^3.2.1", "mocha-junit-reporter": "^2.2.0", "node-fetch": "2", + "playwright-core": "^1.45.1", "postcss-scss": "^4.0.4", - "prettier": "3.2.5", + "prettier": "3.3.2", "raw-loader": "^4.0.2", - "rimraf": "^5.0.0", + "rimraf": "^6.0.0", "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", "ts-node": "^10.9.1", - "typescript": "5.4.5" + "typescript": "5.5.3", + "web-streams-polyfill": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4.19", diff --git a/playwright.config.ts b/playwright.config.ts index 96a8dd95ec5..ba491ff82a6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,11 +16,9 @@ limitations under the License. import { defineConfig } from "@playwright/test"; -import { TestOptions } from "./playwright/element-web-test"; - const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; -export default defineConfig({ +export default defineConfig({ use: { viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, @@ -39,19 +37,10 @@ export default defineConfig({ }, testDir: "playwright/e2e", outputDir: "playwright/test-results", - workers: 1, + workers: process.env.CI ? "50%" : 1, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? [["blob"], ["github"]] : [["html", { outputFolder: "playwright/html-report" }]], - projects: [ - { - name: "Legacy Crypto", - use: { cryptoBackend: "legacy" }, - }, - { - name: "Rust Crypto", - use: { cryptoBackend: "rust" }, - }, - ], snapshotDir: "playwright/snapshots", snapshotPathTemplate: "{snapshotDir}/{testFilePath}/{arg}-{platform}{ext}", + forbidOnly: !!process.env.CI, }); diff --git a/res/css/views/rooms/_SpaceScopeHeader.pcss b/playwright/@types/playwright-core.d.ts similarity index 63% rename from res/css/views/rooms/_SpaceScopeHeader.pcss rename to playwright/@types/playwright-core.d.ts index 4a94793ba72..0ef2ca0ece1 100644 --- a/res/css/views/rooms/_SpaceScopeHeader.pcss +++ b/playwright/@types/playwright-core.d.ts @@ -1,5 +1,5 @@ /* -Copyright 2023 The Matrix.org Foundation C.I.C. +Copyright 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,16 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SpaceScopeHeader { - text-align: center; - - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; - overflow: hidden; - - .mx_BaseAvatar { - margin-right: var(--cpd-space-2x); - vertical-align: middle; - } +declare module "playwright-core/lib/utils" { + // This type is not public in playwright-core utils + export function sanitizeForFilePath(filePath: string): string; } diff --git a/playwright/Dockerfile b/playwright/Dockerfile index 7179e08ab0b..c5a9cdbb815 100644 --- a/playwright/Dockerfile +++ b/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.44.1-jammy +FROM mcr.microsoft.com/playwright:v1.45.1-jammy WORKDIR /work/matrix-react-sdk VOLUME ["/work/element-web/node_modules"] diff --git a/playwright/docker-entrypoint.sh b/playwright/docker-entrypoint.sh index 7bc5d2c9d84..1e2ba553a72 100644 --- a/playwright/docker-entrypoint.sh +++ b/playwright/docker-entrypoint.sh @@ -5,4 +5,4 @@ set -e yarn link yarn --cwd ../element-web install yarn --cwd ../element-web link matrix-react-sdk -npx playwright test --update-snapshots --reporter line --project='Legacy Crypto' $@ +npx playwright test --update-snapshots --reporter line $@ diff --git a/playwright/e2e/accessibility/keyboard-navigation.spec.ts b/playwright/e2e/accessibility/keyboard-navigation.spec.ts new file mode 100644 index 00000000000..b4b74f51877 --- /dev/null +++ b/playwright/e2e/accessibility/keyboard-navigation.spec.ts @@ -0,0 +1,166 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect } from "../../element-web-test"; +import { Bot } from "../../pages/bot"; + +test.describe("Landmark navigation tests", () => { + test.use({ + displayName: "Alice", + }); + + test("without any rooms", async ({ page, homeserver, app, user }) => { + /** + * Without any rooms, there is no tile in the roomlist to be focused. + * So the next landmark in the list should be focused instead. + */ + + // Pressing Control+F6 will first focus the space button + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + + // Pressing Control+F6 again will focus room search + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_RoomSearch")).toBeFocused(); + + // Pressing Control+F6 again will focus the message composer + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_HomePage")).toBeFocused(); + + // Pressing Control+F6 again will bring focus back to the space button + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + + // Now go back in the same order + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_HomePage")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_RoomSearch")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + }); + + test("with an open room", async ({ page, homeserver, app, user }) => { + const bob = new Bot(page, homeserver, { displayName: "Bob" }); + await bob.prepareClient(); + + // create dm with bob + await app.client.evaluate( + async (cli, { bob }) => { + const bobRoom = await cli.createRoom({ is_direct: true }); + await cli.invite(bobRoom.room_id, bob); + }, + { + bob: bob.credentials.userId, + }, + ); + + await app.viewRoomByName("Bob"); + // confirm the room was loaded + await expect(page.getByText("Bob joined the room")).toBeVisible(); + + // Pressing Control+F6 will first focus the space button + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + + // Pressing Control+F6 again will focus room search + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_RoomSearch")).toBeFocused(); + + // Pressing Control+F6 again will focus the room tile in the room list + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_RoomTile_selected")).toBeFocused(); + + // Pressing Control+F6 again will focus the message composer + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_BasicMessageComposer_input")).toBeFocused(); + + // Pressing Control+F6 again will bring focus back to the space button + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + + // Now go back in the same order + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_BasicMessageComposer_input")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_RoomTile_selected")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_RoomSearch")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + }); + + test("without an open room", async ({ page, homeserver, app, user }) => { + const bob = new Bot(page, homeserver, { displayName: "Bob" }); + await bob.prepareClient(); + + // create a dm with bob + await app.client.evaluate( + async (cli, { bob }) => { + const bobRoom = await cli.createRoom({ is_direct: true }); + await cli.invite(bobRoom.room_id, bob); + }, + { + bob: bob.credentials.userId, + }, + ); + + await app.viewRoomByName("Bob"); + // confirm the room was loaded + await expect(page.getByText("Bob joined the room")).toBeVisible(); + + // Close the room + page.goto("/#/home"); + + // Pressing Control+F6 will first focus the space button + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + + // Pressing Control+F6 again will focus room search + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_RoomSearch")).toBeFocused(); + + // Pressing Control+F6 again will focus the room tile in the room list + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_RoomTile")).toBeFocused(); + + // Pressing Control+F6 again will focus the home section + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_HomePage")).toBeFocused(); + + // Pressing Control+F6 will bring focus back to the space button + await page.keyboard.press("ControlOrMeta+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + + // Now go back in same order + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_HomePage")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_RoomTile")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_RoomSearch")).toBeFocused(); + + await page.keyboard.press("ControlOrMeta+Shift+F6"); + await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); + }); +}); diff --git a/playwright/e2e/app-loading/feature-detection.spec.ts b/playwright/e2e/app-loading/feature-detection.spec.ts new file mode 100644 index 00000000000..2acde32c37f --- /dev/null +++ b/playwright/e2e/app-loading/feature-detection.spec.ts @@ -0,0 +1,42 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect } from "../../element-web-test"; + +test(`shows error page if browser lacks Intl support`, async ({ page }) => { + await page.addInitScript({ content: `delete window.Intl;` }); + await page.goto("/"); + + // Lack of Intl support causes the app bundle to fail to load, so we get the iframed + // static error page and need to explicitly look in the iframe becuse Playwright doesn't + // recurse into iframes when looking for elements + const header = await page.frameLocator("iframe").getByText("Unsupported browser"); + await expect(header).toBeVisible(); + + await expect(page).toMatchScreenshot("unsupported-browser.png"); +}); + +test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => { + await page.addInitScript({ content: `delete window.WebAssembly;` }); + await page.goto("/"); + + // Lack of WebAssembly support doesn't cause the bundle to fail loading, so we get + // CompatibilityView, ie. no iframes. + const header = await page.getByText("Unsupported browser"); + await expect(header).toBeVisible(); + + await expect(page).toMatchScreenshot("unsupported-browser-CompatibilityView.png"); +}); diff --git a/playwright/e2e/app-loading/guest-registration.spec.ts b/playwright/e2e/app-loading/guest-registration.spec.ts new file mode 100644 index 00000000000..17ec369359f --- /dev/null +++ b/playwright/e2e/app-loading/guest-registration.spec.ts @@ -0,0 +1,45 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + * Tests for application startup with guest registration enabled on the server. + */ + +import { expect, test } from "../../element-web-test"; + +test.use({ + startHomeserverOpts: "guest-enabled", + config: async ({ homeserver }, use) => { + await use({ + default_server_config: { + "m.homeserver": { base_url: homeserver.config.baseUrl }, + }, + }); + }, +}); + +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("link", { name: "Sign in" })).toBeVisible(); +}); + +test("Room link correctly loads a room view", async ({ page }) => { + await page.goto("/#/room/!room:id"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + await expect(page).toHaveURL(/\/#\/room\/!room:id$/); + await expect(page.getByRole("heading", { name: "Join the conversation with an account" })).toBeVisible(); +}); diff --git a/playwright/e2e/app-loading/stored-credentials.spec.ts b/playwright/e2e/app-loading/stored-credentials.spec.ts new file mode 100644 index 00000000000..f720a545597 --- /dev/null +++ b/playwright/e2e/app-loading/stored-credentials.spec.ts @@ -0,0 +1,68 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "../../element-web-test"; +import { ElementAppPage } from "../../pages/ElementAppPage"; + +/* + * Tests for application startup with credentials stored in localstorage. + */ + +test.use({ displayName: "Boris" }); + +test("Shows the homepage by default", async ({ pageWithCredentials: page }) => { + await page.goto("/"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + + await expect(page).toHaveURL(/\/#\/home/); + await expect(page.getByRole("heading", { name: "Welcome Boris", exact: true })).toBeVisible(); +}); + +test("Shows the last known page on reload", async ({ pageWithCredentials: page }) => { + await page.goto("/"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + + const app = new ElementAppPage(page); + await app.client.createRoom({ name: "Test Room" }); + await app.viewRoomByName("Test Room"); + + // Navigate away + await page.goto("about:blank"); + + // And back again + await page.goto("/"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + + // Check that the room reloaded + await expect(page).toHaveURL(/\/#\/room\//); + await expect(page.locator(".mx_RoomHeader")).toContainText("Test Room"); +}); + +test("Room link correctly loads a room view", async ({ pageWithCredentials: page }) => { + await page.goto("/#/room/!room:id"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + + await expect(page).toHaveURL(/\/#\/room\/!room:id$/); + await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible(); +}); + +test("Login link redirects to home page", async ({ pageWithCredentials: page }) => { + await page.goto("/#/login"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + + await expect(page).toHaveURL(/\/#\/home/); + await expect(page.getByRole("heading", { name: "Welcome Boris", exact: true })).toBeVisible(); +}); diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index 4581801db5b..e60d2732994 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -160,7 +160,7 @@ test.describe("Audio player", () => { // Enable high contrast manually const settings = await app.settings.openUserSettings("Appearance"); - await settings.getByTestId("mx_ThemeChoicePanel").getByText("Use high contrast").click(); + await settings.getByRole("radio", { name: "High contrast" }).click(); await app.closeDialog(); diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts new file mode 100644 index 00000000000..b142fcec4e3 --- /dev/null +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -0,0 +1,133 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import os from "node:os"; +import path from "node:path"; +import * as fsp from "node:fs/promises"; +import * as fs from "node:fs"; +import JSZip from "jszip"; + +import { test, expect } from "../../element-web-test"; + +// Based on https://github.com/Stuk/jszip/issues/466#issuecomment-2097061912 +async function extractZipFileToPath(file: string, outputPath: string): Promise { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }); + } + + const data = await fsp.readFile(file); + const zip = await JSZip.loadAsync(data, { createFolders: true }); + + await new Promise((resolve, reject) => { + let entryCount = 0; + let errorOut = false; + + zip.forEach(() => { + entryCount++; + }); // there is no other way to count the number of entries within the zip file. + + zip.forEach((relativePath, zipEntry) => { + if (errorOut) { + return; + } + + const outputEntryPath = path.join(outputPath, relativePath); + if (zipEntry.dir) { + if (!fs.existsSync(outputEntryPath)) { + fs.mkdirSync(outputEntryPath, { recursive: true }); + } + + entryCount--; + + if (entryCount === 0) { + resolve(); + } + } else { + void zipEntry + .async("blob") + .then(async (content) => Buffer.from(await content.arrayBuffer())) + .then((buffer) => { + const stream = fs.createWriteStream(outputEntryPath); + stream.write(buffer, (error) => { + if (error) { + reject(error); + errorOut = true; + } + }); + stream.on("finish", () => { + entryCount--; + + if (entryCount === 0) { + resolve(); + } + }); + stream.end(); // extremely important on Windows. On Mac / Linux, not so much since those platforms allow multiple apps to read from the same file. Windows doesn't allow that. + }) + .catch((e) => { + errorOut = true; + reject(e); + }); + } + }); + }); + + return zip; +} + +test.describe("HTML Export", () => { + test.use({ + displayName: "Alice", + room: async ({ app, user }, use) => { + const roomId = await app.client.createRoom({ name: "Important Room" }); + await app.viewRoomByName("Important Room"); + await use({ roomId }); + }, + }); + + test("should export html successfully and match screenshot", async ({ page, app, room }) => { + // Send a bunch of messages to populate the room + for (let i = 1; i < 10; i++) { + await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); + } + + // Wait for all the messages to be displayed + await expect( + page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), + ).toBeVisible(); + + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Export Chat" }).click(); + + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Export", exact: true }).click(); + const download = await downloadPromise; + + const dirPath = path.join(os.tmpdir(), "html-export-test"); + const zipPath = `${dirPath}.zip`; + await download.saveAs(zipPath); + + const zip = await extractZipFileToPath(zipPath, dirPath); + await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); + await expect(page).toMatchScreenshot("html-export.png", { + mask: [ + page.getByText("This is the start of export", { exact: false }), + // We need to mask the whole thing because the width of the time part changes + page.locator(".mx_TimelineSeparator"), + page.locator(".mx_MessageTimestamp"), + ], + }); + }); +}); diff --git a/playwright/e2e/composer/CIDER.spec.ts b/playwright/e2e/composer/CIDER.spec.ts new file mode 100644 index 00000000000..779babdaf24 --- /dev/null +++ b/playwright/e2e/composer/CIDER.spec.ts @@ -0,0 +1,106 @@ +/* +Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect } from "../../element-web-test"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; + +const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control"; + +test.describe("Composer", () => { + test.use({ + displayName: "Janet", + }); + + test.use({ + room: async ({ app, user }, use) => { + const roomId = await app.client.createRoom({ name: "Composing Room" }); + await app.viewRoomByName("Composing Room"); + await use({ roomId }); + }, + }); + + test.beforeEach(async ({ room }) => {}); // trigger room fixture + + test.describe("CIDER", () => { + test("sends a message when you click send or press Enter", async ({ page }) => { + const composer = page.getByRole("textbox", { name: "Send a message…" }); + + // Type a message + await composer.pressSequentially("my message 0"); + // It has not been sent yet + await expect(page.locator(".mx_EventTile_body", { hasText: "my message 0" })).not.toBeVisible(); + + // Click send + await page.getByRole("button", { name: "Send message" }).click(); + // It has been sent + await expect( + page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 0" }), + ).toBeVisible(); + + // Type another and press Enter afterward + await composer.pressSequentially("my message 1"); + await composer.press("Enter"); + // It was sent + await expect( + page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 1" }), + ).toBeVisible(); + }); + + test("can write formatted text", async ({ page }) => { + const composer = page.getByRole("textbox", { name: "Send a message…" }); + + await composer.pressSequentially("my bold"); + await composer.press(`${CtrlOrMeta}+KeyB`); + await composer.pressSequentially(" message"); + await page.getByRole("button", { name: "Send message" }).click(); + // Note: both "bold" and "message" are bold, which is probably surprising + await expect(page.locator(".mx_EventTile_body strong", { hasText: "bold message" })).toBeVisible(); + }); + + test("should allow user to input emoji via graphical picker", async ({ page, app }) => { + await app.getComposer(false).getByRole("button", { name: "Emoji" }).click(); + + await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click(); + + await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker + await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message + + await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible(); + }); + + test.describe("when Control+Enter is required to send", () => { + test.beforeEach(async ({ app }) => { + await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); + }); + + test("only sends when you press Control+Enter", async ({ page }) => { + const composer = page.getByRole("textbox", { name: "Send a message…" }); + // Type a message and press Enter + await composer.pressSequentially("my message 3"); + await composer.press("Enter"); + // It has not been sent yet + await expect(page.locator(".mx_EventTile_body", { hasText: "my message 3" })).not.toBeVisible(); + + // Press Control+Enter + await composer.press(`${CtrlOrMeta}+Enter`); + // It was sent + await expect( + page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 3" }), + ).toBeVisible(); + }); + }); + }); +}); diff --git a/playwright/e2e/composer/composer.spec.ts b/playwright/e2e/composer/RTE.spec.ts similarity index 80% rename from playwright/e2e/composer/composer.spec.ts rename to playwright/e2e/composer/RTE.spec.ts index e7be457f83e..53599d5320b 100644 --- a/playwright/e2e/composer/composer.spec.ts +++ b/playwright/e2e/composer/RTE.spec.ts @@ -34,76 +34,6 @@ test.describe("Composer", () => { test.beforeEach(async ({ room }) => {}); // trigger room fixture - test.describe("CIDER", () => { - test("sends a message when you click send or press Enter", async ({ page }) => { - const composer = page.getByRole("textbox", { name: "Send a message…" }); - - // Type a message - await composer.pressSequentially("my message 0"); - // It has not been sent yet - await expect(page.locator(".mx_EventTile_body", { hasText: "my message 0" })).not.toBeVisible(); - - // Click send - await page.getByRole("button", { name: "Send message" }).click(); - // It has been sent - await expect( - page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 0" }), - ).toBeVisible(); - - // Type another and press Enter afterward - await composer.pressSequentially("my message 1"); - await composer.press("Enter"); - // It was sent - await expect( - page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 1" }), - ).toBeVisible(); - }); - - test("can write formatted text", async ({ page }) => { - const composer = page.getByRole("textbox", { name: "Send a message…" }); - - await composer.pressSequentially("my bold"); - await composer.press(`${CtrlOrMeta}+KeyB`); - await composer.pressSequentially(" message"); - await page.getByRole("button", { name: "Send message" }).click(); - // Note: both "bold" and "message" are bold, which is probably surprising - await expect(page.locator(".mx_EventTile_body strong", { hasText: "bold message" })).toBeVisible(); - }); - - test("should allow user to input emoji via graphical picker", async ({ page, app }) => { - await app.getComposer(false).getByRole("button", { name: "Emoji" }).click(); - - await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click(); - - await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker - await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message - - await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible(); - }); - - test.describe("when Control+Enter is required to send", () => { - test.beforeEach(async ({ app }) => { - await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); - }); - - test("only sends when you press Control+Enter", async ({ page }) => { - const composer = page.getByRole("textbox", { name: "Send a message…" }); - // Type a message and press Enter - await composer.pressSequentially("my message 3"); - await composer.press("Enter"); - // It has not been sent yet - await expect(page.locator(".mx_EventTile_body", { hasText: "my message 3" })).not.toBeVisible(); - - // Press Control+Enter - await composer.press(`${CtrlOrMeta}+Enter`); - // It was sent - await expect( - page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 3" }), - ).toBeVisible(); - }); - }); - }); - test.describe("Rich text editor", () => { test.use({ labsFlags: ["feature_wysiwyg_composer"], diff --git a/playwright/e2e/create-room/create-room.spec.ts b/playwright/e2e/create-room/create-room.spec.ts index 0e5882e23c5..651439302d5 100644 --- a/playwright/e2e/create-room/create-room.spec.ts +++ b/playwright/e2e/create-room/create-room.spec.ts @@ -36,7 +36,7 @@ test.describe("Create Room", () => { await dialog.getByRole("button", { name: "Create room" }).click(); await expect(page).toHaveURL(/\/#\/room\/#test-room-1:localhost/); - const header = page.locator(".mx_LegacyRoomHeader"); + const header = page.locator(".mx_RoomHeader"); await expect(header).toContainText(name); await expect(header).toContainText(topic); }); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 326aeaff8e7..3f4621f90fa 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -15,29 +15,10 @@ limitations under the License. */ import type { Page } from "@playwright/test"; -import type { EmittedEvents, Preset } from "matrix-js-sdk/src/matrix"; import { expect, test } from "../../element-web-test"; -import { - copyAndContinue, - createRoom, - createSharedRoomWithUser, - doTwoWaySasVerification, - enableKeyBackup, - logIntoElement, - logOutOfElement, - sendMessageInCurrentRoom, - verifySession, - waitForVerificationRequest, -} from "./utils"; +import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils"; import { Bot } from "../../pages/bot"; import { ElementAppPage } from "../../pages/ElementAppPage"; -import { Client } from "../../pages/client"; -import { isDendrite } from "../../plugins/homeserver/dendrite"; - -const openRoomInfo = async (page: Page) => { - await page.getByRole("button", { name: "Room info" }).click(); - return page.locator(".mx_RightPanel"); -}; const checkDMRoom = async (page: Page) => { const body = page.locator(".mx_RoomView_body"); @@ -88,38 +69,6 @@ const bobJoin = async (page: Page, bob: Bot) => { return roomId; }; -/** configure the given MatrixClient to auto-accept any invites */ -async function autoJoin(client: Client) { - await client.evaluate((cli) => { - cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => { - if (member.membership === "invite" && member.userId === cli.getUserId()) { - cli.joinRoom(member.roomId); - } - }); - }); -} - -const verify = async (page: Page, bob: Bot) => { - const bobsVerificationRequestPromise = waitForVerificationRequest(bob); - - const roomInfo = await openRoomInfo(page); - await roomInfo.getByRole("menuitem", { name: "People" }).click(); - await roomInfo.getByText("Bob").click(); - await roomInfo.getByRole("button", { name: "Verify" }).click(); - await roomInfo.getByRole("button", { name: "Start Verification" }).click(); - - // this requires creating a DM, so can take a while. Give it a longer timeout. - await roomInfo.getByRole("button", { name: "Verify by emoji" }).click({ timeout: 30000 }); - - const request = await bobsVerificationRequestPromise; - // the bot user races with the Element user to hit the "verify by emoji" button - const verifier = await request.evaluateHandle((request) => request.startVerification("m.sas.v1")); - await doTwoWaySasVerification(page, verifier); - await roomInfo.getByRole("button", { name: "They match" }).click(); - await expect(roomInfo.getByText("You've successfully verified Bob!")).toBeVisible(); - await roomInfo.getByRole("button", { name: "Got it" }).click(); -}; - test.describe("Cryptography", function () { test.use({ displayName: "Alice", @@ -248,6 +197,10 @@ test.describe("Cryptography", function () { await page.getByPlaceholder("Security Key").fill(secretStorageKey); await page.getByRole("button", { name: "Continue" }).click(); + // Enter the password + await page.getByPlaceholder("Password").fill(aliceCredentials.password); + await page.getByRole("button", { name: "Continue" }).click(); + await expect(async () => { const masterKey2 = await fetchMasterKey(); expect(masterKey1).not.toEqual(masterKey2); @@ -271,11 +224,11 @@ test.describe("Cryptography", function () { await checkDMRoom(page); const bobRoomId = await bobJoin(page, bob); await testMessages(page, bob, bobRoomId); - await verify(page, bob); + await verify(app, bob); // Assert that verified icon is rendered - await page.getByRole("button", { name: "Room members" }).click(); - await page.getByRole("button", { name: "Room information" }).click(); + await page.getByTestId("base-card-back-button").click(); + await page.locator(".mx_RightPanelTabs").getByText("Info").click(); await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted"); // Take a snapshot of RoomSummaryCard with a verified E2EE icon @@ -293,525 +246,6 @@ test.describe("Cryptography", function () { // we need to have a room with the other user present, so we can open the verification panel await createSharedRoomWithUser(app, bob.credentials.userId); - await verify(page, bob); - }); - - test.describe("event shields", () => { - let testRoomId: string; - - test.beforeEach(async ({ page, bot: bob, user: aliceCredentials, app }) => { - await app.client.bootstrapCrossSigning(aliceCredentials); - await autoJoin(bob); - - // create an encrypted room - testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, { - name: "TestRoom", - initial_state: [ - { - type: "m.room.encryption", - state_key: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - ], - }); - }); - - test("should show the correct shield on e2e events", async ({ - page, - app, - bot: bob, - homeserver, - cryptoBackend, - }) => { - // Bob has a second, not cross-signed, device - const bobSecondDevice = new Bot(page, homeserver, { - bootstrapSecretStorage: false, - bootstrapCrossSigning: false, - }); - bobSecondDevice.setCredentials( - await homeserver.loginUser(bob.credentials.userId, bob.credentials.password), - ); - await bobSecondDevice.prepareClient(); - - await bob.sendEvent(testRoomId, null, "m.room.encrypted", { - algorithm: "m.megolm.v1.aes-sha2", - ciphertext: "the bird is in the hand", - }); - - const last = page.locator(".mx_EventTile_last"); - await expect(last).toContainText("Unable to decrypt message"); - const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/); - await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("This message could not be decrypted"); - - /* Should show a red padlock for an unencrypted message in an e2e room */ - await bob.evaluate( - (cli, testRoomId) => - cli.http.authedRequest( - window.matrixcs.Method.Put, - `/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`, - undefined, - { - msgtype: "m.text", - body: "test unencrypted", - }, - ), - testRoomId, - ); - - await expect(last).toContainText("test unencrypted"); - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); - await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("Not encrypted"); - - /* Should show no padlock for an unverified user */ - // bob sends a valid event - await bob.sendMessage(testRoomId, "test encrypted 1"); - - // the message should appear, decrypted, with no warning, but also no "verified" - const lastTile = page.locator(".mx_EventTile_last"); - const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon"); - await expect(lastTile).toContainText("test encrypted 1"); - // no e2e icon - await expect(lastTileE2eIcon).not.toBeVisible(); - - /* Now verify Bob */ - await verify(page, bob); - - /* Existing message should be updated when user is verified. */ - await expect(last).toContainText("test encrypted 1"); - // still no e2e icon - await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); - - /* should show no padlock, and be verified, for a message from a verified device */ - await bob.sendMessage(testRoomId, "test encrypted 2"); - - await expect(lastTile).toContainText("test encrypted 2"); - // no e2e icon - await expect(lastTileE2eIcon).not.toBeVisible(); - - /* should show red padlock for a message from an unverified device */ - await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); - await expect(lastTile).toContainText("test encrypted from unverified"); - await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); - await lastTileE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner."); - - /* Should show a grey padlock for a message from an unknown device */ - // bob deletes his second device - await bobSecondDevice.evaluate((cli) => cli.logout(true)); - - // wait for the logout to propagate. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info. - async function awaitOneDevice(iterations = 1) { - const rightPanel = page.locator(".mx_RightPanel"); - await rightPanel.getByRole("button", { name: "Room members" }).click(); - await rightPanel.getByText("Bob").click(); - const sessionCountText = await rightPanel - .locator(".mx_UserInfo_devices") - .getByText(" session", { exact: false }) - .textContent(); - // cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here - if (sessionCountText != "1 session" && sessionCountText != "1 verified session") { - if (iterations >= 10) { - throw new Error(`Bob still has ${sessionCountText} after 10 iterations`); - } - await awaitOneDevice(iterations + 1); - } - } - - await awaitOneDevice(); - - // close and reopen the room, to get the shield to update. - await app.viewRoomByName("Bob"); - await app.viewRoomByName("TestRoom"); - - // some debate over whether this should have a red or a grey shield. Legacy crypto shows a grey shield, - // Rust crypto a red one. - await expect(last).toContainText("test encrypted from unverified"); - if (cryptoBackend === "rust") { - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); - } else { - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); - } - await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device."); - }); - - test("Should show a grey padlock for a key restored from backup", async ({ - page, - app, - bot: bob, - homeserver, - user: aliceCredentials, - }) => { - const securityKey = await enableKeyBackup(app); - - // bob sends a valid event - await bob.sendMessage(testRoomId, "test encrypted 1"); - - const lastTile = page.locator(".mx_EventTile_last"); - const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon"); - await expect(lastTile).toContainText("test encrypted 1"); - // no e2e icon - await expect(lastTileE2eIcon).not.toBeVisible(); - - // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for - // the key to be backed up. - await page.waitForTimeout(10000); - - /* log out, and back in */ - await logOutOfElement(page); - await logIntoElement(page, homeserver, aliceCredentials, securityKey); - - /* go back to the test room and find Bob's message again */ - await app.viewRoomById(testRoomId); - await expect(lastTile).toContainText("test encrypted 1"); - // The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning. - // No shield would have no div mx_EventTile_e2eIcon at all. - await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); - await lastTileE2eIcon.hover(); - // The key is coming from backup, so it is not anymore possible to establish if the claimed device - // creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device." - // It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted. - await expect(page.getByRole("tooltip")).toContainText( - "The authenticity of this encrypted message can't be guaranteed on this device.", - ); - }); - - test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => { - // bob has a second, not cross-signed, device - const bobSecondDevice = new Bot(page, homeserver, { - bootstrapSecretStorage: false, - bootstrapCrossSigning: false, - }); - bobSecondDevice.setCredentials( - await homeserver.loginUser(bob.credentials.userId, bob.credentials.password), - ); - await bobSecondDevice.prepareClient(); - - // verify Bob - await verify(page, bob); - - // bob sends a valid event - const testEvent = await bob.sendMessage(testRoomId, "Hoo!"); - - // the message should appear, decrypted, with no warning - await expect( - page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"), - ).not.toBeVisible(); - - // bob sends an edit to the first message with his unverified device - await bobSecondDevice.sendMessage(testRoomId, { - "m.new_content": { - msgtype: "m.text", - body: "Haa!", - }, - "m.relates_to": { - rel_type: "m.replace", - event_id: testEvent.event_id, - }, - }); - - // the edit should have a warning - await expect( - page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon_warning"), - ).toBeVisible(); - - // a second edit from the verified device should be ok - await bob.sendMessage(testRoomId, { - "m.new_content": { - msgtype: "m.text", - body: "Hee!", - }, - "m.relates_to": { - rel_type: "m.replace", - event_id: testEvent.event_id, - }, - }); - - await expect( - page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"), - ).not.toBeVisible(); - }); - }); - - test.describe("decryption failure messages", () => { - test("should handle device-relative historical messages", async ({ - homeserver, - page, - app, - credentials, - user, - cryptoBackend, - }) => { - test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); - test.setTimeout(60000); - - // Start with a logged-in session, without key backup, and send a message. - await createRoom(page, "Test room", true); - await sendMessageInCurrentRoom(page, "test test"); - - // Log out, discarding the key for the sent message. - await logOutOfElement(page, true); - - // Log in again, and see how the message looks. - await logIntoElement(page, homeserver, credentials); - await app.viewRoomByName("Test room"); - const lastTile = page.locator(".mx_EventTile").last(); - await expect(lastTile).toContainText("Historical messages are not available on this device"); - await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); - - // Now, we set up key backup, and then send another message. - const secretStorageKey = await enableKeyBackup(app); - await app.viewRoomByName("Test room"); - await sendMessageInCurrentRoom(page, "test2 test2"); - - // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for - // the key to be backed up. - await page.waitForTimeout(10000); - - // Finally, log out again, and back in, skipping verification for now, and see what we see. - await logOutOfElement(page); - await logIntoElement(page, homeserver, credentials); - await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); - await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); - await app.viewRoomByName("Test room"); - - // There should be two historical events in the timeline - const tiles = await page.locator(".mx_EventTile").all(); - expect(tiles.length).toBeGreaterThanOrEqual(2); - // look at the last two tiles only - for (const tile of tiles.slice(-2)) { - await expect(tile).toContainText("You need to verify this device for access to historical messages"); - await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); - } - - // Now verify our device (setting up key backup), and check what happens - await verifySession(app, secretStorageKey); - const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2); - - // The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though. - await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message"); - await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); - - // The second message should now be decrypted, with a grey shield - await expect(tilesAfterVerify[1]).toContainText("test2 test2"); - await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible(); - }); - - test.describe("non-joined historical messages", () => { - test.skip(isDendrite, "does not yet support membership on events"); - - test("should display undecryptable non-joined historical messages with a different message", async ({ - homeserver, - page, - app, - credentials: aliceCredentials, - user: alice, - cryptoBackend, - bot: bob, - }) => { - test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); - - // Bob creates an encrypted room and sends a message to it. He then invites Alice - const roomId = await bob.evaluate( - async (client, { alice }) => { - const encryptionStatePromise = new Promise((resolve) => { - client.on("RoomState.events" as EmittedEvents, (event, _state, _lastStateEvent) => { - if (event.getType() === "m.room.encryption") { - resolve(); - } - }); - }); - - const { room_id: roomId } = await client.createRoom({ - initial_state: [ - { - type: "m.room.encryption", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - ], - name: "Test room", - preset: "private_chat" as Preset, - }); - - // wait for m.room.encryption event, so that when we send a - // message, it will be encrypted - await encryptionStatePromise; - - await client.sendTextMessage(roomId, "This should be undecryptable"); - - await client.invite(roomId, alice.userId); - - return roomId; - }, - { alice }, - ); - - // Alice accepts the invite - await expect( - page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), - ).toHaveCount(1); - await page.getByRole("treeitem", { name: "Test room" }).click(); - await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); - - // Bob sends an encrypted event and an undecryptable event - await bob.evaluate( - async (client, { roomId }) => { - await client.sendTextMessage(roomId, "This should be decryptable"); - await client.sendEvent( - roomId, - "m.room.encrypted" as any, - { - algorithm: "m.megolm.v1.aes-sha2", - ciphertext: "this+message+will+be+undecryptable", - device_id: client.getDeviceId()!, - sender_key: (await client.getCrypto()!.getOwnDeviceKeys()).ed25519, - session_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - } as any, - ); - }, - { roomId }, - ); - - // We wait for the event tiles that we expect from the messages that - // Bob sent, in sequence. - await expect( - page.locator(`.mx_EventTile`).getByText("You don't have access to this message"), - ).toBeVisible(); - await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); - await expect(page.locator(`.mx_EventTile`).getByText("Unable to decrypt message")).toBeVisible(); - - // And then we ensure that they are where we expect them to be - // Alice should see these event tiles: - // - first message sent by Bob (undecryptable) - // - Bob invited Alice - // - Alice joined the room - // - second message sent by Bob (decryptable) - // - third message sent by Bob (undecryptable) - const tiles = await page.locator(".mx_EventTile").all(); - expect(tiles.length).toBeGreaterThanOrEqual(5); - - // The first message from Bob was sent before Alice was in the room, so should - // be different from the standard UTD message - await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message"); - await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); - - // The second message from Bob should be decryptable - await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable"); - // this tile won't have an e2e icon since we got the key from the sender - - // The third message from Bob is undecryptable, but was sent while Alice was - // in the room and is expected to be decryptable, so this should have the - // standard UTD message - await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message"); - await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); - }); - - test("should be able to jump to a message sent before our last join event", async ({ - homeserver, - page, - app, - credentials: aliceCredentials, - user: alice, - cryptoBackend, - bot: bob, - }) => { - // The old pre-join UTD hiding code would hide events sent - // before our latest join event, even if the event that we're - // jumping to was decryptable. We test that this no longer happens. - - test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); - - // Bob: - // - creates an encrypted room, - // - invites Alice, - // - sends a message to it, - // - kicks Alice, - // - sends a bunch more events - // - invites Alice again - // In this way, there will be an event that Alice can decrypt, - // followed by a bunch of undecryptable events which Alice shouldn't - // expect to be able to decrypt. The old code would have hidden all - // the events, even the decryptable event (which it wouldn't have - // even tried to fetch, if it was far enough back). - const { roomId, eventId } = await bob.evaluate( - async (client, { alice }) => { - const { room_id: roomId } = await client.createRoom({ - initial_state: [ - { - type: "m.room.encryption", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - ], - name: "Test room", - preset: "private_chat" as Preset, - }); - - // 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(); - } - }); - }); - await client.invite(roomId, alice.userId); - // wait for the invite to come back so that we encrypt to Alice - await inviteAlicePromise; - - // send a message that Alice should be able to decrypt - const { event_id: eventId } = await client.sendTextMessage( - roomId, - "This should be decryptable", - ); - - // 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(); - } - }); - }); - await client.kick(roomId, alice.userId); - await kickAlicePromise; - - // send a bunch of messages that Alice won't be able to decrypt - for (let i = 0; i < 20; i++) { - await client.sendTextMessage(roomId, `${i}`); - } - - // invite Alice again - await client.invite(roomId, alice.userId); - - return { roomId, eventId }; - }, - { alice }, - ); - - // Alice accepts the invite - await expect( - page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), - ).toHaveCount(1); - await page.getByRole("treeitem", { name: "Test room" }).click(); - await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); - - // wait until we're joined and see the timeline - await expect(page.locator(`.mx_EventTile`).getByText("Alice joined the room")).toBeVisible(); - - // we should be able to jump to the decryptable message that Bob sent - await page.goto(`#/room/${roomId}/${eventId}`); - - await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); - }); - }); + await verify(app, bob); }); }); diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts new file mode 100644 index 00000000000..bcefa947adb --- /dev/null +++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -0,0 +1,302 @@ +/* +Copyright 2022-2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { EmittedEvents, Preset } from "matrix-js-sdk/src/matrix"; +import { expect, test } from "../../element-web-test"; +import { + createRoom, + enableKeyBackup, + logIntoElement, + logOutOfElement, + sendMessageInCurrentRoom, + verifySession, +} from "./utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; + +test.describe("Cryptography", function () { + test.use({ + displayName: "Alice", + botCreateOpts: { + displayName: "Bob", + autoAcceptInvites: false, + }, + }); + + test.describe("decryption failure messages", () => { + test("should handle device-relative historical messages", async ({ + homeserver, + page, + app, + credentials, + user, + }) => { + test.setTimeout(60000); + + // Start with a logged-in session, without key backup, and send a message. + await createRoom(page, "Test room", true); + await sendMessageInCurrentRoom(page, "test test"); + + // Log out, discarding the key for the sent message. + await logOutOfElement(page, true); + + // Log in again, and see how the message looks. + await logIntoElement(page, homeserver, credentials); + await app.viewRoomByName("Test room"); + const lastTile = page.locator(".mx_EventTile").last(); + await expect(lastTile).toContainText("Historical messages are not available on this device"); + await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + + // Now, we set up key backup, and then send another message. + const secretStorageKey = await enableKeyBackup(app); + await app.viewRoomByName("Test room"); + await sendMessageInCurrentRoom(page, "test2 test2"); + + // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for + // the key to be backed up. + await page.waitForTimeout(10000); + + // Finally, log out again, and back in, skipping verification for now, and see what we see. + await logOutOfElement(page); + await logIntoElement(page, homeserver, credentials); + await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); + await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); + await app.viewRoomByName("Test room"); + + // There should be two historical events in the timeline + const tiles = await page.locator(".mx_EventTile").all(); + expect(tiles.length).toBeGreaterThanOrEqual(2); + // look at the last two tiles only + for (const tile of tiles.slice(-2)) { + await expect(tile).toContainText("You need to verify this device for access to historical messages"); + await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + } + + // Now verify our device (setting up key backup), and check what happens + await verifySession(app, secretStorageKey); + const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2); + + // The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though. + await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message"); + await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + + // The second message should now be decrypted, with a grey shield + await expect(tilesAfterVerify[1]).toContainText("test2 test2"); + await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible(); + }); + + test.describe("non-joined historical messages", () => { + test.skip(isDendrite, "does not yet support membership on events"); + + test("should display undecryptable non-joined historical messages with a different message", async ({ + homeserver, + page, + app, + credentials: aliceCredentials, + user: alice, + bot: bob, + }) => { + // Bob creates an encrypted room and sends a message to it. He then invites Alice + const roomId = await bob.evaluate( + async (client, { alice }) => { + const encryptionStatePromise = new Promise((resolve) => { + client.on("RoomState.events" as EmittedEvents, (event, _state, _lastStateEvent) => { + if (event.getType() === "m.room.encryption") { + resolve(); + } + }); + }); + + const { room_id: roomId } = await client.createRoom({ + initial_state: [ + { + type: "m.room.encryption", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + name: "Test room", + preset: "private_chat" as Preset, + }); + + // wait for m.room.encryption event, so that when we send a + // message, it will be encrypted + await encryptionStatePromise; + + await client.sendTextMessage(roomId, "This should be undecryptable"); + + await client.invite(roomId, alice.userId); + + return roomId; + }, + { alice }, + ); + + // Alice accepts the invite + await expect( + page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), + ).toHaveCount(1); + await page.getByRole("treeitem", { name: "Test room" }).click(); + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + // Bob sends an encrypted event and an undecryptable event + await bob.evaluate( + async (client, { roomId }) => { + await client.sendTextMessage(roomId, "This should be decryptable"); + await client.sendEvent( + roomId, + "m.room.encrypted" as any, + { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: "this+message+will+be+undecryptable", + device_id: client.getDeviceId()!, + sender_key: (await client.getCrypto()!.getOwnDeviceKeys()).ed25519, + session_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + } as any, + ); + }, + { roomId }, + ); + + // We wait for the event tiles that we expect from the messages that + // Bob sent, in sequence. + await expect( + page.locator(`.mx_EventTile`).getByText("You don't have access to this message"), + ).toBeVisible(); + await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); + await expect(page.locator(`.mx_EventTile`).getByText("Unable to decrypt message")).toBeVisible(); + + // And then we ensure that they are where we expect them to be + // Alice should see these event tiles: + // - first message sent by Bob (undecryptable) + // - Bob invited Alice + // - Alice joined the room + // - second message sent by Bob (decryptable) + // - third message sent by Bob (undecryptable) + const tiles = await page.locator(".mx_EventTile").all(); + expect(tiles.length).toBeGreaterThanOrEqual(5); + + // The first message from Bob was sent before Alice was in the room, so should + // be different from the standard UTD message + await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message"); + await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + + // The second message from Bob should be decryptable + await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable"); + // this tile won't have an e2e icon since we got the key from the sender + + // The third message from Bob is undecryptable, but was sent while Alice was + // in the room and is expected to be decryptable, so this should have the + // standard UTD message + await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message"); + await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + }); + + test("should be able to jump to a message sent before our last join event", async ({ + homeserver, + page, + app, + credentials: aliceCredentials, + user: alice, + bot: bob, + }) => { + // Bob: + // - creates an encrypted room, + // - invites Alice, + // - sends a message to it, + // - kicks Alice, + // - sends a bunch more events + // - invites Alice again + // In this way, there will be an event that Alice can decrypt, + // followed by a bunch of undecryptable events which Alice shouldn't + // expect to be able to decrypt. The old code would have hidden all + // the events, even the decryptable event (which it wouldn't have + // even tried to fetch, if it was far enough back). + const { roomId, eventId } = await bob.evaluate( + async (client, { alice }) => { + const { room_id: roomId } = await client.createRoom({ + initial_state: [ + { + type: "m.room.encryption", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + name: "Test room", + preset: "private_chat" as Preset, + }); + + // 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(); + } + }); + }); + await client.invite(roomId, alice.userId); + // wait for the invite to come back so that we encrypt to Alice + await inviteAlicePromise; + + // send a message that Alice should be able to decrypt + const { event_id: eventId } = await client.sendTextMessage( + roomId, + "This should be decryptable", + ); + + // 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(); + } + }); + }); + await client.kick(roomId, alice.userId); + await kickAlicePromise; + + // send a bunch of messages that Alice won't be able to decrypt + for (let i = 0; i < 20; i++) { + await client.sendTextMessage(roomId, `${i}`); + } + + // invite Alice again + await client.invite(roomId, alice.userId); + + return { roomId, eventId }; + }, + { alice }, + ); + + // Alice accepts the invite + await expect( + page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), + ).toHaveCount(1); + await page.getByRole("treeitem", { name: "Test room" }).click(); + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + // wait until we're joined and see the timeline + await expect(page.locator(`.mx_EventTile`).getByText("Alice joined the room")).toBeVisible(); + + // we should be able to jump to the decryptable message that Bob sent + await page.goto(`#/room/${roomId}/${eventId}`); + + await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); + }); + }); + }); +}); diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 13da99ccad4..eb9efde4eed 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -102,7 +102,7 @@ test.describe("Dehydration", () => { await viewRoomSummaryByName(page, app, ROOM_NAME); - await page.getByRole("menuitem", { name: "People" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("People").click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); await getMemberTileByName(page, NAME).click(); diff --git a/playwright/e2e/crypto/verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts similarity index 64% rename from playwright/e2e/crypto/verification.spec.ts rename to playwright/e2e/crypto/device-verification.spec.ts index e471b6b2f52..929da09106d 100644 --- a/playwright/e2e/crypto/verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -15,19 +15,18 @@ limitations under the License. */ import jsQR from "jsqr"; -import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; import type { JSHandle, Locator, Page } from "@playwright/test"; -import type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api"; +import type { VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { test, expect } from "../../element-web-test"; import { + awaitVerifier, checkDeviceIsConnectedKeyBackup, checkDeviceIsCrossSigned, doTwoWaySasVerification, logIntoElement, waitForVerificationRequest, } from "./utils"; -import { Client } from "../../pages/client"; import { Bot } from "../../pages/bot"; test.describe("Device verification", () => { @@ -40,25 +39,26 @@ test.describe("Device verification", () => { // Visit the login page of the app, to load the matrix sdk await page.goto("/#/login"); - await page.pause(); - // wait for the page to load await page.waitForSelector(".mx_AuthPage", { timeout: 30000 }); // Create a new device for alice aliceBotClient = new Bot(page, homeserver, { - rustCrypto: true, bootstrapCrossSigning: true, bootstrapSecretStorage: true, }); aliceBotClient.setCredentials(credentials); - const mxClientHandle = await aliceBotClient.prepareClient(); - - await page.waitForTimeout(20000); - expectedBackupVersion = await mxClientHandle.evaluate(async (mxClient) => { - return await mxClient.getCrypto()!.getActiveSessionBackupVersion(); - }); + // Backup is prepared in the background. Poll until it is ready. + const botClientHandle = await aliceBotClient.prepareClient(); + await expect + .poll(async () => { + expectedBackupVersion = await botClientHandle.evaluate((cli) => + cli.getCrypto()!.getActiveSessionBackupVersion(), + ); + return expectedBackupVersion; + }) + .not.toBe(null); }); // Click the "Verify with another device" button, and have the bot client auto-accept it. @@ -234,115 +234,6 @@ test.describe("Device verification", () => { }); }); -test.describe("User verification", () => { - // note that there are other tests that check user verification works in `crypto.spec.ts`. - - test.use({ - displayName: "Alice", - botCreateOpts: { displayName: "Bob", autoAcceptInvites: true, userIdPrefix: "bob_" }, - room: async ({ page, app, bot: bob, user: aliceCredentials }, use) => { - await app.client.bootstrapCrossSigning(aliceCredentials); - - // the other user creates a DM - const dmRoomId = await createDMRoom(bob, aliceCredentials.userId); - - // accept the DM - await app.viewRoomByName("Bob"); - await page.getByRole("button", { name: "Start chatting" }).click(); - await use({ roomId: dmRoomId }); - }, - }); - - test("can receive a verification request when there is no existing DM", async ({ - page, - bot: bob, - user: aliceCredentials, - toasts, - room: { roomId: dmRoomId }, - }) => { - // once Alice has joined, Bob starts the verification - const bobVerificationRequest = await bob.evaluateHandle( - async (client, { dmRoomId, aliceCredentials }) => { - const room = client.getRoom(dmRoomId); - while (room.getMember(aliceCredentials.userId)?.membership !== "join") { - await new Promise((resolve) => { - room.once(window.matrixcs.RoomStateEvent.Members, resolve); - }); - } - - return client.getCrypto().requestVerificationDM(aliceCredentials.userId, dmRoomId); - }, - { dmRoomId, aliceCredentials }, - ); - - // there should also be a toast - const toast = await toasts.getToast("Verification requested"); - // it should contain the details of the requesting user - await expect(toast.getByText(`Bob (${bob.credentials.userId})`)).toBeVisible(); - // Accept - await toast.getByRole("button", { name: "Verify Session" }).click(); - - // request verification by emoji - await page.locator("#mx_RightPanel").getByRole("button", { name: "Verify by emoji" }).click(); - - /* on the bot side, wait for the verifier to exist ... */ - const botVerifier = await awaitVerifier(bobVerificationRequest); - // ... confirm ... - botVerifier.evaluate((verifier) => verifier.verify()); - // ... and then check the emoji match - await doTwoWaySasVerification(page, botVerifier); - - await page.getByRole("button", { name: "They match" }).click(); - await expect(page.getByText("You've successfully verified Bob!")).toBeVisible(); - await page.getByRole("button", { name: "Got it" }).click(); - }); - - test("can abort emoji verification when emoji mismatch", async ({ - page, - bot: bob, - user: aliceCredentials, - toasts, - room: { roomId: dmRoomId }, - cryptoBackend, - }) => { - test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); - - // once Alice has joined, Bob starts the verification - const bobVerificationRequest = await bob.evaluateHandle( - async (client, { dmRoomId, aliceCredentials }) => { - const room = client.getRoom(dmRoomId); - while (room.getMember(aliceCredentials.userId)?.membership !== "join") { - await new Promise((resolve) => { - room.once(window.matrixcs.RoomStateEvent.Members, resolve); - }); - } - - return client.getCrypto().requestVerificationDM(aliceCredentials.userId, dmRoomId); - }, - { dmRoomId, aliceCredentials }, - ); - - // Accept verification via toast - const toast = await toasts.getToast("Verification requested"); - await toast.getByRole("button", { name: "Verify Session" }).click(); - - // request verification by emoji - await page.locator("#mx_RightPanel").getByRole("button", { name: "Verify by emoji" }).click(); - - /* on the bot side, wait for the verifier to exist ... */ - const botVerifier = await awaitVerifier(bobVerificationRequest); - // ... confirm ... - botVerifier.evaluate((verifier) => verifier.verify()).catch(() => {}); - // ... and abort the verification - await page.getByRole("button", { name: "They don't match" }).click(); - - const dialog = page.locator(".mx_Dialog"); - await expect(dialog.getByText("Your messages are not secure")).toBeVisible(); - await dialog.getByRole("button", { name: "OK" }).click(); - await expect(dialog).not.toBeVisible(); - }); -}); - /** Extract the qrcode out of an on-screen html element */ async function readQrCode(base: Locator) { const qrCode = base.locator('[alt="QR Code"]'); @@ -374,35 +265,3 @@ async function readQrCode(base: Locator) { const result = jsQR(new Uint8ClampedArray(imageData.buffer), imageData.width, imageData.height); return new Uint8Array(result.binaryData); } - -async function createDMRoom(client: Client, userId: string): Promise { - return client.createRoom({ - preset: "trusted_private_chat" as Preset, - visibility: "private" as Visibility, - invite: [userId], - is_direct: true, - initial_state: [ - { - type: "m.room.encryption", - state_key: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - ], - }); -} - -/** - * Wait for a verifier to exist for a VerificationRequest - * - * @param botVerificationRequest - */ -async function awaitVerifier(botVerificationRequest: JSHandle): Promise> { - return botVerificationRequest.evaluateHandle(async (verificationRequest) => { - while (!verificationRequest.verifier) { - await new Promise((r) => verificationRequest.once("change" as any, r)); - } - return verificationRequest.verifier; - }); -} diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts new file mode 100644 index 00000000000..b242dd060c5 --- /dev/null +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -0,0 +1,269 @@ +/* +Copyright 2022-2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "../../element-web-test"; +import { autoJoin, createSharedRoomWithUser, enableKeyBackup, logIntoElement, logOutOfElement, verify } from "./utils"; +import { Bot } from "../../pages/bot"; + +test.describe("Cryptography", function () { + test.use({ + displayName: "Alice", + botCreateOpts: { + displayName: "Bob", + autoAcceptInvites: false, + }, + }); + + test.describe("event shields", () => { + let testRoomId: string; + + test.beforeEach(async ({ page, bot: bob, user: aliceCredentials, app }) => { + await app.client.bootstrapCrossSigning(aliceCredentials); + await autoJoin(bob); + + // create an encrypted room + testRoomId = await createSharedRoomWithUser(app, bob.credentials.userId, { + name: "TestRoom", + initial_state: [ + { + type: "m.room.encryption", + state_key: "", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + }); + }); + + test("should show the correct shield on e2e events", async ({ page, app, bot: bob, homeserver }) => { + // Bob has a second, not cross-signed, device + const bobSecondDevice = new Bot(page, homeserver, { + bootstrapSecretStorage: false, + bootstrapCrossSigning: false, + }); + bobSecondDevice.setCredentials( + await homeserver.loginUser(bob.credentials.userId, bob.credentials.password), + ); + await bobSecondDevice.prepareClient(); + + await bob.sendEvent(testRoomId, null, "m.room.encrypted", { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: "the bird is in the hand", + }); + + const last = page.locator(".mx_EventTile_last"); + await expect(last).toContainText("Unable to decrypt message"); + const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); + await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/); + await lastE2eIcon.focus(); + await expect(page.getByRole("tooltip")).toContainText("This message could not be decrypted"); + + /* Should show a red padlock for an unencrypted message in an e2e room */ + await bob.evaluate( + (cli, testRoomId) => + cli.http.authedRequest( + window.matrixcs.Method.Put, + `/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`, + undefined, + { + msgtype: "m.text", + body: "test unencrypted", + }, + ), + testRoomId, + ); + + await expect(last).toContainText("test unencrypted"); + await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); + await lastE2eIcon.focus(); + await expect(page.getByRole("tooltip")).toContainText("Not encrypted"); + + /* Should show no padlock for an unverified user */ + // bob sends a valid event + await bob.sendMessage(testRoomId, "test encrypted 1"); + + // the message should appear, decrypted, with no warning, but also no "verified" + const lastTile = page.locator(".mx_EventTile_last"); + const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon"); + await expect(lastTile).toContainText("test encrypted 1"); + // no e2e icon + await expect(lastTileE2eIcon).not.toBeVisible(); + + /* Now verify Bob */ + await verify(app, bob); + + /* Existing message should be updated when user is verified. */ + await expect(last).toContainText("test encrypted 1"); + // still no e2e icon + await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); + + /* should show no padlock, and be verified, for a message from a verified device */ + await bob.sendMessage(testRoomId, "test encrypted 2"); + + await expect(lastTile).toContainText("test encrypted 2"); + // no e2e icon + await expect(lastTileE2eIcon).not.toBeVisible(); + + /* should show red padlock for a message from an unverified device */ + await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); + await expect(lastTile).toContainText("test encrypted from unverified"); + await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); + await lastTileE2eIcon.focus(); + await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner."); + + /* Should show a grey padlock for a message from an unknown device */ + // bob deletes his second device + await bobSecondDevice.evaluate((cli) => cli.logout(true)); + + // wait for the logout to propagate. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info. + async function awaitOneDevice(iterations = 1) { + const rightPanel = page.locator(".mx_RightPanel"); + await rightPanel.getByTestId("base-card-back-button").click(); + await rightPanel.getByText("Bob").click(); + const sessionCountText = await rightPanel + .locator(".mx_UserInfo_devices") + .getByText(" session", { exact: false }) + .textContent(); + // cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here + if (sessionCountText != "1 session" && sessionCountText != "1 verified session") { + if (iterations >= 10) { + throw new Error(`Bob still has ${sessionCountText} after 10 iterations`); + } + await awaitOneDevice(iterations + 1); + } + } + + await awaitOneDevice(); + + // close and reopen the room, to get the shield to update. + await app.viewRoomByName("Bob"); + await app.viewRoomByName("TestRoom"); + + await expect(last).toContainText("test encrypted from unverified"); + await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); + await lastE2eIcon.focus(); + await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device."); + }); + + test("Should show a grey padlock for a key restored from backup", async ({ + page, + app, + bot: bob, + homeserver, + user: aliceCredentials, + }) => { + test.slow(); + const securityKey = await enableKeyBackup(app); + + // bob sends a valid event + await bob.sendMessage(testRoomId, "test encrypted 1"); + + const lastTile = page.locator(".mx_EventTile_last"); + const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon"); + await expect(lastTile).toContainText("test encrypted 1"); + // no e2e icon + await expect(lastTileE2eIcon).not.toBeVisible(); + + // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for + // the key to be backed up. + await page.waitForTimeout(10000); + + /* log out, and back in */ + await logOutOfElement(page); + // Reload to work around a Rust crypto bug where it can hold onto the indexeddb even after logout + // https://github.com/element-hq/element-web/issues/25779 + await page.addInitScript(() => { + // When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures + // will re-inject the original credentials into localStorage, which we don't want. + // To work around, we add a second initScript which will clear localStorage again. + window.localStorage.clear(); + }); + await page.reload(); + await logIntoElement(page, homeserver, aliceCredentials, securityKey); + + /* go back to the test room and find Bob's message again */ + await app.viewRoomById(testRoomId); + await expect(lastTile).toContainText("test encrypted 1"); + // The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning. + // No shield would have no div mx_EventTile_e2eIcon at all. + await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); + await lastTileE2eIcon.hover(); + // The key is coming from backup, so it is not anymore possible to establish if the claimed device + // creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device." + // It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted. + await expect(page.getByRole("tooltip")).toContainText( + "The authenticity of this encrypted message can't be guaranteed on this device.", + ); + }); + + test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => { + // bob has a second, not cross-signed, device + const bobSecondDevice = new Bot(page, homeserver, { + bootstrapSecretStorage: false, + bootstrapCrossSigning: false, + }); + bobSecondDevice.setCredentials( + await homeserver.loginUser(bob.credentials.userId, bob.credentials.password), + ); + await bobSecondDevice.prepareClient(); + + // verify Bob + await verify(app, bob); + + // bob sends a valid event + const testEvent = await bob.sendMessage(testRoomId, "Hoo!"); + + // the message should appear, decrypted, with no warning + await expect( + page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"), + ).not.toBeVisible(); + + // bob sends an edit to the first message with his unverified device + await bobSecondDevice.sendMessage(testRoomId, { + "m.new_content": { + msgtype: "m.text", + body: "Haa!", + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: testEvent.event_id, + }, + }); + + // the edit should have a warning + await expect( + page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon_warning"), + ).toBeVisible(); + + // a second edit from the verified device should be ok + await bob.sendMessage(testRoomId, { + "m.new_content": { + msgtype: "m.text", + body: "Hee!", + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: testEvent.event_id, + }, + }); + + await expect( + page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"), + ).not.toBeVisible(); + }); + }); +}); diff --git a/playwright/e2e/crypto/staged-rollout.spec.ts b/playwright/e2e/crypto/staged-rollout.spec.ts deleted file mode 100644 index acdd20bc899..00000000000 --- a/playwright/e2e/crypto/staged-rollout.spec.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* -Copyright 2024 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { test, expect } from "../../element-web-test"; -import { createRoom, enableKeyBackup, logIntoElement, logOutOfElement, sendMessageInCurrentRoom } from "./utils"; -import { SettingLevel } from "../../../src/settings/SettingLevel"; - -test.describe("Adoption of rust stack", () => { - test("Test migration of existing logins when rollout is 100%", async ({ - page, - context, - app, - credentials, - homeserver, - }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - await page.goto("/#/login"); - test.slow(); - - let featureRustCrypto = false; - let stagedRolloutPercent = 0; - - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = { - default_server_config: { - "m.homeserver": { - base_url: "https://server.invalid", - }, - }, - }; - json["features"] = { - feature_rust_crypto: featureRustCrypto, - }; - json["setting_defaults"] = { - "language": "en-GB", - "RustCrypto.staged_rollout_percent": stagedRolloutPercent, - }; - await route.fulfill({ json }); - }); - - // reload to ensure we read the config - await page.reload(); - - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - featureRustCrypto = true; - - await page.reload(); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - stagedRolloutPercent = 100; - - await page.reload(); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); - - test("Test new logins by default on rust stack", async ({ - page, - context, - app, - credentials, - homeserver, - }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - await page.goto("/#/login"); - - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = { - default_server_config: { - "m.homeserver": { - base_url: "https://server.invalid", - }, - }, - }; - // we only want to test the default - json["features"] = {}; - json["setting_defaults"] = { - language: "en-GB", - }; - await route.fulfill({ json }); - }); - - // reload to get the new config - await page.reload(); - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); - - test("Test default is to not rollout existing logins", async ({ - page, - context, - app, - credentials, - homeserver, - }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - - await page.goto("/#/login"); - - // In the project.name = "Legacy crypto" it will be olm crypto - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - // Now simulate a refresh with `feature_rust_crypto` enabled but ensure we use the default rollout - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = {}; - json["features"] = { - feature_rust_crypto: true, - }; - json["setting_defaults"] = { - // We want to test the default so we don't set this - // "RustCrypto.staged_rollout_percent": 0, - }; - await route.fulfill({ json }); - }); - - await page.reload(); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - }); - - test("Migrate using labflag should work", async ({ page, context, app, credentials, homeserver }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - - await page.goto("/#/login"); - - // In the project.name = "Legacy crypto" it will be olm crypto - await logIntoElement(page, homeserver, credentials); - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Olm")).toBeVisible(); - - // We need to enable devtools for this test - await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true); - - // Now simulate a refresh with `feature_rust_crypto` enabled but ensure no automatic migration - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = {}; - json["features"] = { - feature_rust_crypto: true, - }; - json["setting_defaults"] = { - "RustCrypto.staged_rollout_percent": 0, - }; - await route.fulfill({ json }); - }); - - await page.reload(); - - // Go to the labs flag and enable the migration - await app.settings.openUserSettings("Labs"); - await page.getByRole("switch", { name: "Rust cryptography implementation" }).click(); - - // Fixes a bug where a missing session data was shown - // https://github.com/element-hq/element-web/issues/26970 - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); - - test("Test migration of room shields", async ({ page, context, app, credentials, homeserver }, workerInfo) => { - test.skip( - workerInfo.project.name === "Rust Crypto", - "No need to test this on Rust Crypto as we override the config manually", - ); - test.slow(); - - await page.goto("/#/login"); - - // In the project.name = "Legacy crypto" it will be olm crypto - await logIntoElement(page, homeserver, credentials); - - // create a room and send a message - await createRoom(page, "Room1", true); - await sendMessageInCurrentRoom(page, "Hello"); - - // enable backup to save this room key - const securityKey = await enableKeyBackup(app); - - // wait a bit for upload to complete, there is a random timout on key upload - await page.waitForTimeout(6000); - - // logout - await logOutOfElement(page); - - // We logout and log back in in order to get the historical key from backup and have a gray shield - await page.reload(); - await page.goto("/#/login"); - // login again and verify - await logIntoElement(page, homeserver, credentials, securityKey); - - await app.viewRoomByName("Room1"); - - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "Hello" }); - // there should be a shield - await expect(messageDiv.locator(".mx_EventTile_e2eIcon")).toBeVisible(); - } - - // Now type a new message - await sendMessageInCurrentRoom(page, "World"); - - // wait a bit for the message to be sent - await expect( - page - .locator(".mx_EventTile_line") - .filter({ hasText: "World" }) - .locator("..") - .locator(".mx_EventTile_receiptSent"), - ).toBeVisible(); - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "World" }); - // there should not be a shield - expect(await messageDiv.locator(".mx_EventTile_e2eIcon").count()).toEqual(0); - } - - // trigger a migration - await context.route(`http://localhost:8080/config.json*`, async (route) => { - const json = {}; - json["features"] = { - feature_rust_crypto: true, - }; - json["setting_defaults"] = { - "RustCrypto.staged_rollout_percent": 100, - }; - await route.fulfill({ json }); - }); - - await page.reload(); - - await app.viewRoomByName("Room1"); - - // The shields should be migrated properly - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "Hello" }); - await expect(messageDiv).toBeVisible(); - // there should be a shield - await expect(messageDiv.locator(".mx_EventTile_e2eIcon")).toBeVisible(); - } - { - const messageDiv = page.locator(".mx_EventTile_line").filter({ hasText: "World" }); - await expect(messageDiv).toBeVisible(); - // there should not be a shield - expect(await messageDiv.locator(".mx_EventTile_e2eIcon").count()).toEqual(0); - } - - await app.settings.openUserSettings("Help & About"); - await expect(page.getByText("Crypto version: Rust SDK")).toBeVisible(); - }); -}); diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts new file mode 100644 index 00000000000..eac0fb639e1 --- /dev/null +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -0,0 +1,145 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; + +import { test, expect } from "../../element-web-test"; +import { doTwoWaySasVerification, awaitVerifier } from "./utils"; +import { Client } from "../../pages/client"; + +test.describe("User verification", () => { + // note that there are other tests that check user verification works in `crypto.spec.ts`. + + test.use({ + displayName: "Alice", + botCreateOpts: { displayName: "Bob", autoAcceptInvites: true, userIdPrefix: "bob_" }, + room: async ({ page, app, bot: bob, user: aliceCredentials }, use) => { + await app.client.bootstrapCrossSigning(aliceCredentials); + + // the other user creates a DM + const dmRoomId = await createDMRoom(bob, aliceCredentials.userId); + + // accept the DM + await app.viewRoomByName("Bob"); + await page.getByRole("button", { name: "Start chatting" }).click(); + await use({ roomId: dmRoomId }); + }, + }); + + test("can receive a verification request when there is no existing DM", async ({ + page, + bot: bob, + user: aliceCredentials, + toasts, + room: { roomId: dmRoomId }, + }) => { + // once Alice has joined, Bob starts the verification + const bobVerificationRequest = await bob.evaluateHandle( + async (client, { dmRoomId, aliceCredentials }) => { + const room = client.getRoom(dmRoomId); + while (room.getMember(aliceCredentials.userId)?.membership !== "join") { + await new Promise((resolve) => { + room.once(window.matrixcs.RoomStateEvent.Members, resolve); + }); + } + + return client.getCrypto().requestVerificationDM(aliceCredentials.userId, dmRoomId); + }, + { dmRoomId, aliceCredentials }, + ); + + // there should also be a toast + const toast = await toasts.getToast("Verification requested"); + // it should contain the details of the requesting user + await expect(toast.getByText(`Bob (${bob.credentials.userId})`)).toBeVisible(); + // Accept + await toast.getByRole("button", { name: "Verify User" }).click(); + + // request verification by emoji + await page.locator("#mx_RightPanel").getByRole("button", { name: "Verify by emoji" }).click(); + + /* on the bot side, wait for the verifier to exist ... */ + const botVerifier = await awaitVerifier(bobVerificationRequest); + // ... confirm ... + botVerifier.evaluate((verifier) => verifier.verify()); + // ... and then check the emoji match + await doTwoWaySasVerification(page, botVerifier); + + await page.getByRole("button", { name: "They match" }).click(); + await expect(page.getByText("You've successfully verified Bob!")).toBeVisible(); + await page.getByRole("button", { name: "Got it" }).click(); + }); + + test("can abort emoji verification when emoji mismatch", async ({ + page, + bot: bob, + user: aliceCredentials, + toasts, + room: { roomId: dmRoomId }, + }) => { + // once Alice has joined, Bob starts the verification + const bobVerificationRequest = await bob.evaluateHandle( + async (client, { dmRoomId, aliceCredentials }) => { + const room = client.getRoom(dmRoomId); + while (room.getMember(aliceCredentials.userId)?.membership !== "join") { + await new Promise((resolve) => { + room.once(window.matrixcs.RoomStateEvent.Members, resolve); + }); + } + + return client.getCrypto().requestVerificationDM(aliceCredentials.userId, dmRoomId); + }, + { dmRoomId, aliceCredentials }, + ); + + // Accept verification via toast + const toast = await toasts.getToast("Verification requested"); + await toast.getByRole("button", { name: "Verify User" }).click(); + + // request verification by emoji + await page.locator("#mx_RightPanel").getByRole("button", { name: "Verify by emoji" }).click(); + + /* on the bot side, wait for the verifier to exist ... */ + const botVerifier = await awaitVerifier(bobVerificationRequest); + // ... confirm ... + botVerifier.evaluate((verifier) => verifier.verify()).catch(() => {}); + // ... and abort the verification + await page.getByRole("button", { name: "They don't match" }).click(); + + const dialog = page.locator(".mx_Dialog"); + await expect(dialog.getByText("Your messages are not secure")).toBeVisible(); + await dialog.getByRole("button", { name: "OK" }).click(); + await expect(dialog).not.toBeVisible(); + }); +}); + +async function createDMRoom(client: Client, userId: string): Promise { + return client.createRoom({ + preset: "trusted_private_chat" as Preset, + visibility: "private" as Visibility, + invite: [userId], + is_direct: true, + initial_state: [ + { + type: "m.room.encryption", + state_key: "", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + }); +} diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 5b0bf29b976..3c1e267111c 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -27,6 +27,7 @@ import type { import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; import { Client } from "../../pages/client"; import { ElementAppPage } from "../../pages/ElementAppPage"; +import { Bot } from "../../pages/bot"; /** * wait for the given client to receive an incoming verification request, and automatically accept it @@ -113,6 +114,13 @@ export async function checkDeviceIsConnectedKeyBackup( expectedBackupVersion: string, checkBackupKeyInCache: boolean, ): Promise { + // Sanity check the given backup version: if it's null, something went wrong earlier in the test. + if (!expectedBackupVersion) { + throw new Error( + `Invalid backup version passed to \`checkDeviceIsConnectedKeyBackup\`: ${expectedBackupVersion}`, + ); + } + await page.getByRole("button", { name: "User menu" }).click(); await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click(); await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible(); @@ -320,3 +328,60 @@ export async function createRoom(page: Page, roomName: string, isEncrypted: bool await expect(page.getByText("Encryption enabled")).toBeVisible(); } } + +/** + * Configure the given MatrixClient to auto-accept any invites + * @param client - the client to configure + */ +export async function autoJoin(client: Client) { + await client.evaluate((cli) => { + cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => { + if (member.membership === "invite" && member.userId === cli.getUserId()) { + cli.joinRoom(member.roomId); + } + }); + }); +} + +/** + * Verify a user by emoji + * @param page - the page to use + * @param bob - the user to verify + */ +export const verify = async (app: ElementAppPage, bob: Bot) => { + const page = app.page; + const bobsVerificationRequestPromise = waitForVerificationRequest(bob); + + const roomInfo = await app.toggleRoomInfoPanel(); + await page.locator(".mx_RightPanelTabs").getByText("People").click(); + await roomInfo.getByText("Bob").click(); + await roomInfo.getByRole("button", { name: "Verify" }).click(); + await roomInfo.getByRole("button", { name: "Start Verification" }).click(); + + // this requires creating a DM, so can take a while. Give it a longer timeout. + await roomInfo.getByRole("button", { name: "Verify by emoji" }).click({ timeout: 30000 }); + + const request = await bobsVerificationRequestPromise; + // the bot user races with the Element user to hit the "verify by emoji" button + const verifier = await request.evaluateHandle((request) => request.startVerification("m.sas.v1")); + await doTwoWaySasVerification(page, verifier); + await roomInfo.getByRole("button", { name: "They match" }).click(); + await expect(roomInfo.getByText("You've successfully verified Bob!")).toBeVisible(); + await roomInfo.getByRole("button", { name: "Got it" }).click(); +}; + +/** + * Wait for a verifier to exist for a VerificationRequest + * + * @param botVerificationRequest + */ +export async function awaitVerifier( + botVerificationRequest: JSHandle, +): Promise> { + return botVerificationRequest.evaluateHandle(async (verificationRequest) => { + while (!verificationRequest.verifier) { + await new Promise((r) => verificationRequest.once("change" as any, r)); + } + return verificationRequest.verifier; + }); +} diff --git a/playwright/e2e/file-upload/image-upload.spec.ts b/playwright/e2e/file-upload/image-upload.spec.ts index 8f0403af31c..d75d20f441b 100644 --- a/playwright/e2e/file-upload/image-upload.spec.ts +++ b/playwright/e2e/file-upload/image-upload.spec.ts @@ -38,8 +38,8 @@ test.describe("Image Upload", () => { .locator(".mx_MessageComposer_actions input[type='file']") .setInputFiles("playwright/sample-files/riot.png"); - expect(page.getByRole("button", { name: "Upload" })).toBeEnabled(); - expect(page.getByRole("button", { name: "Close dialog" })).toBeEnabled(); - expect(page).toMatchScreenshot("image-upload-preview.png"); + await expect(page.getByRole("button", { name: "Upload" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "Close dialog" })).toBeEnabled(); + await expect(page).toMatchScreenshot("image-upload-preview.png"); }); }); diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts new file mode 100644 index 00000000000..260242ebc6c --- /dev/null +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -0,0 +1,77 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "../../element-web-test"; +import { selectHomeserver } from "../utils"; + +const username = "user1234"; +// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. +const password = "oETo7MPf0o"; +const email = "user@nowhere.dummy"; + +test.describe("Forgot Password", () => { + test.use({ + startHomeserverOpts: ({ mailhog }, use) => + use({ + template: "email", + variables: { + SMTP_HOST: "host.containers.internal", + SMTP_PORT: mailhog.instance.smtpPort, + }, + }), + }); + + test("renders properly", async ({ page, homeserver }) => { + await page.goto("/"); + + await page.getByRole("link", { name: "Sign in" }).click(); + + // need to select a homeserver at this stage, before entering the forgot password flow + await selectHomeserver(page, homeserver.config.baseUrl); + + await page.getByRole("button", { name: "Forgot password?" }).click(); + + await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); + }); + + test("renders email verification dialog properly", async ({ page, homeserver }) => { + const user = await homeserver.registerUser(username, password); + + await homeserver.setThreepid(user.userId, "email", email); + + await page.goto("/"); + + await page.getByRole("link", { name: "Sign in" }).click(); + await selectHomeserver(page, homeserver.config.baseUrl); + + await page.getByRole("button", { name: "Forgot password?" }).click(); + + await page.getByRole("textbox", { name: "Email address" }).fill(email); + + await page.getByRole("button", { name: "Send email" }).click(); + + await page.getByRole("button", { name: "Next" }).click(); + + await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password); + await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password); + + await page.getByRole("button", { name: "Reset password" }).click(); + + await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport(); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png"); + }); +}); diff --git a/playwright/e2e/integration-manager/get-openid-token.spec.ts b/playwright/e2e/integration-manager/get-openid-token.spec.ts index c107bb2cbcb..a0f099cb62b 100644 --- a/playwright/e2e/integration-manager/get-openid-token.spec.ts +++ b/playwright/e2e/integration-manager/get-openid-token.spec.ts @@ -118,8 +118,8 @@ test.describe("Integration Manager: Get OpenID Token", () => { await app.viewRoomByName(ROOM_NAME); }); - test("should successfully obtain an openID token", async ({ page }) => { - await openIntegrationManager(page); + test("should successfully obtain an openID token", async ({ page, app }) => { + await openIntegrationManager(app); await sendActionFromIntegrationManager(page, integrationManagerUrl); const iframe = page.frameLocator(`iframe[src*="${integrationManagerUrl}"]`); diff --git a/playwright/e2e/integration-manager/kick.spec.ts b/playwright/e2e/integration-manager/kick.spec.ts index b5ca6a1b3a5..afe2de0f194 100644 --- a/playwright/e2e/integration-manager/kick.spec.ts +++ b/playwright/e2e/integration-manager/kick.spec.ts @@ -167,7 +167,7 @@ test.describe("Integration Manager: Kick", () => { await app.client.inviteUser(room.roomId, targetUser.credentials.userId); await expect(page.getByText(`${BOT_DISPLAY_NAME} joined the room`)).toBeVisible(); - await openIntegrationManager(page); + await openIntegrationManager(app); await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId); await closeIntegrationManager(page, integrationManagerUrl); await expectKickedMessage(page, true); @@ -185,7 +185,7 @@ test.describe("Integration Manager: Kick", () => { }, }); - await openIntegrationManager(page); + await openIntegrationManager(app); await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId); await closeIntegrationManager(page, integrationManagerUrl); await expectKickedMessage(page, false); @@ -197,7 +197,7 @@ test.describe("Integration Manager: Kick", () => { await expect(page.getByText(`${BOT_DISPLAY_NAME} joined the room`)).toBeVisible(); await targetUser.leave(room.roomId); - await openIntegrationManager(page); + await openIntegrationManager(app); await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId); await closeIntegrationManager(page, integrationManagerUrl); await expectKickedMessage(page, false); @@ -209,7 +209,7 @@ test.describe("Integration Manager: Kick", () => { await expect(page.getByText(`${BOT_DISPLAY_NAME} joined the room`)).toBeVisible(); await app.client.ban(room.roomId, targetUser.credentials.userId); - await openIntegrationManager(page); + await openIntegrationManager(app); await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId); await closeIntegrationManager(page, integrationManagerUrl); await expectKickedMessage(page, false); @@ -218,7 +218,7 @@ test.describe("Integration Manager: Kick", () => { test("should no-op if the target was never a room member", async ({ page, app, bot: targetUser, room }) => { await app.viewRoomByName(ROOM_NAME); - await openIntegrationManager(page); + await openIntegrationManager(app); await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, targetUser.credentials.userId); await closeIntegrationManager(page, integrationManagerUrl); await expectKickedMessage(page, false); diff --git a/playwright/e2e/integration-manager/read_events.spec.ts b/playwright/e2e/integration-manager/read_events.spec.ts index b178596674d..2e2ee8d1871 100644 --- a/playwright/e2e/integration-manager/read_events.spec.ts +++ b/playwright/e2e/integration-manager/read_events.spec.ts @@ -142,7 +142,7 @@ test.describe("Integration Manager: Read Events", () => { // Send a state event const sendEventResponse = await app.client.sendStateEvent(room.roomId, eventType, eventContent, stateKey); - await openIntegrationManager(page); + await openIntegrationManager(app); // Read state events await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, eventType, stateKey); @@ -162,7 +162,7 @@ test.describe("Integration Manager: Read Events", () => { // Send a state event const sendEventResponse = await app.client.sendStateEvent(room.roomId, eventType, eventContent, stateKey); - await openIntegrationManager(page); + await openIntegrationManager(app); // Read state events await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, eventType, stateKey); @@ -196,7 +196,7 @@ test.describe("Integration Manager: Read Events", () => { app.client.sendStateEvent(room.roomId, eventType, eventContent3, stateKey3), ]); - await openIntegrationManager(page); + await openIntegrationManager(app); // Read state events await sendActionFromIntegrationManager( @@ -217,11 +217,11 @@ test.describe("Integration Manager: Read Events", () => { await expect(iframe.locator("#message-response")).toContainText(`"content":${JSON.stringify(eventContent3)}`); }); - test("should fail to read an event type which is not allowed", async ({ page, room }) => { + test("should fail to read an event type which is not allowed", async ({ page, app, room }) => { const eventType = "com.example.event"; const stateKey = ""; - await openIntegrationManager(page); + await openIntegrationManager(app); // Read state events await sendActionFromIntegrationManager(page, integrationManagerUrl, room.roomId, eventType, stateKey); diff --git a/playwright/e2e/integration-manager/send_event.spec.ts b/playwright/e2e/integration-manager/send_event.spec.ts index 61bad8a3ec7..ea2c3553045 100644 --- a/playwright/e2e/integration-manager/send_event.spec.ts +++ b/playwright/e2e/integration-manager/send_event.spec.ts @@ -137,7 +137,7 @@ test.describe("Integration Manager: Send Event", () => { ); await app.viewRoomByName(ROOM_NAME); - await openIntegrationManager(page); + await openIntegrationManager(app); }); test("should send a state event", async ({ page, app, room }) => { diff --git a/playwright/e2e/integration-manager/utils.ts b/playwright/e2e/integration-manager/utils.ts index 259ff732c79..c6a2fb998ee 100644 --- a/playwright/e2e/integration-manager/utils.ts +++ b/playwright/e2e/integration-manager/utils.ts @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type { Page } from "@playwright/test"; +import type { ElementAppPage } from "../../pages/ElementAppPage"; -export async function openIntegrationManager(page: Page) { - await page.getByRole("button", { name: "Room info" }).click(); +export async function openIntegrationManager(app: ElementAppPage) { + const { page } = app; + await app.toggleRoomInfoPanel(); await page .locator(".mx_RoomSummaryCard_appsGroup") .getByRole("button", { name: "Add widgets, bridges & bots" }) diff --git a/playwright/e2e/invite/invite-dialog.spec.ts b/playwright/e2e/invite/invite-dialog.spec.ts index 98a57c8eb1a..d9e086aaa1f 100644 --- a/playwright/e2e/invite/invite-dialog.spec.ts +++ b/playwright/e2e/invite/invite-dialog.spec.ts @@ -36,7 +36,7 @@ test.describe("Invite dialog", function () { await expect(page.getByText("Hanako created and configured the room.")).toBeVisible(); // Open the room info panel - await page.getByRole("button", { name: "Room info" }).click(); + await app.toggleRoomInfoPanel(); await page.locator(".mx_BaseCard").getByRole("menuitem", { name: "Invite" }).click(); @@ -114,12 +114,9 @@ test.describe("Invite dialog", function () { // Assert that the hovered user name on invitation UI does not have background color // TODO: implement the test on room-header.spec.ts - const roomHeader = page.locator(".mx_LegacyRoomHeader"); - await roomHeader.locator(".mx_LegacyRoomHeader_name--textonly").hover(); - await expect(roomHeader.locator(".mx_LegacyRoomHeader_name--textonly")).toHaveCSS( - "background-color", - "rgba(0, 0, 0, 0)", - ); + const roomHeader = page.locator(".mx_RoomHeader"); + await roomHeader.locator(".mx_RoomHeader_heading").hover(); + await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); // Send a message to invite the bots const composer = app.getComposer().locator("[contenteditable]"); diff --git a/playwright/e2e/knock/create-knock-room.spec.ts b/playwright/e2e/knock/create-knock-room.spec.ts index 8763c0fd6a8..9e610766d32 100644 --- a/playwright/e2e/knock/create-knock-room.spec.ts +++ b/playwright/e2e/knock/create-knock-room.spec.ts @@ -31,7 +31,7 @@ test.describe("Create Knock Room", () => { await dialog.getByRole("option", { name: "Ask to join" }).click(); await dialog.getByRole("button", { name: "Create room" }).click(); - await expect(page.locator(".mx_LegacyRoomHeader").getByText("Cybersecurity")).toBeVisible(); + await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible(); const urlHash = await page.evaluate(() => window.location.hash); const roomId = urlHash.replace("#/room/", ""); @@ -48,7 +48,7 @@ test.describe("Create Knock Room", () => { await dialog.getByRole("textbox", { name: "Name" }).fill("Cybersecurity"); await dialog.getByRole("button", { name: "Create room" }).click(); - await expect(page.locator(".mx_LegacyRoomHeader").getByText("Cybersecurity")).toBeVisible(); + await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible(); const urlHash = await page.evaluate(() => window.location.hash); const roomId = urlHash.replace("#/room/", ""); @@ -74,7 +74,7 @@ test.describe("Create Knock Room", () => { await dialog.getByText("Make this room visible in the public room directory.").click(); await dialog.getByRole("button", { name: "Create room" }).click(); - await expect(page.locator(".mx_LegacyRoomHeader").getByText("Cybersecurity")).toBeVisible(); + await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible(); const urlHash = await page.evaluate(() => window.location.hash); const roomId = urlHash.replace("#/room/", ""); diff --git a/playwright/e2e/lazy-loading/lazy-loading.spec.ts b/playwright/e2e/lazy-loading/lazy-loading.spec.ts index 8b815898136..1a20100d1a2 100644 --- a/playwright/e2e/lazy-loading/lazy-loading.spec.ts +++ b/playwright/e2e/lazy-loading/lazy-loading.spec.ts @@ -78,9 +78,10 @@ test.describe("Lazy Loading", () => { } } - async function openMemberlist(page: Page): Promise { - await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Room info" }).click(); - await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "People" }).click(); // \d represents the number of the room members + async function openMemberlist(app: ElementAppPage): Promise { + await app.toggleRoomInfoPanel(); + const { page } = app; + await page.locator(".mx_RightPanelTabs").getByText("People").click(); } function getMemberInMemberlist(page: Page, name: string): Locator { @@ -123,7 +124,7 @@ test.describe("Lazy Loading", () => { // Alice should see 2 messages from every charly with the correct display name await checkPaginatedDisplayNames(app, charly1to5); - await openMemberlist(page); + await openMemberlist(app); await checkMemberList(page, charly1to5); await joinCharliesWhileAliceIsOffline(page, app, charly6to10); await checkMemberList(page, charly6to10); diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login.spec.ts index 8d2e324e93a..bbadf02d08c 100644 --- a/playwright/e2e/login/login.spec.ts +++ b/playwright/e2e/login/login.spec.ts @@ -14,36 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { test, expect } from "../../element-web-test"; +import { expect, test } from "../../element-web-test"; import { doTokenRegistration } from "./utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { selectHomeserver } from "../utils"; test.describe("Login", () => { - test.describe("m.login.password", () => { + test.describe("Password login", () => { test.use({ startHomeserverOpts: "consent" }); const username = "user1234"; const password = ""; // :TCHAP: remove pwd to pass git guardian - test.beforeEach(async ({ page, homeserver }) => { + test.beforeEach(async ({ homeserver }) => { await homeserver.registerUser(username, password); - await page.goto("/#/login"); }); - test("logs in with an existing account and lands on the home screen", async ({ + test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({ page, homeserver, checkA11y, }) => { - // first pick the homeserver, as otherwise the user picker won't be visible - await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); - await page.getByRole("button", { name: "Continue", exact: true }).click(); - // wait for the dialog to go away - await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); + await page.goto("/"); - await expect(page.locator(".mx_Spinner")).toHaveCount(0); - await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserver.config.baseUrl); + // Should give us the welcome page initially + await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible(); + + // Start the login process + await page.getByRole("link", { name: "Sign in" }).click(); + + // first pick the homeserver, as otherwise the user picker won't be visible + await selectHomeserver(page, homeserver.config.baseUrl); await page.getByRole("button", { name: "Edit" }).click(); @@ -56,14 +57,7 @@ test.describe("Login", () => { await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid"); // switch back to the custom homeserver - await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); - await page.getByRole("button", { name: "Continue", exact: true }).click(); - // wait for the dialog to go away - await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); - - await expect(page.locator(".mx_Spinner")).toHaveCount(0); - await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserver.config.baseUrl); + await selectHomeserver(page, homeserver.config.baseUrl); await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688 @@ -76,6 +70,20 @@ test.describe("Login", () => { await expect(page).toHaveURL(/\/#\/home$/); }); + + test("Follows the original link after login", async ({ page, homeserver }) => { + await page.goto("/#/room/!room:id"); // should redirect to the welcome page + await page.getByRole("link", { name: "Sign in" }).click(); + + await selectHomeserver(page, homeserver.config.baseUrl); + + await page.getByRole("textbox", { name: "Username" }).fill(username); + await page.getByPlaceholder("Password").fill(password); + await page.getByRole("button", { name: "Sign in" }).click(); + + await expect(page).toHaveURL(/\/#\/room\/!room:id$/); + await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible(); + }); }); // tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server diff --git a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts index 287ac77cd41..3070d5fad0d 100644 --- a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts +++ b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts @@ -35,10 +35,10 @@ test.describe("1:1 chat room", () => { await page.goto(`/#/user/${user2.userId}?action=chat`); }); - test("should open new 1:1 chat room after leaving the old one", async ({ page, user2 }) => { + test("should open new 1:1 chat room after leaving the old one", async ({ page, app, user2 }) => { // leave 1:1 chat room - await page.locator(".mx_LegacyRoomHeader_nametext").getByText(user2.displayName).click(); - await page.getByRole("menuitem", { name: "Leave" }).click(); + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Leave room" }).click(); await page.getByRole("button", { name: "Leave" }).click(); // wait till the room was left @@ -49,6 +49,6 @@ test.describe("1:1 chat room", () => { // open new 1:1 chat room await page.goto(`/#/user/${user2.userId}?action=chat`); - await expect(page.locator(".mx_LegacyRoomHeader_nametext").getByText(user2.displayName)).toBeVisible(); + await expect(page.locator(".mx_RoomHeader_heading").getByText(user2.displayName)).toBeVisible(); }); }); diff --git a/playwright/e2e/polls/pollHistory.spec.ts b/playwright/e2e/polls/pollHistory.spec.ts index 458bb544c7c..e9ebf0a30db 100644 --- a/playwright/e2e/polls/pollHistory.spec.ts +++ b/playwright/e2e/polls/pollHistory.spec.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ import { test, expect } from "../../element-web-test"; -import type { Page } from "@playwright/test"; import type { Bot } from "../../pages/bot"; import type { Client } from "../../pages/client"; +import { ElementAppPage } from "../../pages/ElementAppPage"; test.describe("Poll history", () => { type CreatePollOptions = { @@ -66,8 +66,9 @@ test.describe("Poll history", () => { }); }; - async function openPollHistory(page: Page): Promise { - await page.getByRole("button", { name: "Room info" }).click(); + async function openPollHistory(app: ElementAppPage): Promise { + const { page } = app; + await app.toggleRoomInfoPanel(); await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "Poll history" }).click(); } @@ -116,7 +117,7 @@ test.describe("Poll history", () => { await botVoteForOption(bot, roomId, pollId2, pollParams1.options[1].id); await endPoll(bot, roomId, pollId2); - await openPollHistory(page); + await openPollHistory(app); // these polls are also in the timeline // focus on the poll history dialog diff --git a/playwright/e2e/presence/presence.spec.ts b/playwright/e2e/presence/presence.spec.ts index 861181ba56a..e52b97844b8 100644 --- a/playwright/e2e/presence/presence.spec.ts +++ b/playwright/e2e/presence/presence.spec.ts @@ -59,7 +59,7 @@ test.describe("Presence tests", () => { ); await app.client.createRoom({}); // trigger sync - await page.getByRole("button", { name: "Room info" }).click(); + await app.toggleRoomInfoPanel(); await page.locator(".mx_RightPanel").getByText("People").click(); await expect(page.locator(".mx_EntityTile_unreachable")).toContainText("Bob"); await expect(page.locator(".mx_EntityTile_unreachable")).toContainText("User's server unreachable"); diff --git a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts new file mode 100644 index 00000000000..62394cccb5b --- /dev/null +++ b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts @@ -0,0 +1,191 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("editing messages", () => { + test.describe("in threads", () => { + test("An edit of a threaded message makes the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given we have read the thread + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.assertReadThread("Resp1"); + await util.goTo(room1); + + // When a message inside it is edited + await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); + + // Then the room and thread are read + await util.assertStillRead(room2); + await util.goTo(room2); + await util.assertReadThread("Msg1"); + }); + + test("Reading an edit of a threaded message makes the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an edited thread message appears after we read it + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.assertReadThread("Resp1"); + await util.goTo(room1); + await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); + await util.assertStillRead(room2); + + // When I read it + await util.goTo(room2); + await util.openThread("Msg1"); + + // Then the room and thread are still read + await util.assertStillRead(room2); + await util.assertReadThread("Msg1"); + }); + + test("Marking a room as read after an edit in a thread makes it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an edit in a thread is making the room unread + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Msg1", + msg.threadedOff("Msg1", "Resp1"), + msg.editOf("Resp1", "Edit1"), + ]); + await util.assertUnread(room2, 1); + + // When I mark the room as read + await util.markAsRead(room2); + + // Then it is read + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + }); + + test("Editing a thread message after marking as read leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a room is marked as read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.assertUnread(room2, 1); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When a message is edited + await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); + + // Then the room remains read + await util.assertStillRead(room2); + await util.assertReadThread("Msg1"); + }); + + test("A room with an edited threaded message is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an edit in a thread is leaving a room read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.markAsRead(room2); + await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); + await util.assertStillRead(room2); + + // When I restart + await util.saveAndReload(); + + // Then is it still read + await util.assertRead(room2); + }); + + test("A room where all threaded edits are read is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.assertUnread(room2, 1); + await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); + await util.assertUnread(room2, 1); + + await util.goTo(room2); + + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + await util.goTo(room1); // Make sure we are looking at room1 after reload + await util.assertStillRead(room2); + + await util.saveAndReload(); + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + }); + + test("A room where all threaded edits are marked as read is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Msg1", + msg.threadedOff("Msg1", "Resp1"), + msg.editOf("Resp1", "Edit1"), + ]); + await util.assertUnread(room2, 1); + await util.markAsRead(room2); + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + + // When I restart + await util.saveAndReload(); + + // It is still read + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts new file mode 100644 index 00000000000..e03a011a4d6 --- /dev/null +++ b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts @@ -0,0 +1,180 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("editing messages", () => { + test.describe("in the main timeline", () => { + test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { + // Given I am not looking at the room + await util.goTo(room1); + + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + // When an edit appears in the room + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); + + // Then it remains read + await util.assertStillRead(room2); + }); + test("Reading an edit leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { + // Given an edit is making the room unread + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); + await util.assertStillRead(room2); + + // When I read it + await util.goTo(room2); + + // Then the room stays read + await util.assertStillRead(room2); + await util.goTo(room1); + await util.assertStillRead(room2); + }); + test("Editing a message after marking as read leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given the room is marked as read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When a message is edited + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); + + // Then the room remains read + await util.assertStillRead(room2); + }); + test("Editing a reply after reading it makes the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given the room is all read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]); + await util.assertUnread(room2, 2); + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + // When a message is edited + await util.receiveMessages(room2, [msg.editOf("Reply1", "Reply1 Edit1")]); + + // Then it remains read + await util.assertStillRead(room2); + }); + test("Editing a reply after marking as read makes the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a reply is marked as read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]); + await util.assertUnread(room2, 2); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When the reply is edited + await util.receiveMessages(room2, [msg.editOf("Reply1", "Reply1 Edit1")]); + + // Then the room remains read + await util.assertStillRead(room2); + }); + test("A room with an edit is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a message is marked as read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When an edit appears in the room + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); + + // Then it remains read + await util.assertStillRead(room2); + + // And remains so after a reload + await util.saveAndReload(); + await util.assertStillRead(room2); + }); + test("An edited message becomes read if it happens while I am looking", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a message is marked as read + await util.goTo(room2); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertRead(room2); + + // When I see an edit appear in the room I am looking at + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); + + // Then it becomes read + await util.assertStillRead(room2); + }); + test("A room where all edits are read is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a message was edited and read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.editOf("Msg1", "Msg1 Edit1")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.assertRead(room2); + + // When I reload + await util.saveAndReload(); + + // Then the room is still read + await util.assertRead(room2); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts new file mode 100644 index 00000000000..279845f5d2d --- /dev/null +++ b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts @@ -0,0 +1,179 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("editing messages", () => { + test.describe("thread roots", () => { + test("An edit of a thread root leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I have read a thread + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Msg1"); + await util.backToThreadsList(); + await util.assertRead(room2); + await util.goTo(room1); + + // When the thread root is edited + await util.receiveMessages(room2, [msg.editOf("Msg1", "Edit1")]); + + // Then the room is read + await util.assertStillRead(room2); + + // And the thread is read + await util.goTo(room2); + await util.assertStillRead(room2); + await util.assertReadThread("Edit1"); + }); + + test("Reading an edit of a thread root leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a fully-read thread exists + await util.goTo(room2); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.goTo(room1); + await util.assertRead(room2); + + // When the thread root is edited + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); + + // And I read that edit + await util.goTo(room2); + + // Then the room becomes read and stays read + await util.assertStillRead(room2); + await util.goTo(room1); + await util.assertStillRead(room2); + }); + + test("Editing a thread root after reading leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a fully-read thread exists + await util.goTo(room2); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.goTo(room1); + + // When the thread root is edited + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); + + // Then the room stays read + await util.assertStillRead(room2); + }); + + test("Marking a room as read after an edit of a thread root keeps it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a fully-read thread exists + await util.goTo(room2); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.goTo(room1); + await util.assertRead(room2); + + // When the thread root is edited (and I receive another message + // to allow Mark as read) + await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1"), "Msg2"]); + + // And when I mark the room as read + await util.markAsRead(room2); + + // Then the room becomes read and stays read + await util.assertStillRead(room2); + await util.goTo(room1); + await util.assertStillRead(room2); + }); + + test("Editing a thread root that is a reply after marking as read leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread based on a reply exists and is read because it is marked as read + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Msg", + msg.replyTo("Msg", "Reply"), + msg.threadedOff("Reply", "InThread"), + ]); + await util.assertUnread(room2, 2); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When I edit the thread root + await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]); + + // Then the room is read + await util.assertStillRead(room2); + + // And the thread is read + await util.goTo(room2); + await util.assertReadThread("Edited Reply"); + }); + + test("Marking a room as read after an edit of a thread root that is a reply leaves it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread based on a reply exists and the reply has been edited + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Msg", + msg.replyTo("Msg", "Reply"), + msg.threadedOff("Reply", "InThread"), + ]); + await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]); + await util.assertUnread(room2, 2); + + // When I mark the room as read + await util.markAsRead(room2); + + // Then the room and thread are read + await util.assertStillRead(room2); + await util.goTo(room2); + await util.assertReadThread("Edited Reply"); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/editing-messages.spec.ts b/playwright/e2e/read-receipts/editing-messages.spec.ts deleted file mode 100644 index 5005ad62bfb..00000000000 --- a/playwright/e2e/read-receipts/editing-messages.spec.ts +++ /dev/null @@ -1,504 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* See readme.md for tips on writing these tests. */ - -import { test } from "."; - -test.describe("Read receipts", () => { - test.describe("editing messages", () => { - test.describe("in the main timeline", () => { - test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { - // Given I am not looking at the room - await util.goTo(room1); - - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - // When an edit appears in the room - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); - - // Then it remains read - await util.assertStillRead(room2); - }); - test("Reading an edit leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { - // Given an edit is making the room unread - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); - await util.assertStillRead(room2); - - // When I read it - await util.goTo(room2); - - // Then the room stays read - await util.assertStillRead(room2); - await util.goTo(room1); - await util.assertStillRead(room2); - }); - test("Editing a message after marking as read leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given the room is marked as read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When a message is edited - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); - - // Then the room remains read - await util.assertStillRead(room2); - }); - test("Editing a reply after reading it makes the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given the room is all read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]); - await util.assertUnread(room2, 2); - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - // When a message is edited - await util.receiveMessages(room2, [msg.editOf("Reply1", "Reply1 Edit1")]); - - // Then it remains read - await util.assertStillRead(room2); - }); - test("Editing a reply after marking as read makes the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a reply is marked as read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]); - await util.assertUnread(room2, 2); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When the reply is edited - await util.receiveMessages(room2, [msg.editOf("Reply1", "Reply1 Edit1")]); - - // Then the room remains read - await util.assertStillRead(room2); - }); - test("A room with an edit is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a message is marked as read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When an edit appears in the room - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); - - // Then it remains read - await util.assertStillRead(room2); - - // And remains so after a reload - await util.saveAndReload(); - await util.assertStillRead(room2); - }); - test("An edited message becomes read if it happens while I am looking", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a message is marked as read - await util.goTo(room2); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertRead(room2); - - // When I see an edit appear in the room I am looking at - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); - - // Then it becomes read - await util.assertStillRead(room2); - }); - test("A room where all edits are read is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a message was edited and read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.editOf("Msg1", "Msg1 Edit1")]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.assertRead(room2); - - // When I reload - await util.saveAndReload(); - - // Then the room is still read - await util.assertRead(room2); - }); - }); - - test.describe("in threads", () => { - test("An edit of a threaded message makes the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given we have read the thread - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.assertReadThread("Resp1"); - await util.goTo(room1); - - // When a message inside it is edited - await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); - - // Then the room and thread are read - await util.assertStillRead(room2); - await util.goTo(room2); - await util.assertReadThread("Msg1"); - }); - - test("Reading an edit of a threaded message makes the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an edited thread message appears after we read it - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.assertReadThread("Resp1"); - await util.goTo(room1); - await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); - await util.assertStillRead(room2); - - // When I read it - await util.goTo(room2); - await util.openThread("Msg1"); - - // Then the room and thread are still read - await util.assertStillRead(room2); - await util.assertReadThread("Msg1"); - }); - - test("Marking a room as read after an edit in a thread makes it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an edit in a thread is making the room unread - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Msg1", - msg.threadedOff("Msg1", "Resp1"), - msg.editOf("Resp1", "Edit1"), - ]); - await util.assertUnread(room2, 1); - - // When I mark the room as read - await util.markAsRead(room2); - - // Then it is read - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - }); - - test("Editing a thread message after marking as read leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a room is marked as read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 1); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When a message is edited - await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); - - // Then the room remains read - await util.assertStillRead(room2); - await util.assertReadThread("Msg1"); - }); - - test("A room with an edited threaded message is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an edit in a thread is leaving a room read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.markAsRead(room2); - await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); - await util.assertStillRead(room2); - - // When I restart - await util.saveAndReload(); - - // Then is it still read - await util.assertRead(room2); - }); - - test("A room where all threaded edits are read is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 1); - await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); - await util.assertUnread(room2, 1); - - await util.goTo(room2); - - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - await util.goTo(room1); // Make sure we are looking at room1 after reload - await util.assertStillRead(room2); - - await util.saveAndReload(); - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - }); - - test("A room where all threaded edits are marked as read is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Msg1", - msg.threadedOff("Msg1", "Resp1"), - msg.editOf("Resp1", "Edit1"), - ]); - await util.assertUnread(room2, 1); - await util.markAsRead(room2); - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - - // When I restart - await util.saveAndReload(); - - // It is still read - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - }); - }); - - test.describe("thread roots", () => { - test("An edit of a thread root leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I have read a thread - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Msg1"); - await util.backToThreadsList(); - await util.assertRead(room2); - await util.goTo(room1); - - // When the thread root is edited - await util.receiveMessages(room2, [msg.editOf("Msg1", "Edit1")]); - - // Then the room is read - await util.assertStillRead(room2); - - // And the thread is read - await util.goTo(room2); - await util.assertStillRead(room2); - await util.assertReadThread("Edit1"); - }); - - test("Reading an edit of a thread root leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a fully-read thread exists - await util.goTo(room2); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.goTo(room1); - await util.assertRead(room2); - - // When the thread root is edited - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); - - // And I read that edit - await util.goTo(room2); - - // Then the room becomes read and stays read - await util.assertStillRead(room2); - await util.goTo(room1); - await util.assertStillRead(room2); - }); - - test("Editing a thread root after reading leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a fully-read thread exists - await util.goTo(room2); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.goTo(room1); - - // When the thread root is edited - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1")]); - - // Then the room stays read - await util.assertStillRead(room2); - }); - - test("Marking a room as read after an edit of a thread root keeps it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a fully-read thread exists - await util.goTo(room2); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.goTo(room1); - await util.assertRead(room2); - - // When the thread root is edited (and I receive another message - // to allow Mark as read) - await util.receiveMessages(room2, [msg.editOf("Msg1", "Msg1 Edit1"), "Msg2"]); - - // And when I mark the room as read - await util.markAsRead(room2); - - // Then the room becomes read and stays read - await util.assertStillRead(room2); - await util.goTo(room1); - await util.assertStillRead(room2); - }); - - test("Editing a thread root that is a reply after marking as read leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread based on a reply exists and is read because it is marked as read - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Msg", - msg.replyTo("Msg", "Reply"), - msg.threadedOff("Reply", "InThread"), - ]); - await util.assertUnread(room2, 2); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When I edit the thread root - await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]); - - // Then the room is read - await util.assertStillRead(room2); - - // And the thread is read - await util.goTo(room2); - await util.assertReadThread("Edited Reply"); - }); - - test("Marking a room as read after an edit of a thread root that is a reply leaves it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread based on a reply exists and the reply has been edited - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Msg", - msg.replyTo("Msg", "Reply"), - msg.threadedOff("Reply", "InThread"), - ]); - await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]); - await util.assertUnread(room2, 2); - - // When I mark the room as read - await util.markAsRead(room2); - - // Then the room and thread are read - await util.assertStillRead(room2); - await util.goTo(room2); - await util.assertReadThread("Edited Reply"); - }); - }); - }); -}); diff --git a/playwright/e2e/read-receipts/high-level.spec.ts b/playwright/e2e/read-receipts/high-level.spec.ts index e237afd64a8..a3c2c0de3dd 100644 --- a/playwright/e2e/read-receipts/high-level.spec.ts +++ b/playwright/e2e/read-receipts/high-level.spec.ts @@ -19,77 +19,6 @@ limitations under the License. import { customEvent, many, test } from "."; test.describe("Read receipts", () => { - test.describe("Message ordering", () => { - test.describe("in the main timeline", () => { - test.fixme( - "A receipt for the last event in sync order (even with wrong ts) marks a room as read", - () => {}, - ); - test.fixme( - "A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", - () => {}, - ); - }); - - test.describe("in threads", () => { - // These don't pass yet - we need MSC4033 - we don't even know the Sync order yet - test.fixme( - "A receipt for the last event in sync order (even with wrong ts) marks a thread as read", - () => {}, - ); - test.fixme( - "A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", - () => {}, - ); - - // These pass now and should not later - we should use order from MSC4033 instead of ts - // These are broken out - test.fixme( - "A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", - () => {}, - ); - test.fixme( - "A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", - () => {}, - ); - test.fixme( - "A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", - () => {}, - ); - test.fixme( - "A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", - () => {}, - ); - test.fixme( - "A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", - () => {}, - ); - test.fixme( - "A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", - () => {}, - ); - }); - - test.describe("thread roots", () => { - test.fixme( - "A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", - () => {}, - ); - test.fixme( - "A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", - () => {}, - ); - test.fixme( - "A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", - () => {}, - ); - test.fixme( - "A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", - () => {}, - ); - }); - }); - test.describe("Ignored events", () => { test("If all events after receipt are unimportant, the room is read", async ({ roomAlpha: room1, @@ -249,7 +178,6 @@ test.describe("Read receipts", () => { }); test("Paging up to find old threads that were never read keeps the room unread", async ({ - cryptoBackend, roomAlpha: room1, roomBeta: room2, util, @@ -338,7 +266,6 @@ test.describe("Read receipts", () => { }); test("After marking room as read, paging up to find old threads that were never read leaves the room read", async ({ - cryptoBackend, roomAlpha: room1, roomBeta: room2, util, @@ -416,79 +343,4 @@ test.describe("Read receipts", () => { await util.assertReadThread("Root3"); }); }); - - test.describe("Room list order", () => { - test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - page, - }) => { - await util.goTo(room2); - - // Display the unread first room - await util.toggleRoomUnreadOrder(); - await util.receiveMessages(room1, ["Msg1"]); - await page.reload(); - - // Room 1 has an unread message and should be displayed first - await util.assertRoomListOrder([room1, room2]); - }); - - test("Rooms with unread threads appear at the top of room list if 'unread first' is selected", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - await util.goTo(room2); - await util.receiveMessages(room1, ["Msg1"]); - await util.markAsRead(room1); - await util.assertRead(room1); - - // Display the unread first room - await util.toggleRoomUnreadOrder(); - await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]); - await util.saveAndReload(); - - // Room 1 has an unread message and should be displayed first - await util.assertRoomListOrder([room1, room2]); - }); - }); - - test.describe("Notifications", () => { - test.describe("in the main timeline", () => { - test.fixme("A new message that mentions me shows a notification", () => {}); - test.fixme( - "Reading a notifying message reduces the notification count in the room list, space and tab", - () => {}, - ); - test.fixme( - "Reading the last notifying message removes the notification marker from room list, space and tab", - () => {}, - ); - test.fixme("Editing a message to mentions me shows a notification", () => {}); - test.fixme("Reading the last notifying edited message removes the notification marker", () => {}); - test.fixme("Redacting a notifying message removes the notification marker", () => {}); - }); - - test.describe("in threads", () => { - test.fixme("A new threaded message that mentions me shows a notification", () => {}); - test.fixme("Reading a notifying threaded message removes the notification count", () => {}); - test.fixme( - "Notification count remains steady when reading threads that contain seen notifications", - () => {}, - ); - test.fixme( - "Notification count remains steady when paging up thread view even when threads contain seen notifications", - () => {}, - ); - test.fixme( - "Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", - () => {}, - ); - test.fixme("Redacting a notifying threaded message removes the notification marker", () => {}); - }); - }); }); diff --git a/playwright/e2e/read-receipts/index.ts b/playwright/e2e/read-receipts/index.ts index 4dd0450fb9c..1b671929074 100644 --- a/playwright/e2e/read-receipts/index.ts +++ b/playwright/e2e/read-receipts/index.ts @@ -399,11 +399,10 @@ class Helpers { } /** - * Close the threads panel. (Actually, close any right panel, but for these - * tests we only open the threads panel.) + * Close the threads panel. */ async closeThreadsPanel() { - await this.page.locator(".mx_RightPanel").getByLabel("Close").click(); + await this.page.locator(".mx_RoomHeader").getByLabel("Threads").click(); await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible(); } @@ -411,7 +410,7 @@ class Helpers { * Return to the list of threads, given we are viewing a single thread. */ async backToThreadsList() { - await this.page.locator(".mx_RightPanel").getByLabel("Threads").click(); + await this.page.locator(".mx_RoomHeader").getByLabel("Threads").click(); } /** @@ -531,15 +530,14 @@ class Helpers { // whether it's open or not - wait here to give it a chance to settle. await this.page.waitForTimeout(200); - const ariaCurrent = await this.page.getByTestId("threadsButton").getAttribute("aria-current"); - if (ariaCurrent !== "true") { - await this.page.getByTestId("threadsButton").click(); - } - const threadPanel = this.page.locator(".mx_ThreadPanel"); + const isThreadPanelOpen = (await threadPanel.count()) !== 0; + if (!isThreadPanelOpen) { + await this.page.locator(".mx_RoomHeader").getByLabel("Threads").click(); + } await expect(threadPanel).toBeVisible(); await threadPanel.evaluate(($panel) => { - const $button = $panel.querySelector('.mx_BaseCard_back[aria-label="Threads"]'); + const $button = $panel.querySelector('[data-testid="base-card-back-button"]'); // If the Threads back button is present then click it - the // threads button can open either threads list or thread panel if ($button) { diff --git a/playwright/e2e/read-receipts/message-ordering.spec.ts b/playwright/e2e/read-receipts/message-ordering.spec.ts new file mode 100644 index 00000000000..73c640d35a6 --- /dev/null +++ b/playwright/e2e/read-receipts/message-ordering.spec.ts @@ -0,0 +1,92 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("Message ordering", () => { + test.describe("in the main timeline", () => { + test.fixme( + "A receipt for the last event in sync order (even with wrong ts) marks a room as read", + () => {}, + ); + test.fixme( + "A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", + () => {}, + ); + }); + + test.describe("in threads", () => { + // These don't pass yet - we need MSC4033 - we don't even know the Sync order yet + test.fixme( + "A receipt for the last event in sync order (even with wrong ts) marks a thread as read", + () => {}, + ); + test.fixme( + "A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", + () => {}, + ); + + // These pass now and should not later - we should use order from MSC4033 instead of ts + // These are broken out + test.fixme( + "A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", + () => {}, + ); + test.fixme( + "A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", + () => {}, + ); + test.fixme( + "A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", + () => {}, + ); + test.fixme( + "A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", + () => {}, + ); + test.fixme( + "A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", + () => {}, + ); + test.fixme( + "A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", + () => {}, + ); + }); + + test.describe("thread roots", () => { + test.fixme( + "A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", + () => {}, + ); + test.fixme( + "A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", + () => {}, + ); + test.fixme( + "A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", + () => {}, + ); + test.fixme( + "A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", + () => {}, + ); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/new-messages.spec.ts b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts similarity index 55% rename from playwright/e2e/read-receipts/new-messages.spec.ts rename to playwright/e2e/read-receipts/new-messages-in-threads.spec.ts index 97308a4bb29..37b43bae1dc 100644 --- a/playwright/e2e/read-receipts/new-messages.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts @@ -20,151 +20,6 @@ import { many, test } from "."; test.describe("Read receipts", () => { test.describe("new messages", () => { - test.describe("in the main timeline", () => { - test("Receiving a message makes a room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I am in a different room - await util.goTo(room1); - await util.assertRead(room2); - - // When I receive some messages - await util.receiveMessages(room2, ["Msg1"]); - - // Then the room is marked as unread - await util.assertUnread(room2, 1); - }); - test("Reading latest message makes the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I have some unread messages - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - - // When I read the main timeline - await util.goTo(room2); - - // Then the room becomes read - await util.assertRead(room2); - }); - test("Reading an older message leaves the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given there are lots of messages in a room - await util.goTo(room1); - await util.receiveMessages(room2, many("Msg", 30)); - await util.assertUnread(room2, 30); - - // When I jump to one of the older messages - await msg.jumpTo(room2.name, "Msg0001"); - - // Then the room is still unread, but some messages were read - await util.assertUnreadLessThan(room2, 30); - }); - test("Marking a room as read makes it read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { - // Given I have some unread messages - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - - // When I mark the room as read - await util.markAsRead(room2); - - // Then it is read - await util.assertRead(room2); - }); - test("Receiving a new message after marking as read makes it unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I have marked my messages as read - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When I receive a new message - await util.receiveMessages(room2, ["Msg2"]); - - // Then the room is unread - await util.assertUnread(room2, 1); - }); - test("A room with a new message is still unread after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I have an unread message - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - - // When I restart - await util.saveAndReload(); - - // Then I still have an unread message - await util.assertUnread(room2, 1); - }); - test("A room where all messages are read is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I have read all messages - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.assertRead(room2); - - // When I restart - await util.saveAndReload(); - - // Then all messages are still read - await util.assertRead(room2); - }); - test("A room that was marked as read is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I have marked all messages as read - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1"]); - await util.assertUnread(room2, 1); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When I restart - await util.saveAndReload(); - - // Then all messages are still read - await util.assertRead(room2); - }); - }); - test.describe("in threads", () => { test("Receiving a message makes a room unread", async ({ roomAlpha: room1, @@ -450,100 +305,5 @@ test.describe("Read receipts", () => { await util.assertReadThread("Msg1"); }); }); - - test.describe("thread roots", () => { - test("Reading a thread root does not mark the thread as read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread exists - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 1); // (Sanity) - - // When I read the main timeline - await util.goTo(room2); - - // Then room doesn't appear unread but the thread does - await util.assertRead(room2); - await util.assertUnreadThread("Msg1"); - }); - - test("Reading a thread root within the thread view marks it as read in the main timeline", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given lots of messages are on the main timeline, and one has a thread off it - await util.goTo(room1); - await util.receiveMessages(room2, [ - ...many("beforeThread", 30), - "ThreadRoot", - msg.threadedOff("ThreadRoot", "InThread"), - ...many("afterThread", 30), - ]); - await util.assertUnread(room2, 61); // Sanity - - // When I jump to an old message and read the thread - await msg.jumpTo(room2.name, "beforeThread0000"); - // When the thread is opened, the timeline is scrolled until the thread root reached the center - await util.openThread("ThreadRoot"); - - // Then the thread root is marked as read in the main timeline, - // 30 remaining messages are unread - 7 messages are displayed under the thread root - await util.assertUnread(room2, 30 - 7); - }); - - test("Creating a new thread based on a reply makes the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a message and reply exist and are read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]); - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - await util.assertRead(room2); - - // When I receive a thread message created on the reply - await util.receiveMessages(room2, [msg.threadedOff("Reply1", "Resp1")]); - - // Then the thread is unread - await util.goTo(room2); - await util.assertUnreadThread("Reply1"); - }); - - test("Reading a thread whose root is a reply makes the thread read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an unread thread off a reply exists - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Msg1", - msg.replyTo("Msg1", "Reply1"), - msg.threadedOff("Reply1", "Resp1"), - ]); - await util.assertUnread(room2, 2); - await util.goTo(room2); - await util.assertRead(room2); - await util.assertUnreadThread("Reply1"); - - // When I read the thread - await util.openThread("Reply1"); - - // Then the room and thread are read - await util.assertRead(room2); - await util.assertReadThread("Reply1"); - }); - }); }); }); diff --git a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts new file mode 100644 index 00000000000..eb528f28161 --- /dev/null +++ b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts @@ -0,0 +1,168 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { many, test } from "."; + +test.describe("Read receipts", () => { + test.describe("new messages", () => { + test.describe("in the main timeline", () => { + test("Receiving a message makes a room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I am in a different room + await util.goTo(room1); + await util.assertRead(room2); + + // When I receive some messages + await util.receiveMessages(room2, ["Msg1"]); + + // Then the room is marked as unread + await util.assertUnread(room2, 1); + }); + test("Reading latest message makes the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I have some unread messages + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + + // When I read the main timeline + await util.goTo(room2); + + // Then the room becomes read + await util.assertRead(room2); + }); + test("Reading an older message leaves the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given there are lots of messages in a room + await util.goTo(room1); + await util.receiveMessages(room2, many("Msg", 30)); + await util.assertUnread(room2, 30); + + // When I jump to one of the older messages + await msg.jumpTo(room2.name, "Msg0001"); + + // Then the room is still unread, but some messages were read + await util.assertUnreadLessThan(room2, 30); + }); + test("Marking a room as read makes it read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { + // Given I have some unread messages + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + + // When I mark the room as read + await util.markAsRead(room2); + + // Then it is read + await util.assertRead(room2); + }); + test("Receiving a new message after marking as read makes it unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I have marked my messages as read + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When I receive a new message + await util.receiveMessages(room2, ["Msg2"]); + + // Then the room is unread + await util.assertUnread(room2, 1); + }); + test("A room with a new message is still unread after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I have an unread message + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + + // When I restart + await util.saveAndReload(); + + // Then I still have an unread message + await util.assertUnread(room2, 1); + }); + test("A room where all messages are read is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I have read all messages + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.assertRead(room2); + + // When I restart + await util.saveAndReload(); + + // Then all messages are still read + await util.assertRead(room2); + }); + test("A room that was marked as read is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I have marked all messages as read + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1"]); + await util.assertUnread(room2, 1); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When I restart + await util.saveAndReload(); + + // Then all messages are still read + await util.assertRead(room2); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts new file mode 100644 index 00000000000..526bac4bffc --- /dev/null +++ b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts @@ -0,0 +1,118 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { many, test } from "."; + +test.describe("Read receipts", () => { + test.describe("new messages", () => { + test.describe("thread roots", () => { + test("Reading a thread root does not mark the thread as read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread exists + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + await util.assertUnread(room2, 1); // (Sanity) + + // When I read the main timeline + await util.goTo(room2); + + // Then room doesn't appear unread but the thread does + await util.assertRead(room2); + await util.assertUnreadThread("Msg1"); + }); + + test("Reading a thread root within the thread view marks it as read in the main timeline", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given lots of messages are on the main timeline, and one has a thread off it + await util.goTo(room1); + await util.receiveMessages(room2, [ + ...many("beforeThread", 30), + "ThreadRoot", + msg.threadedOff("ThreadRoot", "InThread"), + ...many("afterThread", 30), + ]); + await util.assertUnread(room2, 61); // Sanity + + // When I jump to an old message and read the thread + await msg.jumpTo(room2.name, "beforeThread0000"); + // When the thread is opened, the timeline is scrolled until the thread root reached the center + await util.openThread("ThreadRoot"); + + // Then the thread root is marked as read in the main timeline, + // 30 remaining messages are unread - 7 messages are displayed under the thread root + await util.assertUnread(room2, 30 - 7); + }); + + test("Creating a new thread based on a reply makes the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a message and reply exist and are read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", msg.replyTo("Msg1", "Reply1")]); + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + await util.assertRead(room2); + + // When I receive a thread message created on the reply + await util.receiveMessages(room2, [msg.threadedOff("Reply1", "Resp1")]); + + // Then the thread is unread + await util.goTo(room2); + await util.assertUnreadThread("Reply1"); + }); + + test("Reading a thread whose root is a reply makes the thread read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an unread thread off a reply exists + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Msg1", + msg.replyTo("Msg1", "Reply1"), + msg.threadedOff("Reply1", "Resp1"), + ]); + await util.assertUnread(room2, 2); + await util.goTo(room2); + await util.assertRead(room2); + await util.assertUnreadThread("Reply1"); + + // When I read the thread + await util.openThread("Reply1"); + + // Then the room and thread are read + await util.assertRead(room2); + await util.assertReadThread("Reply1"); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/notifications.spec.ts b/playwright/e2e/read-receipts/notifications.spec.ts new file mode 100644 index 00000000000..5d87de1bb6c --- /dev/null +++ b/playwright/e2e/read-receipts/notifications.spec.ts @@ -0,0 +1,56 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("Notifications", () => { + test.describe("in the main timeline", () => { + test.fixme("A new message that mentions me shows a notification", () => {}); + test.fixme( + "Reading a notifying message reduces the notification count in the room list, space and tab", + () => {}, + ); + test.fixme( + "Reading the last notifying message removes the notification marker from room list, space and tab", + () => {}, + ); + test.fixme("Editing a message to mentions me shows a notification", () => {}); + test.fixme("Reading the last notifying edited message removes the notification marker", () => {}); + test.fixme("Redacting a notifying message removes the notification marker", () => {}); + }); + + test.describe("in threads", () => { + test.fixme("A new threaded message that mentions me shows a notification", () => {}); + test.fixme("Reading a notifying threaded message removes the notification count", () => {}); + test.fixme( + "Notification count remains steady when reading threads that contain seen notifications", + () => {}, + ); + test.fixme( + "Notification count remains steady when paging up thread view even when threads contain seen notifications", + () => {}, + ); + test.fixme( + "Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", + () => {}, + ); + test.fixme("Redacting a notifying threaded message removes the notification marker", () => {}); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/reactions.spec.ts b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts similarity index 56% rename from playwright/e2e/read-receipts/reactions.spec.ts rename to playwright/e2e/read-receipts/reactions-in-threads.spec.ts index 69208e5fc9e..dcd97ac431b 100644 --- a/playwright/e2e/read-receipts/reactions.spec.ts +++ b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts @@ -20,82 +20,6 @@ import { test, expect } from "."; test.describe("Read receipts", () => { test.describe("reactions", () => { - test.describe("in the main timeline", () => { - test("Receiving a reaction to a message does not make a room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - - // When I read the main timeline - await util.goTo(room2); - await util.assertRead(room2); - - await util.goTo(room1); - await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); - await util.assertRead(room2); - }); - test("Reacting to a message after marking as read does not make the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - - await util.markAsRead(room2); - await util.assertRead(room2); - - await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); - await util.assertRead(room2); - }); - test("A room with an unread reaction is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - - await util.markAsRead(room2); - await util.assertRead(room2); - - await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); - await util.assertRead(room2); - - await util.saveAndReload(); - await util.assertRead(room2); - }); - test("A room where all reactions are read is still read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1", "Msg2", msg.reactionTo("Msg2", "🪿")]); - await util.assertUnread(room2, 2); - - await util.markAsRead(room2); - await util.assertRead(room2); - - await util.saveAndReload(); - await util.assertRead(room2); - }); - }); - test.describe("in threads", () => { test("A reaction to a threaded message does not make the room unread", async ({ roomAlpha: room1, @@ -281,97 +205,5 @@ test.describe("Read receipts", () => { await expect(await page.locator(".mx_ThreadPanel").getByLabel("Mae reacted with 😀")).not.toBeVisible(); }); }); - - test.describe("thread roots", () => { - test("A reaction to a thread root does not make the room unread", async ({ - page, - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a read thread root exists - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - - // When someone reacts to it - await util.goTo(room1); - await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); - await page.waitForTimeout(200); - - // Then the room is still read - await util.assertRead(room2); - // as is the thread - await util.assertReadThread("Msg1"); - }); - - test("Reading a reaction to a thread root leaves the room read", async ({ - page, - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a read thread root exists - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Msg1"); - await util.assertRead(room2); - - // And the reaction to it does not make us unread - await util.goTo(room1); - await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - - // When we read the reaction and go away again - await util.goTo(room2); - await util.openThread("Msg1"); - await util.assertRead(room2); - await util.goTo(room1); - await page.waitForTimeout(200); - - // Then the room is still read - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - }); - - test("Reacting to a thread root after marking as read makes the room unread but not the thread", async ({ - page, - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread root exists - await util.goTo(room1); - await util.assertRead(room2); - await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 1); - - // And we have marked the room as read - await util.markAsRead(room2); - await util.assertRead(room2); - await util.assertReadThread("Msg1"); - - // When someone reacts to it - await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); - await page.waitForTimeout(200); - - // Then the room is still read - await util.assertRead(room2); - // as is the thread - await util.assertReadThread("Msg1"); - }); - }); }); }); diff --git a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts new file mode 100644 index 00000000000..54f0c89afe0 --- /dev/null +++ b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts @@ -0,0 +1,99 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("reactions", () => { + test.describe("in the main timeline", () => { + test("Receiving a reaction to a message does not make a room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + + // When I read the main timeline + await util.goTo(room2); + await util.assertRead(room2); + + await util.goTo(room1); + await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); + await util.assertRead(room2); + }); + test("Reacting to a message after marking as read does not make the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + + await util.markAsRead(room2); + await util.assertRead(room2); + + await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); + await util.assertRead(room2); + }); + test("A room with an unread reaction is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + + await util.markAsRead(room2); + await util.assertRead(room2); + + await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); + await util.assertRead(room2); + + await util.saveAndReload(); + await util.assertRead(room2); + }); + test("A room where all reactions are read is still read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1", "Msg2", msg.reactionTo("Msg2", "🪿")]); + await util.assertUnread(room2, 2); + + await util.markAsRead(room2); + await util.assertRead(room2); + + await util.saveAndReload(); + await util.assertRead(room2); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts new file mode 100644 index 00000000000..9c1be63e5b5 --- /dev/null +++ b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts @@ -0,0 +1,115 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("reactions", () => { + test.describe("thread roots", () => { + test("A reaction to a thread root does not make the room unread", async ({ + page, + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a read thread root exists + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + + // When someone reacts to it + await util.goTo(room1); + await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); + await page.waitForTimeout(200); + + // Then the room is still read + await util.assertRead(room2); + // as is the thread + await util.assertReadThread("Msg1"); + }); + + test("Reading a reaction to a thread root leaves the room read", async ({ + page, + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a read thread root exists + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Msg1"); + await util.assertRead(room2); + + // And the reaction to it does not make us unread + await util.goTo(room1); + await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + + // When we read the reaction and go away again + await util.goTo(room2); + await util.openThread("Msg1"); + await util.assertRead(room2); + await util.goTo(room1); + await page.waitForTimeout(200); + + // Then the room is still read + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + }); + + test("Reacting to a thread root after marking as read makes the room unread but not the thread", async ({ + page, + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread root exists + await util.goTo(room1); + await util.assertRead(room2); + await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); + await util.assertUnread(room2, 1); + + // And we have marked the room as read + await util.markAsRead(room2); + await util.assertRead(room2); + await util.assertReadThread("Msg1"); + + // When someone reacts to it + await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); + await page.waitForTimeout(200); + + // Then the room is still read + await util.assertRead(room2); + // as is the thread + await util.assertReadThread("Msg1"); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/redactions.spec.ts b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts similarity index 52% rename from playwright/e2e/read-receipts/redactions.spec.ts rename to playwright/e2e/read-receipts/redactions-in-threads.spec.ts index f7affbed212..323748e7e08 100644 --- a/playwright/e2e/read-receipts/redactions.spec.ts +++ b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts @@ -20,314 +20,6 @@ import { test } from "."; test.describe("Read receipts", () => { test.describe("redactions", () => { - test.describe("in the main timeline", () => { - test("Redacting the message pointed to by my receipt leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given I have read the messages in a room - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - // When the latest message is redacted - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - - // Then the room remains read - await util.assertStillRead(room2); - }); - - test("Reading an unread room after a redaction of the latest message makes it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an unread room - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - - // And the latest message has been redacted - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - - // When I read the room - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - // Then it becomes read - await util.assertStillRead(room2); - }); - test("Reading an unread room after a redaction of an older message makes it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an unread room with an earlier redaction - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); - - // When I read the room - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - // Then it becomes read - await util.assertStillRead(room2); - }); - test("Marking an unread room as read after a redaction makes it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an unread room where latest message is redacted - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 1); - - // When I mark it as read - await util.markAsRead(room2); - - // Then it becomes read - await util.assertRead(room2); - }); - test("Sending and redacting a message after marking the room as read makes it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a room that is marked as read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When a message is sent and then redacted - await util.receiveMessages(room2, ["Msg3"]); - await util.assertUnread(room2, 1); - await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); - - // Then the room is read - await util.assertRead(room2); - }); - test("Redacting a message after marking the room as read leaves it read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a room that is marked as read - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); - await util.assertUnread(room2, 3); - await util.markAsRead(room2); - await util.assertRead(room2); - - // When we redact some messages - await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); - await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); - - // Then it is still read - await util.assertStillRead(room2); - }); - test("Redacting one of the unread messages reduces the unread count", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an unread room - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); - await util.assertUnread(room2, 3); - - // When I redact a non-latest message - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - - // Then the unread count goes down - await util.assertUnread(room2, 2); - - // And when I redact the latest message - await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); - - // Then the unread count goes down again - await util.assertUnread(room2, 1); - }); - test("Redacting one of the unread messages reduces the unread count after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given unread count was reduced by redacting messages - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); - await util.assertUnread(room2, 3); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); - await util.assertUnread(room2, 1); - - // When I restart - await util.saveAndReload(); - - // Then the unread count is still reduced - await util.assertUnread(room2, 1); - }); - test("Redacting all unread messages makes the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an unread room - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - - // When I redact all the unread messages - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); - - // Then the room is back to being read - await util.assertRead(room2); - }); - test("Redacting all unread messages makes the room read after restart", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given all unread messages were redacted - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); - await util.assertRead(room2); - - // When I restart - await util.saveAndReload(); - - // Then the room is still read - await util.assertRead(room2); - }); - test("Reacting to a redacted message leaves the room read", async ({ - page, - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a redacted message exists - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 1); - - // And the room is read - await util.goTo(room2); - await util.assertRead(room2); - await page.waitForTimeout(200); - await util.goTo(room1); - - // When I react to the redacted message - await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); - - // Then the room is still read - await util.assertStillRead(room2); - }); - test("Editing a redacted message leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a redacted message exists - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 1); - - // And the room is read - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - // When I attempt to edit the redacted message - await util.receiveMessages(room2, [msg.editOf("Msg2", "Msg2 is BACK")]); - - // Then the room is still read - await util.assertStillRead(room2); - }); - test("A reply to a redacted message makes the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a message was redacted - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 1); - - // And the room is read - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - - // When I receive a reply to the redacted message - await util.receiveMessages(room2, [msg.replyTo("Msg2", "Reply to Msg2")]); - - // Then the room is unread - await util.assertUnread(room2, 1); - }); - test("Reading a reply to a redacted message marks the room as read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given someone replied to a redacted message - await util.goTo(room1); - await util.receiveMessages(room2, ["Msg1", "Msg2"]); - await util.assertUnread(room2, 2); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.assertRead(room2); - await util.goTo(room1); - await util.receiveMessages(room2, [msg.replyTo("Msg2", "Reply to Msg2")]); - await util.assertUnread(room2, 1); - - // When I read the reply - await util.goTo(room2); - await util.assertRead(room2); - - // Then the room is unread - await util.goTo(room1); - await util.assertStillRead(room2); - }); - }); - test.describe("in threads", () => { test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({ roomAlpha: room1, @@ -866,214 +558,5 @@ test.describe("Read receipts", () => { await util.assertReadThread("Root"); }); }); - - test.describe("thread roots", () => { - test("Redacting a thread root after it was read leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - test.slow(); - - // Given a thread exists and is read - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Root", - msg.threadedOff("Root", "Msg2"), - msg.threadedOff("Root", "Msg3"), - ]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Root"); - await util.assertRead(room2); - await util.assertReadThread("Root"); - - // When someone redacts the thread root - await util.receiveMessages(room2, [msg.redactionOf("Root")]); - - // Then the room is still read - await util.assertStillRead(room2); - }); - - /* - * Disabled for the same reason as "A thread with a read redaction is still read after restart" - * above - */ - test.skip("Redacting a thread root still allows us to read the thread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given an unread thread exists - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Root", - msg.threadedOff("Root", "Msg2"), - msg.threadedOff("Root", "Msg3"), - ]); - await util.assertUnread(room2, 1); - - // When someone redacts the thread root - await util.receiveMessages(room2, [msg.redactionOf("Root")]); - - // Then the room is still unread - await util.assertUnread(room2, 1); - - // And I can open the thread and read it - await util.goTo(room2); - await util.assertRead(room2); - // The redacted message gets collapsed into, "foo was invited, joined and removed a message" - await util.openCollapsedMessage(1); - await util.openThread("Message deleted"); - await util.assertRead(room2); - await util.assertReadThread("Root"); - }); - - test("Sending a threaded message onto a redacted thread root leaves the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread exists, is read and its root is redacted - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Root", - msg.threadedOff("Root", "Msg2"), - msg.threadedOff("Root", "Msg3"), - ]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Root"); - await util.assertRead(room2); - await util.assertReadThread("Root"); - await util.receiveMessages(room2, [msg.redactionOf("Root")]); - - // When we receive a new message on it - await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg4")]); - - // Then the room is read but the thread is unread - await util.assertRead(room2); - await util.goTo(room2); - await util.assertUnreadThread("Message deleted"); - }); - - test("Reacting to a redacted thread root leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread exists, is read and the root was redacted - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Root", - msg.threadedOff("Root", "Msg2"), - msg.threadedOff("Root", "Msg3"), - ]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Root"); - await util.assertRead(room2); - await util.assertReadThread("Root"); - await util.receiveMessages(room2, [msg.redactionOf("Root")]); - - // When I react to the old root - await util.receiveMessages(room2, [msg.reactionTo("Root", "y")]); - - // Then the room is still read - await util.assertRead(room2); - await util.assertReadThread("Root"); - }); - - test("Editing a redacted thread root leaves the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread exists, is read and the root was redacted - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Root", - msg.threadedOff("Root", "Msg2"), - msg.threadedOff("Root", "Msg3"), - ]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Root"); - await util.assertRead(room2); - await util.assertReadThread("Root"); - await util.receiveMessages(room2, [msg.redactionOf("Root")]); - - // When I edit the old root - await util.receiveMessages(room2, [msg.editOf("Root", "New Root")]); - - // Then the room is still read - await util.assertRead(room2); - // as is the thread - await util.assertReadThread("Root"); - }); - - test("Replying to a redacted thread root makes the room unread", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread exists, is read and the root was redacted - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Root", - msg.threadedOff("Root", "Msg2"), - msg.threadedOff("Root", "Msg3"), - ]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Root"); - await util.assertRead(room2); - await util.assertReadThread("Root"); - await util.receiveMessages(room2, [msg.redactionOf("Root")]); - - // When I reply to the old root - await util.receiveMessages(room2, [msg.replyTo("Root", "Reply!")]); - - // Then the room is unread - await util.assertUnread(room2, 1); - }); - - test("Reading a reply to a redacted thread root makes the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given a thread exists, is read and the root was redacted, and - // someone replied to it - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Root", - msg.threadedOff("Root", "Msg2"), - msg.threadedOff("Root", "Msg3"), - ]); - await util.assertUnread(room2, 1); - await util.goTo(room2); - await util.openThread("Root"); - await util.assertRead(room2); - await util.assertReadThread("Root"); - await util.receiveMessages(room2, [msg.redactionOf("Root")]); - await util.assertStillRead(room2); - await util.receiveMessages(room2, [msg.replyTo("Root", "Reply!")]); - await util.assertUnread(room2, 1); - - // When I read the room - await util.goTo(room2); - - // Then it becomes read - await util.assertRead(room2); - }); - }); }); }); diff --git a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts new file mode 100644 index 00000000000..cb7393a63fd --- /dev/null +++ b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts @@ -0,0 +1,331 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("redactions", () => { + test.describe("in the main timeline", () => { + test("Redacting the message pointed to by my receipt leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given I have read the messages in a room + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + // When the latest message is redacted + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + + // Then the room remains read + await util.assertStillRead(room2); + }); + + test("Reading an unread room after a redaction of the latest message makes it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an unread room + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + + // And the latest message has been redacted + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + + // When I read the room + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + // Then it becomes read + await util.assertStillRead(room2); + }); + test("Reading an unread room after a redaction of an older message makes it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an unread room with an earlier redaction + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); + + // When I read the room + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + // Then it becomes read + await util.assertStillRead(room2); + }); + test("Marking an unread room as read after a redaction makes it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an unread room where latest message is redacted + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 1); + + // When I mark it as read + await util.markAsRead(room2); + + // Then it becomes read + await util.assertRead(room2); + }); + test("Sending and redacting a message after marking the room as read makes it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a room that is marked as read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When a message is sent and then redacted + await util.receiveMessages(room2, ["Msg3"]); + await util.assertUnread(room2, 1); + await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); + + // Then the room is read + await util.assertRead(room2); + }); + test("Redacting a message after marking the room as read leaves it read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a room that is marked as read + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); + await util.assertUnread(room2, 3); + await util.markAsRead(room2); + await util.assertRead(room2); + + // When we redact some messages + await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); + await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); + + // Then it is still read + await util.assertStillRead(room2); + }); + test("Redacting one of the unread messages reduces the unread count", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an unread room + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); + await util.assertUnread(room2, 3); + + // When I redact a non-latest message + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + + // Then the unread count goes down + await util.assertUnread(room2, 2); + + // And when I redact the latest message + await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); + + // Then the unread count goes down again + await util.assertUnread(room2, 1); + }); + test("Redacting one of the unread messages reduces the unread count after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given unread count was reduced by redacting messages + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); + await util.assertUnread(room2, 3); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); + await util.assertUnread(room2, 1); + + // When I restart + await util.saveAndReload(); + + // Then the unread count is still reduced + await util.assertUnread(room2, 1); + }); + test("Redacting all unread messages makes the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an unread room + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + + // When I redact all the unread messages + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); + + // Then the room is back to being read + await util.assertRead(room2); + }); + test("Redacting all unread messages makes the room read after restart", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given all unread messages were redacted + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.receiveMessages(room2, [msg.redactionOf("Msg1")]); + await util.assertRead(room2); + + // When I restart + await util.saveAndReload(); + + // Then the room is still read + await util.assertRead(room2); + }); + test("Reacting to a redacted message leaves the room read", async ({ + page, + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a redacted message exists + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 1); + + // And the room is read + await util.goTo(room2); + await util.assertRead(room2); + await page.waitForTimeout(200); + await util.goTo(room1); + + // When I react to the redacted message + await util.receiveMessages(room2, [msg.reactionTo("Msg2", "🪿")]); + + // Then the room is still read + await util.assertStillRead(room2); + }); + test("Editing a redacted message leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a redacted message exists + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 1); + + // And the room is read + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + // When I attempt to edit the redacted message + await util.receiveMessages(room2, [msg.editOf("Msg2", "Msg2 is BACK")]); + + // Then the room is still read + await util.assertStillRead(room2); + }); + test("A reply to a redacted message makes the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a message was redacted + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 1); + + // And the room is read + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + + // When I receive a reply to the redacted message + await util.receiveMessages(room2, [msg.replyTo("Msg2", "Reply to Msg2")]); + + // Then the room is unread + await util.assertUnread(room2, 1); + }); + test("Reading a reply to a redacted message marks the room as read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given someone replied to a redacted message + await util.goTo(room1); + await util.receiveMessages(room2, ["Msg1", "Msg2"]); + await util.assertUnread(room2, 2); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.assertRead(room2); + await util.goTo(room1); + await util.receiveMessages(room2, [msg.replyTo("Msg2", "Reply to Msg2")]); + await util.assertUnread(room2, 1); + + // When I read the reply + await util.goTo(room2); + await util.assertRead(room2); + + // Then the room is unread + await util.goTo(room1); + await util.assertStillRead(room2); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts new file mode 100644 index 00000000000..0ded3957fbc --- /dev/null +++ b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts @@ -0,0 +1,232 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("redactions", () => { + test.describe("thread roots", () => { + test("Redacting a thread root after it was read leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + test.slow(); + + // Given a thread exists and is read + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Root", + msg.threadedOff("Root", "Msg2"), + msg.threadedOff("Root", "Msg3"), + ]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Root"); + await util.assertRead(room2); + await util.assertReadThread("Root"); + + // When someone redacts the thread root + await util.receiveMessages(room2, [msg.redactionOf("Root")]); + + // Then the room is still read + await util.assertStillRead(room2); + }); + + /* + * Disabled for the same reason as "A thread with a read redaction is still read after restart" + * above + */ + test.skip("Redacting a thread root still allows us to read the thread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given an unread thread exists + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Root", + msg.threadedOff("Root", "Msg2"), + msg.threadedOff("Root", "Msg3"), + ]); + await util.assertUnread(room2, 1); + + // When someone redacts the thread root + await util.receiveMessages(room2, [msg.redactionOf("Root")]); + + // Then the room is still unread + await util.assertUnread(room2, 1); + + // And I can open the thread and read it + await util.goTo(room2); + await util.assertRead(room2); + // The redacted message gets collapsed into, "foo was invited, joined and removed a message" + await util.openCollapsedMessage(1); + await util.openThread("Message deleted"); + await util.assertRead(room2); + await util.assertReadThread("Root"); + }); + + test("Sending a threaded message onto a redacted thread root leaves the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread exists, is read and its root is redacted + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Root", + msg.threadedOff("Root", "Msg2"), + msg.threadedOff("Root", "Msg3"), + ]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Root"); + await util.assertRead(room2); + await util.assertReadThread("Root"); + await util.receiveMessages(room2, [msg.redactionOf("Root")]); + + // When we receive a new message on it + await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg4")]); + + // Then the room is read but the thread is unread + await util.assertRead(room2); + await util.goTo(room2); + await util.assertUnreadThread("Message deleted"); + }); + + test("Reacting to a redacted thread root leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread exists, is read and the root was redacted + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Root", + msg.threadedOff("Root", "Msg2"), + msg.threadedOff("Root", "Msg3"), + ]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Root"); + await util.assertRead(room2); + await util.assertReadThread("Root"); + await util.receiveMessages(room2, [msg.redactionOf("Root")]); + + // When I react to the old root + await util.receiveMessages(room2, [msg.reactionTo("Root", "y")]); + + // Then the room is still read + await util.assertRead(room2); + await util.assertReadThread("Root"); + }); + + test("Editing a redacted thread root leaves the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread exists, is read and the root was redacted + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Root", + msg.threadedOff("Root", "Msg2"), + msg.threadedOff("Root", "Msg3"), + ]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Root"); + await util.assertRead(room2); + await util.assertReadThread("Root"); + await util.receiveMessages(room2, [msg.redactionOf("Root")]); + + // When I edit the old root + await util.receiveMessages(room2, [msg.editOf("Root", "New Root")]); + + // Then the room is still read + await util.assertRead(room2); + // as is the thread + await util.assertReadThread("Root"); + }); + + test("Replying to a redacted thread root makes the room unread", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread exists, is read and the root was redacted + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Root", + msg.threadedOff("Root", "Msg2"), + msg.threadedOff("Root", "Msg3"), + ]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Root"); + await util.assertRead(room2); + await util.assertReadThread("Root"); + await util.receiveMessages(room2, [msg.redactionOf("Root")]); + + // When I reply to the old root + await util.receiveMessages(room2, [msg.replyTo("Root", "Reply!")]); + + // Then the room is unread + await util.assertUnread(room2, 1); + }); + + test("Reading a reply to a redacted thread root makes the room read", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + // Given a thread exists, is read and the root was redacted, and + // someone replied to it + await util.goTo(room1); + await util.receiveMessages(room2, [ + "Root", + msg.threadedOff("Root", "Msg2"), + msg.threadedOff("Root", "Msg3"), + ]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThread("Root"); + await util.assertRead(room2); + await util.assertReadThread("Root"); + await util.receiveMessages(room2, [msg.redactionOf("Root")]); + await util.assertStillRead(room2); + await util.receiveMessages(room2, [msg.replyTo("Root", "Reply!")]); + await util.assertUnread(room2, 1); + + // When I read the room + await util.goTo(room2); + + // Then it becomes read + await util.assertRead(room2); + }); + }); + }); +}); diff --git a/playwright/e2e/read-receipts/room-list-order.spec.ts b/playwright/e2e/read-receipts/room-list-order.spec.ts new file mode 100644 index 00000000000..2b430229189 --- /dev/null +++ b/playwright/e2e/read-receipts/room-list-order.spec.ts @@ -0,0 +1,61 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { test } from "."; + +test.describe("Read receipts", () => { + test.describe("Room list order", () => { + test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + page, + }) => { + await util.goTo(room2); + + // Display the unread first room + await util.toggleRoomUnreadOrder(); + await util.receiveMessages(room1, ["Msg1"]); + await page.reload(); + + // Room 1 has an unread message and should be displayed first + await util.assertRoomListOrder([room1, room2]); + }); + + test("Rooms with unread threads appear at the top of room list if 'unread first' is selected", async ({ + roomAlpha: room1, + roomBeta: room2, + util, + msg, + }) => { + await util.goTo(room2); + await util.receiveMessages(room1, ["Msg1"]); + await util.markAsRead(room1); + await util.assertRead(room1); + + // Display the unread first room + await util.toggleRoomUnreadOrder(); + await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]); + await util.saveAndReload(); + + // Room 1 has an unread message and should be displayed first + await util.assertRoomListOrder([room1, room2]); + }); + }); +}); diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index 84e7614e8ef..52dd1133143 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -50,7 +50,7 @@ test.describe("FilePanel", () => { test.describe("render", () => { test("should render empty state", async ({ page }) => { // Wait until the information about the empty state is rendered - await expect(page.locator(".mx_FilePanel_empty")).toBeVisible(); + await expect(page.locator(".mx_EmptyState")).toBeVisible(); // Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332 await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); diff --git a/playwright/e2e/right-panel/notification-panel.spec.ts b/playwright/e2e/right-panel/notification-panel.spec.ts index 6223c1c13f7..aa7dedf73ab 100644 --- a/playwright/e2e/right-panel/notification-panel.spec.ts +++ b/playwright/e2e/right-panel/notification-panel.spec.ts @@ -35,7 +35,7 @@ test.describe("NotificationPanel", () => { await page.getByRole("button", { name: "Notifications" }).click(); // Wait until the information about the empty state is rendered - await expect(page.locator(".mx_NotificationPanel_empty")).toBeVisible(); + await expect(page.locator(".mx_EmptyState")).toBeVisible(); // Take a snapshot of RightPanel await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 4f578748d6e..f282d83d62c 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -63,9 +63,9 @@ test.describe("RightPanel", () => { await app.closeDialog(); // Close and reopen the right panel to render the room address - await page.getByRole("button", { name: "Room info" }).click(); + await app.toggleRoomInfoPanel(); await expect(page.locator(".mx_RightPanel")).not.toBeVisible(); - await page.getByRole("button", { name: "Room info" }).click(); + await app.toggleRoomInfoPanel(); await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-name-and-address.png"); }); @@ -104,26 +104,26 @@ test.describe("RightPanel", () => { await page.getByRole("menuitem", { name: "Files" }).click(); await expect(page.locator(".mx_FilePanel")).toBeVisible(); - await expect(page.locator(".mx_FilePanel_empty")).toBeVisible(); + await expect(page.locator(".mx_EmptyState")).toBeVisible(); - await page.getByRole("button", { name: "Room information" }).click(); + await page.getByTestId("base-card-back-button").click(); await checkRoomSummaryCard(page, ROOM_NAME); }); test("should handle viewing room member", async ({ page, app }) => { await viewRoomSummaryByName(page, app, ROOM_NAME); - await page.getByRole("menuitem", { name: "People" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("People").click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); await getMemberTileByName(page, NAME).click(); await expect(page.locator(".mx_UserInfo")).toBeVisible(); await expect(page.locator(".mx_UserInfo_profile").getByText(NAME)).toBeVisible(); - await page.getByRole("button", { name: "Room members" }).click(); + await page.getByTestId("base-card-back-button").click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); - await page.getByRole("button", { name: "Room information" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("Info").click(); await checkRoomSummaryCard(page, ROOM_NAME); }); }); @@ -138,14 +138,12 @@ test.describe("RightPanel", () => { .getByRole("button", { name: /\d member/ }) .click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); - await expect(page.locator(".mx_SpaceScopeHeader").getByText(SPACE_NAME)).toBeVisible(); await getMemberTileByName(page, NAME).click(); await expect(page.locator(".mx_UserInfo")).toBeVisible(); await expect(page.locator(".mx_UserInfo_profile").getByText(NAME)).toBeVisible(); - await expect(page.locator(".mx_SpaceScopeHeader").getByText(SPACE_NAME)).toBeVisible(); - await page.getByRole("button", { name: "Back" }).click(); + await page.getByTestId("base-card-back-button").click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); }); }); diff --git a/playwright/e2e/right-panel/utils.ts b/playwright/e2e/right-panel/utils.ts index a8dac8394d0..5e2e39be0d4 100644 --- a/playwright/e2e/right-panel/utils.ts +++ b/playwright/e2e/right-panel/utils.ts @@ -20,7 +20,7 @@ import { ElementAppPage } from "../../pages/ElementAppPage"; export async function viewRoomSummaryByName(page: Page, app: ElementAppPage, name: string): Promise { await app.viewRoomByName(name); - await page.getByRole("button", { name: "Room info" }).click(); + await app.toggleRoomInfoPanel(); return checkRoomSummaryCard(page, name); } diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts index 4008517d093..ca49f1190bc 100644 --- a/playwright/e2e/room/room-header.spec.ts +++ b/playwright/e2e/room/room-header.spec.ts @@ -18,7 +18,6 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { ElementAppPage } from "../../pages/ElementAppPage"; -import type { Container } from "../../../src/stores/widgets/types"; test.describe("Room Header", () => { test.use({ @@ -33,24 +32,28 @@ test.describe("Room Header", () => { await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); - const header = page.locator(".mx_LegacyRoomHeader"); - // Names (aria-label) of every button rendered on mx_LegacyRoomHeader by default - const expectedButtonNames = [ - "Room options", // The room name button next to the room avatar, which renders dropdown menu on click - "Voice call", - "Video call", - "Search", - "Threads", - "Notifications", - "Room info", - ]; - - // Assert they are found and visible - for (const name of expectedButtonNames) { - await expect(header.getByRole("button", { name })).toBeVisible(); - } + const header = page.locator(".mx_RoomHeader"); + + // There's two room info button - the header itself and the i button + const infoButtons = header.getByRole("button", { name: "Room info" }); + await expect(infoButtons).toHaveCount(2); + await expect(infoButtons.first()).toBeVisible(); + await expect(infoButtons.last()).toBeVisible(); + + // Memberlist button + await expect(header.locator(".mx_FacePile")).toBeVisible(); - // Assert that just those seven buttons exist on mx_LegacyRoomHeader by default + // There should be both a voice and a video call button + // but they'll be disabled + const callButtons = header.getByRole("button", { name: "There's no one here to call" }); + await expect(callButtons).toHaveCount(2); + await expect(callButtons.first()).toBeVisible(); + await expect(callButtons.last()).toBeVisible(); + + await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); + + // Assert that there are six buttons in total await expect(header.getByRole("button")).toHaveCount(7); await expect(header).toMatchScreenshot("room-header.png"); @@ -67,14 +70,15 @@ test.describe("Room Header", () => { await app.client.createRoom({ name: LONG_ROOM_NAME }); await app.viewRoomByName(LONG_ROOM_NAME); - const header = page.locator(".mx_LegacyRoomHeader"); + const header = page.locator(".mx_RoomHeader"); // Wait until the room name is set - await expect(page.locator(".mx_LegacyRoomHeader_nametext").getByText(LONG_ROOM_NAME)).toBeVisible(); + await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed // Note these assertions do not check the size of mx_LegacyRoomHeader_name button - const buttons = page.locator(".mx_LegacyRoomHeader_button"); - await expect(buttons).toHaveCount(6); + const buttons = header.locator(".mx_Flex").getByRole("button"); + await expect(buttons).toHaveCount(5); + for (const button of await buttons.all()) { await expect(button).toBeVisible(); await expect(button).toHaveCSS("height", "32px"); @@ -83,44 +87,6 @@ test.describe("Room Header", () => { await expect(header).toMatchScreenshot("room-header-long-name.png"); }); - - test("should have buttons highlighted by being clicked", async ({ page, app, user }) => { - await app.client.createRoom({ name: "Test Room" }); - await app.viewRoomByName("Test Room"); - - const header = page.locator(".mx_LegacyRoomHeader"); - // Check these buttons - const buttonsHighlighted = ["Threads", "Notifications", "Room info"]; - - for (const name of buttonsHighlighted) { - await header.getByRole("button", { name: name }).click(); // Highlight the button - } - - await expect(header).toMatchScreenshot("room-header-highlighted.png"); - }); - }); - - test.describe("with feature_pinning enabled", () => { - test.use({ labsFlags: ["feature_pinning"] }); - - test("should render the pin button for pinned messages card", async ({ page, app, user }) => { - await app.client.createRoom({ name: "Test Room" }); - await app.viewRoomByName("Test Room"); - - const composer = app.getComposer().locator("[contenteditable]"); - await composer.fill("Test message"); - await composer.press("Enter"); - - const lastTile = page.locator(".mx_EventTile_last"); - await lastTile.hover(); - await lastTile.getByRole("button", { name: "Options" }).click(); - - await page.getByRole("menuitem", { name: "Pin" }).click(); - - await expect( - page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Pinned messages" }), - ).toBeVisible(); - }); }); test.describe("with a video room", () => { @@ -141,30 +107,27 @@ test.describe("Room Header", () => { test.describe("and with feature_notifications enabled", () => { test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] }); - test("should render buttons for room options, beta pill, invite, chat, and room info", async ({ - page, - app, - user, - }) => { + test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => { await createVideoRoom(page, app); - const header = page.locator(".mx_LegacyRoomHeader"); - // Names (aria-label) of the buttons on the video room header - const expectedButtonNames = [ - "Room options", - "Video rooms are a beta feature Click for more info", // Beta pill - "Invite", - "Chat", - "Room info", - ]; - - // Assert they are found and visible - for (const name of expectedButtonNames) { - await expect(header.getByRole("button", { name })).toBeVisible(); - } + const header = page.locator(".mx_RoomHeader"); + + // There's two room info button - the header itself and the i button + const infoButtons = header.getByRole("button", { name: "Room info" }); + await expect(infoButtons).toHaveCount(2); + await expect(infoButtons.first()).toBeVisible(); + await expect(infoButtons.last()).toBeVisible(); + + // Facepile + await expect(header.locator(".mx_FacePile")).toBeVisible(); + + // Chat, Threads and Notification buttons + await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); // Assert that there is not a button except those buttons - await expect(header.getByRole("button")).toHaveCount(7); + await expect(header.getByRole("button")).toHaveCount(6); await expect(header).toMatchScreenshot("room-header-video-room.png"); }); @@ -177,7 +140,7 @@ test.describe("Room Header", () => { }) => { await createVideoRoom(page, app); - await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Chat" }).click(); + await page.locator(".mx_RoomHeader").getByRole("button", { name: "Chat" }).click(); // Assert that the call view is still visible await expect(page.locator(".mx_CallView")).toBeVisible(); @@ -188,114 +151,4 @@ test.describe("Room Header", () => { ).toBeVisible(); }); }); - - test.describe("with a widget", () => { - const ROOM_NAME = "Test Room with a widget"; - const WIDGET_ID = "fake-widget"; - const WIDGET_HTML = ` - - - Fake Widget - - - Hello World - - - `; - - test.beforeEach(async ({ page, app, user, webserver }) => { - const widgetUrl = webserver.start(WIDGET_HTML); - const roomId = await app.client.createRoom({ name: ROOM_NAME }); - - // setup widget via state event - await app.client.evaluate( - async (matrixClient, { roomId, widgetUrl, id }) => { - await matrixClient.sendStateEvent( - roomId, - "im.vector.modular.widgets", - { - id, - creatorUserId: "somebody", - type: "widget", - name: "widget", - url: widgetUrl, - }, - id, - ); - await matrixClient.sendStateEvent( - roomId, - "io.element.widgets.layout", - { - widgets: { - [id]: { - container: "top" as Container, - index: 1, - width: 100, - height: 0, - }, - }, - }, - "", - ); - }, - { - roomId, - widgetUrl, - id: WIDGET_ID, - }, - ); - - // open the room - await app.viewRoomByName(ROOM_NAME); - }); - - test("should highlight the apps button", async ({ page, app, user }) => { - // Assert that AppsDrawer is rendered - await expect(page.locator(".mx_AppsDrawer")).toBeVisible(); - - const header = page.locator(".mx_LegacyRoomHeader"); - // Assert that "Hide Widgets" button is rendered and aria-checked is set to true - await expect(header.getByRole("button", { name: "Hide Widgets" })).toHaveAttribute("aria-checked", "true"); - - await expect(header).toMatchScreenshot("room-header-with-apps-button-highlighted.png"); - }); - - test("should support hiding a widget", async ({ page, app, user }) => { - await expect(page.locator(".mx_AppsDrawer")).toBeVisible(); - - const header = page.locator(".mx_LegacyRoomHeader"); - // Click the apps button to hide AppsDrawer - await header.getByRole("button", { name: "Hide Widgets" }).click(); - - // Assert that "Show widgets" button is rendered and aria-checked is set to false - await expect(header.getByRole("button", { name: "Show Widgets" })).toHaveAttribute("aria-checked", "false"); - - // Assert that AppsDrawer is not rendered - await expect(page.locator(".mx_AppsDrawer")).not.toBeVisible(); - - await expect(header).toMatchScreenshot("room-header-with-apps-button-not-highlighted.png"); - }); - }); - - test.describe("with encryption", () => { - test("should render the E2E icon and the buttons", async ({ page, app, user }) => { - // Create an encrypted room - await app.client.createRoom({ - name: "Test Encrypted Room", - initial_state: [ - { - type: "m.room.encryption", - state_key: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - ], - }); - await app.viewRoomByName("Test Encrypted Room"); - - const header = page.locator(".mx_LegacyRoomHeader"); - await expect(header).toMatchScreenshot("encrypted-room-header.png"); - }); - }); }); diff --git a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab.spec.ts deleted file mode 100644 index 7e16d739558..00000000000 --- a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -/* -Copyright 2023 Suguru Hirahara - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { test, expect } from "../../element-web-test"; -import { SettingLevel } from "../../../src/settings/SettingLevel"; - -test.describe("Appearance user settings tab", () => { - test.use({ - displayName: "Hanako", - }); - - test("should be rendered properly", async ({ page, user, app }) => { - const tab = await app.settings.openUserSettings("Appearance"); - - // Click "Show advanced" link button - await tab.getByRole("button", { name: "Show advanced" }).click(); - - // Assert that "Hide advanced" link button is rendered - await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible(); - - await expect(tab).toMatchScreenshot("appearance-tab.png"); - }); - - test("should support switching layouts", async ({ page, user, app }) => { - // Create and view a room first - await app.client.createRoom({ name: "Test Room" }); - await app.viewRoomByName("Test Room"); - - await app.settings.openUserSettings("Appearance"); - - const buttons = page.locator(".mx_LayoutSwitcher_RadioButton"); - - // Assert that the layout selected by default is "Modern" - await expect( - buttons.locator(".mx_StyledRadioButton_enabled", { - hasText: "Modern", - }), - ).toBeVisible(); - - // Assert that the room layout is set to group (modern) layout - await expect(page.locator(".mx_RoomView_body[data-layout='group']")).toBeVisible(); - - // Select the first layout - await buttons.first().click(); - // Assert that the layout selected is "IRC (Experimental)" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible(); - - // Assert that the room layout is set to IRC layout - await expect(page.locator(".mx_RoomView_body[data-layout='irc']")).toBeVisible(); - - // Select the last layout - await buttons.last().click(); - - // Assert that the layout selected is "Message bubbles" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible(); - - // Assert that the room layout is set to bubble layout - await expect(page.locator(".mx_RoomView_body[data-layout='bubble']")).toBeVisible(); - }); - - test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); - - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); - await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); - - // Default browser font size is 16px and the select value is 0 - // -4 value is 12px - await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); - - await expect(page).toMatchScreenshot("window-12px.png"); - }); - - test("should support enabling compact group (modern) layout", async ({ page, app, user }) => { - // Create and view a room first - await app.client.createRoom({ name: "Test Room" }); - await app.viewRoomByName("Test Room"); - - await app.settings.openUserSettings("Appearance"); - - // Click "Show advanced" link button - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - await tab.getByRole("button", { name: "Show advanced" }).click(); - - await tab.locator("label", { hasText: "Use a more compact 'Modern' layout" }).click(); - - // Assert that the room layout is set to compact group (modern) layout - await expect(page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout")).toBeVisible(); - }); - - test("should disable compact group (modern) layout option on IRC layout and bubble layout", async ({ - page, - app, - user, - }) => { - await app.settings.openUserSettings("Appearance"); - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - - const checkDisabled = async () => { - await expect(tab.getByRole("checkbox", { name: "Use a more compact 'Modern' layout" })).toBeDisabled(); - }; - - // Click "Show advanced" link button - await tab.getByRole("button", { name: "Show advanced" }).click(); - - const buttons = page.locator(".mx_LayoutSwitcher_RadioButton"); - - // Enable IRC layout - await buttons.first().click(); - - // Assert that the layout selected is "IRC (Experimental)" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible(); - - await checkDisabled(); - - // Enable bubble layout - await buttons.last().click(); - - // Assert that the layout selected is "IRC (Experimental)" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible(); - - await checkDisabled(); - }); - - test("should support enabling system font", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - - // Click "Show advanced" link button - await tab.getByRole("button", { name: "Show advanced" }).click(); - - await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click(); - await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click(); - - // Assert that the font-family value was removed - await expect(page.locator("body")).toHaveCSS("font-family", '""'); - }); - - test.describe("Theme Choice Panel", () => { - test.beforeEach(async ({ app, user }) => { - // Disable the default theme for consistency in case ThemeWatcher automatically chooses it - await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); - }); - - test("should be rendered with the light theme selected", async ({ page, app }) => { - await app.settings.openUserSettings("Appearance"); - const themePanel = page.getByTestId("mx_ThemeChoicePanel"); - - const useSystemTheme = themePanel.getByTestId("checkbox-use-system-theme"); - await expect(useSystemTheme.getByText("Match system theme")).toBeVisible(); - // Assert that 'Match system theme' is not checked - // Note that mx_Checkbox_checkmark exists and is hidden by CSS if it is not checked - await expect(useSystemTheme.locator(".mx_Checkbox_checkmark")).not.toBeVisible(); - - const selectors = themePanel.getByTestId("theme-choice-panel-selectors"); - await expect(selectors.locator(".mx_ThemeSelector_light")).toBeVisible(); - await expect(selectors.locator(".mx_ThemeSelector_dark")).toBeVisible(); - // Assert that the light theme is selected - await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_enabled")).toBeVisible(); - // Assert that the buttons for the light and dark theme are not enabled - await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).not.toBeVisible(); - await expect(selectors.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).not.toBeVisible(); - - // Assert that the checkbox for the high contrast theme is rendered - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible(); - }); - - test("should disable the labels for themes and the checkbox for the high contrast theme if the checkbox for the system theme is clicked", async ({ - page, - app, - }) => { - await app.settings.openUserSettings("Appearance"); - const themePanel = page.getByTestId("mx_ThemeChoicePanel"); - - await themePanel.locator(".mx_Checkbox", { hasText: "Match system theme" }).click(); - - // Assert that the labels for the light theme and dark theme are disabled - await expect(themePanel.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).toBeVisible(); - await expect(themePanel.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).toBeVisible(); - - // Assert that there does not exist a label for an enabled theme - await expect(themePanel.locator("label.mx_StyledRadioButton_enabled")).not.toBeVisible(); - - // Assert that the checkbox and label to enable the high contrast theme should not exist - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible(); - }); - - test("should not render the checkbox and the label for the high contrast theme if the dark theme is selected", async ({ - page, - app, - }) => { - await app.settings.openUserSettings("Appearance"); - const themePanel = page.getByTestId("mx_ThemeChoicePanel"); - - // Assert that the checkbox and the label to enable the high contrast theme should exist - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible(); - - // Enable the dark theme - await themePanel.locator(".mx_ThemeSelector_dark").click(); - - // Assert that the checkbox and the label should not exist - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible(); - }); - }); -}); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts new file mode 100644 index 00000000000..aa00681f61b --- /dev/null +++ b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -0,0 +1,63 @@ +/* +Copyright 2023 Suguru Hirahara + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "."; + +test.describe("Appearance user settings tab", () => { + test.use({ + displayName: "Hanako", + }); + + test("should be rendered properly", async ({ page, user, app }) => { + const tab = await app.settings.openUserSettings("Appearance"); + + // Click "Show advanced" link button + await tab.getByRole("button", { name: "Show advanced" }).click(); + + // Assert that "Hide advanced" link button is rendered + await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible(); + + await expect(tab).toMatchScreenshot("appearance-tab.png"); + }); + + test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { + await app.settings.openUserSettings("Appearance"); + + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); + await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); + + // Default browser font size is 16px and the select value is 0 + // -4 value is 12px + await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); + + await expect(page).toMatchScreenshot("window-12px.png"); + }); + + test("should support enabling system font", async ({ page, app, user }) => { + await app.settings.openUserSettings("Appearance"); + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + + // Click "Show advanced" link button + await tab.getByRole("button", { name: "Show advanced" }).click(); + + await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click(); + await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click(); + + // Assert that the font-family value was removed + await expect(page.locator("body")).toHaveCSS("font-family", '""'); + }); +}); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/index.ts b/playwright/e2e/settings/appearance-user-settings-tab/index.ts new file mode 100644 index 00000000000..e8641306ed5 --- /dev/null +++ b/playwright/e2e/settings/appearance-user-settings-tab/index.ts @@ -0,0 +1,241 @@ +/* + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Locator, Page } from "@playwright/test"; + +import { ElementAppPage } from "../../../pages/ElementAppPage"; +import { test as base, expect } from "../../../element-web-test"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import { Layout } from "../../../../src/settings/enums/Layout"; + +export { expect }; + +/** + * Set up for the appearance tab test + */ +export const test = base.extend<{ + util: Helpers; +}>({ + util: async ({ page, app }, use) => { + await use(new Helpers(page, app)); + }, +}); + +/** + * A collection of helper functions for the appearance tab test + * The goal is to make easier to get and interact with the button, input, or other elements of the appearance tab + */ +class Helpers { + private CUSTOM_THEME_URL = "http://custom.theme"; + private CUSTOM_THEME = { + name: "Custom theme", + isDark: false, + colors: {}, + }; + + constructor( + private page: Page, + private app: ElementAppPage, + ) {} + + /** + * Open the appearance tab + */ + openAppearanceTab() { + return this.app.settings.openUserSettings("Appearance"); + } + + /** + * Compare screenshot and hide the matrix chat + * @param locator + * @param screenshot + */ + assertScreenshot(locator: Locator, screenshot: `${string}.png`) { + return expect(locator).toMatchScreenshot(screenshot, { + css: ` + #matrixchat { + display: none; + } + `, + }); + } + + // Theme Panel + + /** + * Disable in the settings the system theme + */ + disableSystemTheme() { + return this.app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); + } + + /** + * Return the theme section + */ + getThemePanel() { + return this.page.getByTestId("themePanel"); + } + + /** + * Return the system theme toggle + */ + getMatchSystemThemeCheckbox() { + return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" }); + } + + /** + * Return the theme radio button + * @param theme - the theme to select + * @private + */ + private getThemeRadio(theme: string) { + return this.getThemePanel().getByRole("radio", { name: theme }); + } + + /** + * Return the light theme radio button + */ + getLightTheme() { + return this.getThemeRadio("Light"); + } + + /** + * Return the dark theme radio button + */ + getDarkTheme() { + return this.getThemeRadio("Dark"); + } + + /** + * Return the custom theme radio button + */ + getCustomTheme() { + return this.getThemeRadio(this.CUSTOM_THEME.name); + } + + /** + * Return the high contrast theme radio button + */ + getHighContrastTheme() { + return this.getThemeRadio("High contrast"); + } + + /** + * Add a custom theme + * Mock the request to the custom and return a fake local custom theme + */ + async addCustomTheme() { + await this.page.route(this.CUSTOM_THEME_URL, (route) => + route.fulfill({ body: JSON.stringify(this.CUSTOM_THEME) }), + ); + await this.page.getByRole("textbox", { name: "Add custom theme" }).fill(this.CUSTOM_THEME_URL); + await this.page.getByRole("button", { name: "Add custom theme" }).click(); + await this.page.unroute(this.CUSTOM_THEME_URL); + } + + /** + * Remove the custom theme + */ + removeCustomTheme() { + return this.getThemePanel().getByRole("listitem", { name: this.CUSTOM_THEME.name }).getByRole("button").click(); + } + + // Message layout Panel + + /** + * Create and display a room named Test Room + */ + async createAndDisplayRoom() { + await this.app.client.createRoom({ name: "Test Room" }); + await this.app.viewRoomByName("Test Room"); + } + + /** + * Assert the room layout + * @param layout + * @private + */ + private assertRoomLayout(layout: Layout) { + return expect(this.page.locator(`.mx_RoomView_body[data-layout=${layout}]`)).toBeVisible(); + } + + /** + * Assert the room layout is modern + */ + assertModernLayout() { + return this.assertRoomLayout(Layout.Group); + } + + /** + * Assert the room layout is bubble + */ + assertBubbleLayout() { + return this.assertRoomLayout(Layout.Bubble); + } + + /** + * Return the layout panel + */ + getMessageLayoutPanel() { + return this.page.getByTestId("layoutPanel"); + } + + /** + * Return the layout radio button + * @param layoutName + * @private + */ + private getLayout(layoutName: string) { + return this.getMessageLayoutPanel().getByRole("radio", { name: layoutName }); + } + + /** + * Return the message bubbles layout radio button + */ + getBubbleLayout() { + return this.getLayout("Message bubbles"); + } + + /** + * Return the modern layout radio button + */ + getModernLayout() { + return this.getLayout("Modern"); + } + + /** + * Return the IRC layout radio button + */ + getIRCLayout() { + return this.getLayout("IRC (experimental)"); + } + + /** + * Return the compact layout checkbox + */ + getCompactLayoutCheckbox() { + return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" }); + } + + /** + * Assert the compact layout is enabled + */ + assertCompactLayout() { + return expect( + this.page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout"), + ).toBeVisible(); + } +} diff --git a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts new file mode 100644 index 00000000000..1a22696da12 --- /dev/null +++ b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts @@ -0,0 +1,66 @@ +/* +Copyright 2023 Suguru Hirahara + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "."; + +test.describe("Appearance user settings tab", () => { + test.use({ + displayName: "Hanako", + }); + + test.describe("Message Layout Panel", () => { + test.beforeEach(async ({ app, user, util }) => { + await util.createAndDisplayRoom(); + await util.assertModernLayout(); + await util.openAppearanceTab(); + }); + + test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => { + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); + + await util.getBubbleLayout().click(); + + // Assert that modern are irc layout are not selected + await expect(util.getBubbleLayout()).toBeChecked(); + await expect(util.getModernLayout()).not.toBeChecked(); + await expect(util.getIRCLayout()).not.toBeChecked(); + + // Assert that the room layout is set to bubble layout + await util.assertBubbleLayout(); + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); + }); + + test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => { + await expect(util.getCompactLayoutCheckbox()).not.toBeChecked(); + + await util.getCompactLayoutCheckbox().click(); + await util.assertCompactLayout(); + }); + + test("should disable compact layout when the modern layout is not selected", async ({ + page, + app, + user, + util, + }) => { + await expect(util.getCompactLayoutCheckbox()).not.toBeDisabled(); + + // Select the bubble layout, which should disable the compact layout checkbox + await util.getBubbleLayout().click(); + await expect(util.getCompactLayoutCheckbox()).toBeDisabled(); + }); + }); +}); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts new file mode 100644 index 00000000000..2b1e8cc14de --- /dev/null +++ b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts @@ -0,0 +1,89 @@ +/* +Copyright 2023 Suguru Hirahara + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "."; + +test.describe("Appearance user settings tab", () => { + test.use({ + displayName: "Hanako", + }); + + test.describe("Theme Choice Panel", () => { + test.beforeEach(async ({ app, user, util }) => { + // Disable the default theme for consistency in case ThemeWatcher automatically chooses it + await util.disableSystemTheme(); + await util.openAppearanceTab(); + }); + + test("should be rendered with the light theme selected", async ({ page, app, util }) => { + // Assert that 'Match system theme' is not checked + await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); + + // Assert that the light theme is selected + await expect(util.getLightTheme()).toBeChecked(); + // Assert that the dark and high contrast themes are not selected + await expect(util.getDarkTheme()).not.toBeChecked(); + await expect(util.getHighContrastTheme()).not.toBeChecked(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); + }); + + test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => { + await util.getMatchSystemThemeCheckbox().click(); + + // Assert that the themes are disabled + await expect(util.getLightTheme()).toBeDisabled(); + await expect(util.getDarkTheme()).toBeDisabled(); + await expect(util.getHighContrastTheme()).toBeDisabled(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); + }); + + test("should change the theme to dark", async ({ page, app, util }) => { + // Assert that the light theme is selected + await expect(util.getLightTheme()).toBeChecked(); + + await util.getDarkTheme().click(); + + // Assert that the light and high contrast themes are not selected + await expect(util.getLightTheme()).not.toBeChecked(); + await expect(util.getDarkTheme()).toBeChecked(); + await expect(util.getHighContrastTheme()).not.toBeChecked(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-dark.png"); + }); + + test.describe("custom theme", () => { + test.use({ + labsFlags: ["feature_custom_themes"], + }); + + test("should render the custom theme section", async ({ page, app, util }) => { + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); + }); + + test("should be able to add and remove a custom theme", async ({ page, app, util }) => { + await util.addCustomTheme(); + + await expect(util.getCustomTheme()).not.toBeChecked(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); + + await util.removeCustomTheme(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); + }); + }); + }); +}); diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts index ec3c14b2ca4..123c214288f 100644 --- a/playwright/e2e/settings/general-room-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts @@ -34,7 +34,7 @@ test.describe("General room settings tab", () => { // Assert that "Show less" details element is rendered await expect(settings.getByText("Show less")).toBeVisible(); - await expect(settings).toMatchScreenshot(); + await expect(settings).toMatchScreenshot("General-room-settings-tab-should-be-rendered-properly-1.png"); // Click the "Show less" details element await settings.getByText("Show less").click(); diff --git a/playwright/e2e/settings/general-user-settings-tab.spec.ts b/playwright/e2e/settings/general-user-settings-tab.spec.ts index 41210292a3a..0ba85e890be 100644 --- a/playwright/e2e/settings/general-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-user-settings-tab.spec.ts @@ -18,7 +18,6 @@ import { test, expect } from "../../element-web-test"; const USER_NAME = "Bob"; const USER_NAME_NEW = "Alice"; -const IntegrationManager = "scalar.vector.im"; test.describe("General user settings tab", () => { test.use({ @@ -73,40 +72,6 @@ test.describe("General user settings tab", () => { // Assert that the add button is rendered await expect(phoneNumbers.getByRole("button", { name: "Add" })).toBeVisible(); - // Check language and region setting dropdown - const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput"); - await languageInput.scrollIntoViewIfNeeded(); - // Check the default value - await expect(languageInput.getByText("English")).toBeVisible(); - // Click the button to display the dropdown menu - await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); - // Assert that the default option is rendered and highlighted - languageInput.getByRole("option", { name: /Albanian/ }); - await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass( - /mx_Dropdown_option_highlight/, - ); - await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible(); - // Click again to close the dropdown - await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); - // Assert that the default value is rendered again - await expect(languageInput.getByText("English")).toBeVisible(); - - const setIdServer = uut.locator(".mx_SetIdServer"); - await setIdServer.scrollIntoViewIfNeeded(); - // Assert that an input area for identity server exists - await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible(); - - const setIntegrationManager = uut.locator(".mx_SetIntegrationManager"); - await setIntegrationManager.scrollIntoViewIfNeeded(); - await expect( - setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager", { hasText: IntegrationManager }), - ).toBeVisible(); - // Make sure integration manager's toggle switch is enabled - await expect(setIntegrationManager.locator(".mx_ToggleSwitch_enabled")).toBeVisible(); - await expect(setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager")).toHaveText( - "Manage integrations(scalar.vector.im)", - ); - // Assert the account deactivation button is displayed const accountManagementSection = uut.getByTestId("account-management-section"); await accountManagementSection.scrollIntoViewIfNeeded(); @@ -120,6 +85,12 @@ test.describe("General user settings tab", () => { await expect(uut).toMatchScreenshot("general-smallscreen.png"); }); + test("should show tooltips on narrow screen", async ({ page, uut }) => { + await page.setViewportSize({ width: 700, height: 600 }); + await page.getByRole("tab", { name: "General" }).hover(); + await expect(page.getByRole("tooltip")).toHaveText("General"); + }); + test("should support adding and removing a profile picture", async ({ uut, page }) => { const profileSettings = uut.locator(".mx_UserProfileSettings"); // Upload a picture diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 2dbd267162d..a67909b47be 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -1,5 +1,6 @@ /* Copyright 2023 Suguru Hirahara +Copyright 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +20,10 @@ import { test, expect } from "../../element-web-test"; test.describe("Preferences user settings tab", () => { test.use({ displayName: "Bob", + uut: async ({ app, user }, use) => { + const locator = await app.settings.openUserSettings("Preferences"); + await use(locator); + }, }); test("should be rendered properly", async ({ app, user }) => { @@ -26,6 +31,26 @@ test.describe("Preferences user settings tab", () => { // Assert that the top heading is rendered await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible(); - await expect(tab).toMatchScreenshot(); + await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png"); + }); + + test("should be able to change the app language", async ({ uut, user }) => { + // Check language and region setting dropdown + const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput"); + await languageInput.scrollIntoViewIfNeeded(); + // Check the default value + await expect(languageInput.getByText("English")).toBeVisible(); + // Click the button to display the dropdown menu + await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); + // Assert that the default option is rendered and highlighted + languageInput.getByRole("option", { name: /Albanian/ }); + await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass( + /mx_Dropdown_option_highlight/, + ); + await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible(); + // Click again to close the dropdown + await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); + // Assert that the default value is rendered again + await expect(languageInput.getByText("English")).toBeVisible(); }); }); diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts index 08640f603ba..8d1f442ba5c 100644 --- a/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -1,5 +1,6 @@ /* Copyright 2023 Suguru Hirahara +Copyright 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,6 +17,8 @@ limitations under the License. import { test, expect } from "../../element-web-test"; +const IntegrationManager = "scalar.vector.im"; + test.describe("Security user settings tab", () => { test.describe("with posthog enabled", () => { test.use({ @@ -44,8 +47,36 @@ test.describe("Security user settings tab", () => { test("should be rendered properly", async ({ app, page }) => { const tab = await app.settings.openUserSettings("Security"); await tab.getByRole("button", { name: "Learn more" }).click(); - await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(); + await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( + "Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1.png", + ); }); }); + + test("should contain section to set ID server", async ({ app }) => { + const tab = await app.settings.openUserSettings("Security"); + + const setIdServer = tab.locator(".mx_SetIdServer"); + await setIdServer.scrollIntoViewIfNeeded(); + // Assert that an input area for identity server exists + await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible(); + }); + + test("should enable show integrations as enabled", async ({ app, page }) => { + const tab = await app.settings.openUserSettings("Security"); + + const setIntegrationManager = tab.locator(".mx_SetIntegrationManager"); + await setIntegrationManager.scrollIntoViewIfNeeded(); + await expect( + setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager", { + hasText: IntegrationManager, + }), + ).toBeVisible(); + // Make sure integration manager's toggle switch is enabled + await expect(setIntegrationManager.locator(".mx_ToggleSwitch_enabled")).toBeVisible(); + await expect(setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager")).toHaveText( + "Manage integrations(scalar.vector.im)", + ); + }); }); }); diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index 8bafe2e8049..8b013c44bb8 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -337,12 +337,10 @@ export class Helpers { } /** - * Assert that the thread panel is focused (actually the 'close' button, specifically) + * Assert that the thread tab is focused */ - assertThreadPanelFocused() { - return expect( - this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByLabel("Close"), - ).toBeFocused(); + assertThreadTabFocused() { + return expect(this.page.locator("#thread-panel-tab")).toBeFocused(); } /** diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 7d0b694ef57..66a3bc58e50 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -161,17 +161,12 @@ test.describe("Threads Activity Centre", () => { await util.assertNoTacIndicator(); }); - test("should focus the thread panel close button when clicking an item in the TAC", async ({ - room1, - room2, - util, - msg, - }) => { + test("should focus the thread tab when clicking an item in the TAC", async ({ room1, room2, util, msg }) => { await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); await util.openTac(); await util.clickRoomInTac(room1.name); - await util.assertThreadPanelFocused(); + await util.assertThreadTabFocused(); }); }); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index 177eccdc106..5d10937b671 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -21,7 +21,7 @@ import type { Locator, Page } from "@playwright/test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; function roomHeaderName(page: Page): Locator { - return page.locator(".mx_LegacyRoomHeader_nametext"); + return page.locator(".mx_RoomHeader_heading"); } async function startDM(app: ElementAppPage, page: Page, name: string): Promise { diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index 9b5ea46511e..7898457d053 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -252,7 +252,8 @@ test.describe("Threads", () => { await expect(locator.locator(".mx_ThreadSummary_content").getByText("How are things?")).toBeAttached(); locator = page.getByRole("button", { name: "Threads" }); - await expect(locator).toHaveClass(/mx_LegacyRoomHeader_button--unread/); // User asserts thread list unread indicator + await expect(locator).toHaveAttribute("data-indicator", "default"); // User asserts thread list unread indicator + // await expect(locator).toHaveClass(/mx_LegacyRoomHeader_button--unread/); await locator.click(); // User opens thread list // User asserts thread with correct root & latest events & unread dot @@ -433,7 +434,7 @@ test.describe("Threads", () => { await textbox.press("Enter"); await expect(locator.locator(".mx_EventTile_last").getByText("Hello Mr. User")).toBeAttached(); // Close thread - await locator.getByRole("button", { name: "Close" }).click(); + await locator.getByTestId("base-card-close-button").click(); // Open existing thread locator = page @@ -486,7 +487,7 @@ test.describe("Threads", () => { await textbox.press("Enter"); await expect(threadPanel.locator(".mx_EventTile_last").getByText(threadMessage)).toBeVisible(); // Close thread - await threadPanel.getByRole("button", { name: "Close" }).click(); + await threadPanel.getByTestId("base-card-close-button").click(); }; await sendMessage("Hello Mr. Bot"); @@ -495,14 +496,12 @@ test.describe("Threads", () => { await createThread("Hello again Mr. Bot", "Hello again Mr. User in a thread"); // Open thread panel - await page.getByTestId("threadsButton").click(); + await page.locator(".mx_RoomHeader").getByRole("button", { name: "Threads" }).click(); const threadPanel = page.locator(".mx_ThreadPanel"); await expect( threadPanel.locator(".mx_EventTile_last").getByText("Hello again Mr. User in a thread"), ).toBeVisible(); - // Open threads list - await page.locator(".mx_BaseCard_back").click(); const rightPanel = page.locator(".mx_RightPanel"); // Check that the threads are listed await expect(rightPanel.locator(".mx_EventTile").getByText("Hello Mr. User in a thread")).toBeVisible(); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 60aa1e2a27b..6068385194c 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -410,6 +410,7 @@ test.describe("Timeline", () => { { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], + hideTooltips: true, }, ); @@ -427,6 +428,7 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-and-messages-irc-layout.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], + hideTooltips: true, }); // 3. Alignment of expanded GELS and placeholder of deleted message @@ -447,6 +449,7 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-redaction-placeholder.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], + hideTooltips: true, }); // 4. Alignment of expanded GELS, placeholder of deleted message, and emote @@ -469,6 +472,7 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], + hideTooltips: true, }); }); @@ -481,6 +485,7 @@ test.describe("Timeline", () => { display: none !important; } `, + hideTooltips: true, }; await sendEvent(app.client, room.roomId); @@ -568,9 +573,9 @@ test.describe("Timeline", () => { ); }); - test("should set inline start padding to a hidden event line", async ({ page, app, room, cryptoBackend }) => { + test("should set inline start padding to a hidden event line", async ({ page, app, room }) => { test.skip( - cryptoBackend === "rust", + true, "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", ); await sendEvent(app.client, room.roomId); @@ -779,12 +784,12 @@ test.describe("Timeline", () => { await sendEvent(app.client, room.roomId, true); await page.goto(`/#/room/${room.roomId}`); - await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click(); + await app.toggleRoomInfoPanel(); - await expect(page.locator(".mx_SearchBar")).toMatchScreenshot("search-bar-on-timeline.png"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); - await page.locator(".mx_SearchBar_input").getByRole("textbox").fill("Message"); - await page.locator(".mx_SearchBar_input").getByRole("textbox").press("Enter"); + await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); for (const locator of await page .locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight") @@ -804,7 +809,7 @@ test.describe("Timeline", () => { await page.goto(`/#/room/${room.roomId}`); // Open a room setting dialog - await page.getByRole("button", { name: "Room options" }).click(); + await app.toggleRoomInfoPanel(); await page.getByRole("menuitem", { name: "Settings" }).click(); // Set a room topic to render a TextualEvent @@ -818,12 +823,9 @@ test.describe("Timeline", () => { page.getByText(`${OLD_NAME} changed the topic to "This is a room for ${stringToSearch}.".`), ).toHaveClass(/mx_TextualEvent/); - // Display the room search bar - await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click(); - // Search the string to display both the message and TextualEvent on search results panel - await page.locator(".mx_SearchBar").getByRole("textbox").fill(stringToSearch); - await page.locator(".mx_SearchBar").getByRole("textbox").press("Enter"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill(stringToSearch); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); // On search results panel const resultsPanel = page.locator(".mx_RoomView_searchResultsPanel"); @@ -1199,10 +1201,10 @@ test.describe("Timeline", () => { // Install our mocks and preventative measures await context.route("**/_matrix/client/versions", async (route) => { - // Force enable MSC3916, which may require the service worker's internal cache to be cleared later. + // Force enable MSC3916/Matrix 1.11, which may require the service worker's internal cache to be cleared later. const json = await (await route.fetch()).json(); - if (!json["unstable_features"]) json["unstable_features"] = {}; - json["unstable_features"]["org.matrix.msc3916"] = true; + if (!json["versions"]) json["versions"] = []; + json["versions"].push("v1.11"); await route.fulfill({ json }); }); await context.route("**/_matrix/media/*/download/**", async (route) => { @@ -1219,14 +1221,14 @@ test.describe("Timeline", () => { json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, }); }); - await context.route("**/_matrix/client/unstable/org.matrix.msc3916/download/**", async (route) => { + await context.route("**/_matrix/client/v1/download/**", async (route) => { expect(route.request().headers()["Authorization"]).toBeDefined(); // we can't use route.continue() because no configured homeserver supports MSC3916 yet await route.fulfill({ body: NEW_AVATAR, }); }); - await context.route("**/_matrix/client/unstable/org.matrix.msc3916/thumbnail/**", async (route) => { + await context.route("**/_matrix/client/v1/thumbnail/**", async (route) => { expect(route.request().headers()["Authorization"]).toBeDefined(); // we can't use route.continue() because no configured homeserver supports MSC3916 yet await route.fulfill({ diff --git a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts index 09a140d4418..0799dc0f604 100644 --- a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts +++ b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts @@ -35,7 +35,9 @@ test.describe("User Onboarding (new user)", () => { }); test("page is shown and preference exists", async ({ page, app }) => { - await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(); + await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot( + "User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png", + ); await app.settings.openUserSettings("Preferences"); await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible(); }); diff --git a/playwright/e2e/utils.ts b/playwright/e2e/utils.ts index 30aff64dd8f..e7587c7dfbc 100644 --- a/playwright/e2e/utils.ts +++ b/playwright/e2e/utils.ts @@ -17,8 +17,8 @@ limitations under the License. */ import { uniqueId } from "lodash"; +import { expect, type Page } from "@playwright/test"; -import type { Page } from "@playwright/test"; import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { Client } from "../pages/client"; @@ -63,4 +63,15 @@ export async function waitForRoom( ); } +export async function selectHomeserver(page: Page, homeserverUrl: string) { + await page.getByRole("button", { name: "Edit" }).click(); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserverUrl); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + // wait for the dialog to go away + await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); + + await expect(page.locator(".mx_Spinner")).toHaveCount(0); + await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserverUrl); +} + export const CommandOrControl = process.platform === "darwin" ? "Meta" : "Control"; diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 2317978898d..d6fd1b48c19 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -15,9 +15,10 @@ limitations under the License. */ import { test as base, expect as baseExpect, Locator, Page, ExpectMatcherState, ElementHandle } from "@playwright/test"; +import { sanitizeForFilePath } from "playwright-core/lib/utils"; import AxeBuilder from "@axe-core/playwright"; import _ from "lodash"; -import { basename } from "node:path"; +import { basename, extname } from "node:path"; import type mailhog from "mailhog"; import type { IConfigOptions } from "../src/IConfigOptions"; @@ -63,84 +64,77 @@ const CONFIG_JSON: Partial = { }, }; -export type TestOptions = { - cryptoBackend: "legacy" | "rust"; -}; - interface CredentialsWithDisplayName extends Credentials { displayName: string; } -export const test = base.extend< - TestOptions & { - axe: AxeBuilder; - checkA11y: () => Promise; - - /** - * The contents of the config.json to send when the client requests it. - */ - config: typeof CONFIG_JSON; - - /** - * The options with which to run the {@link #homeserver} fixture. - */ - startHomeserverOpts: StartHomeserverOpts | string; - - homeserver: HomeserverInstance; - oAuthServer: { port: number }; - - /** - * The displayname to use for the user registered in {@link #credentials}. - * - * To set it, call `test.use({ displayName: "myDisplayName" })` in the test file or `describe` block. - * See {@link https://playwright.dev/docs/api/class-test#test-use}. - */ - displayName?: string; - - /** - * A test fixture which registers a test user on the {@link #homeserver} and supplies the details - * of the registered user. - */ - credentials: CredentialsWithDisplayName; - - /** - * The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, - * but adds an initScript which will populate localStorage with the user's details from - * {@link #credentials} and {@link #homeserver}. - * - * Similar to {@link #user}, but doesn't load the app. - */ - pageWithCredentials: Page; - - /** - * A (rather poorly-named) test fixture which registers a user per {@link #credentials}, stores - * the credentials into localStorage per {@link #homeserver}, and then loads the front page of the - * app. - */ - user: CredentialsWithDisplayName; - - /** - * The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, - * but wraps the returned `Page` in a class of utilities for interacting with the Element-Web UI, - * {@link ElementAppPage}. - */ - app: ElementAppPage; - - mailhog: { api: mailhog.API; instance: Instance }; - crypto: Crypto; - room?: { roomId: string }; - toasts: Toasts; - uut?: Locator; // Unit Under Test, useful place to refer a prepared locator - botCreateOpts: CreateBotOpts; - bot: Bot; - slidingSyncProxy: ProxyInstance; - labsFlags: string[]; - webserver: Webserver; - } ->({ - cryptoBackend: ["legacy", { option: true }], +export const test = base.extend<{ + axe: AxeBuilder; + checkA11y: () => Promise; + + /** + * The contents of the config.json to send when the client requests it. + */ + config: typeof CONFIG_JSON; + + /** + * The options with which to run the {@link #homeserver} fixture. + */ + startHomeserverOpts: StartHomeserverOpts | string; + + homeserver: HomeserverInstance; + oAuthServer: { port: number }; + + /** + * The displayname to use for the user registered in {@link #credentials}. + * + * To set it, call `test.use({ displayName: "myDisplayName" })` in the test file or `describe` block. + * See {@link https://playwright.dev/docs/api/class-test#test-use}. + */ + displayName?: string; + + /** + * A test fixture which registers a test user on the {@link #homeserver} and supplies the details + * of the registered user. + */ + credentials: CredentialsWithDisplayName; + + /** + * The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, + * but adds an initScript which will populate localStorage with the user's details from + * {@link #credentials} and {@link #homeserver}. + * + * Similar to {@link #user}, but doesn't load the app. + */ + pageWithCredentials: Page; + + /** + * A (rather poorly-named) test fixture which registers a user per {@link #credentials}, stores + * the credentials into localStorage per {@link #homeserver}, and then loads the front page of the + * app. + */ + user: CredentialsWithDisplayName; + + /** + * The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, + * but wraps the returned `Page` in a class of utilities for interacting with the Element-Web UI, + * {@link ElementAppPage}. + */ + app: ElementAppPage; + + mailhog: { api: mailhog.API; instance: Instance }; + crypto: Crypto; + room?: { roomId: string }; + toasts: Toasts; + uut?: Locator; // Unit Under Test, useful place to refer a prepared locator + botCreateOpts: CreateBotOpts; + bot: Bot; + slidingSyncProxy: ProxyInstance; + labsFlags: string[]; + webserver: Webserver; +}>({ config: CONFIG_JSON, - page: async ({ context, page, config, cryptoBackend, labsFlags }, use) => { + page: async ({ context, page, config, labsFlags }, use) => { await context.route(`http://localhost:8080/config.json*`, async (route) => { const json = { ...CONFIG_JSON, ...config }; json["features"] = { @@ -151,10 +145,6 @@ export const test = base.extend< return obj; }, {}), }; - // the default is to use rust now, so set to `false` if on legacy backend - if (cryptoBackend === "legacy") { - json.features.feature_rust_crypto = false; - } await route.fulfill({ json }); }); await use(page); @@ -309,20 +299,40 @@ export const test = base.extend< }, }); +// Based on https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/util.ts#L206C8-L210C2 +function sanitizeFilePathBeforeExtension(filePath: string): string { + const ext = extname(filePath); + const base = filePath.substring(0, filePath.length - ext.length); + return sanitizeForFilePath(base) + ext; +} + export const expect = baseExpect.extend({ async toMatchScreenshot( this: ExpectMatcherState, receiver: Page | Locator, - name?: `${string}.png`, + name: `${string}.png`, options?: { mask?: Array; omitBackground?: boolean; + hideTooltips?: boolean; timeout?: number; css?: string; }, ) { + const testInfo = test.info(); + if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); + const page = "page" in receiver ? receiver.page() : receiver; + let hideTooltipsCss: string | undefined; + if (options?.hideTooltips) { + hideTooltipsCss = ` + .mx_Tooltip_visible { + visibility: hidden !important; + } + `; + } + // We add a custom style tag before taking screenshots const style = (await page.addStyleTag({ content: ` @@ -342,17 +352,31 @@ export const expect = baseExpect.extend({ .mx_ReplyChain { border-left-color: var(--cpd-color-blue-1200) !important; } + /* Avoid flakiness from hover styling */ + .mx_ReplyChain_show { + color: var(--cpd-color-text-secondary) !important; + } /* Use monospace font for timestamp for consistent mask width */ .mx_MessageTimestamp { font-family: Inconsolata !important; } + ${hideTooltipsCss ?? ""} ${options?.css ?? ""} `, })) as ElementHandle; - await baseExpect(receiver).toHaveScreenshot(name, options); + const screenshotName = sanitizeFilePathBeforeExtension(name); + await baseExpect(receiver).toHaveScreenshot(screenshotName, options); await style.evaluate((tag) => tag.remove()); + + testInfo.annotations.push({ + // `_` prefix hides it from the HTML reporter + type: "_screenshot", + // include a path relative to `playwright/snapshots/` + description: testInfo.snapshotPath(screenshotName).split("/playwright/snapshots/", 2)[1], + }); + return { pass: true, message: () => "", name: "toMatchScreenshot" }; }, }); diff --git a/playwright/flaky-reporter.ts b/playwright/flaky-reporter.ts index 3d358bb74d1..95023e31baf 100644 --- a/playwright/flaky-reporter.ts +++ b/playwright/flaky-reporter.ts @@ -53,7 +53,10 @@ class FlakyReporter implements Reporter { const headers = { Authorization: `Bearer ${GITHUB_TOKEN}` }; // Fetch all existing issues with the flaky-test label. - const issuesRequest = await fetch(`${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}`, { headers }); + const issuesRequest = await fetch( + `${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=100&sort=created`, + { headers }, + ); const issues = await issuesRequest.json(); for (const flake of this.flakes) { const title = ISSUE_TITLE_PREFIX + "`" + flake + "`"; @@ -61,6 +64,12 @@ class FlakyReporter implements Reporter { if (existingIssue) { console.log(`Found issue ${existingIssue.number} for ${flake}, adding comment...`); + // Ensure that the test is open + await fetch(existingIssue.url, { + method: "PATCH", + headers, + body: JSON.stringify({ state: "open" }), + }); await fetch(`${existingIssue.url}/comments`, { method: "POST", headers, diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index ac9b4ffef80..af79994358c 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -171,4 +171,13 @@ export class ElementAppPage { await spotlight.open(); return spotlight; } + + /** + * Opens/closes the room info panel + * @returns locator to the right panel + */ + public async toggleRoomInfoPanel(): Promise { + await this.page.getByRole("button", { name: "Room info" }).first().click(); + return this.page.locator(".mx_RightPanel"); + } } diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index 333d895dfe9..3b461301088 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -45,10 +45,6 @@ export interface CreateBotOpts { * Whether to generate cross-signing keys */ bootstrapCrossSigning?: boolean; - /** - * Whether to use the rust crypto impl. Defaults to false (for now!) - */ - rustCrypto?: boolean; /** * Whether to bootstrap the secret storage */ @@ -188,11 +184,7 @@ export class Bot extends Client { return cli; } - if (opts.rustCrypto) { - await cli.initRustCrypto({ useIndexedDB: false }); - } else { - await cli.initCrypto(); - } + await cli.initRustCrypto({ useIndexedDB: false }); cli.setGlobalErrorOnUnknownDevices(false); await cli.startClient(); diff --git a/playwright/pages/settings.ts b/playwright/pages/settings.ts index c0efb6770c8..1b7d099c6c4 100644 --- a/playwright/pages/settings.ts +++ b/playwright/pages/settings.ts @@ -91,12 +91,17 @@ export class Settings { } /** - * Open room settings (via room header menu), returns a locator to the dialog + * Open room settings (via room info panel), returns a locator to the dialog * @param tab the name of the tab to switch to after opening, optional. */ public async openRoomSettings(tab?: string): Promise { - await this.page.getByRole("banner").getByRole("button", { name: "Room options", exact: true }).click(); - await this.page.locator(".mx_RoomTile_contextMenu").getByRole("menuitem", { name: "Settings" }).click(); + // Open right panel if not open + const rightPanel = this.page.locator(".mx_RightPanel"); + if ((await rightPanel.count()) === 0) { + await this.page.getByRole("button", { name: "Room info" }).first().click(); + } + await rightPanel.getByRole("menuitem", { name: "Settings" }).click(); + if (tab) await this.switchTab(tab); return this.page.locator(".mx_Dialog").filter({ has: this.page.locator(".mx_RoomSettingsDialog") }); } diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index 1e0cfb3b39c..b14ba70082d 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -39,6 +39,15 @@ export interface HomeserverInstance { * @param password login password */ loginUser(userId: string, password: string): Promise; + + /** + * Sets a third party identifier for the given user. This only supports setting a single 3pid and will + * replace any others. + * @param userId The full ID of the user to edit (as returned from registerUser) + * @param medium The medium of the 3pid to set + * @param address The address of the 3pid to set + */ + setThreepid(userId: string, medium: string, address: string): Promise; } export interface StartHomeserverOpts { diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index c88fd641d9d..5cf63917203 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -28,7 +28,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for `matrixdotorg/synapse` image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:c357ea1486c8cd2613932e97bd2f6ff4e8b4c4fafcb2c28d1e8ec0d383c56d9d"; +const DOCKER_TAG = "develop@sha256:9e193236098ae5ff66c9bf79252e318fd561ceb1322d5495780a11d9dbdcfb17"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); @@ -94,6 +94,8 @@ export class Synapse implements Homeserver, HomeserverInstance { protected docker: Docker = new Docker(); public config: HomeserverConfig & { serverId: string }; + private adminToken?: string; + public constructor(private readonly request: APIRequestContext) {} /** @@ -152,12 +154,17 @@ export class Synapse implements Homeserver, HomeserverInstance { return [path.join(synapseLogsPath, "stdout.log"), path.join(synapseLogsPath, "stderr.log")]; } - public async registerUser(username: string, password: string, displayName?: string): Promise { + private async registerUserInternal( + username: string, + password: string, + displayName?: string, + admin = false, + ): Promise { const url = `${this.config.baseUrl}/_synapse/admin/v1/register`; const { nonce } = await this.request.get(url).then((r) => r.json()); const mac = crypto .createHmac("sha1", this.config.registrationSecret) - .update(`${nonce}\0${username}\0${password}\0notadmin`) + .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) .digest("hex"); const res = await this.request.post(url, { data: { @@ -165,7 +172,7 @@ export class Synapse implements Homeserver, HomeserverInstance { username, password, mac, - admin: false, + admin, displayname: displayName, }, }); @@ -185,6 +192,10 @@ export class Synapse implements Homeserver, HomeserverInstance { }; } + public registerUser(username: string, password: string, displayName?: string): Promise { + return this.registerUserInternal(username, password, displayName, false); + } + public async loginUser(userId: string, password: string): Promise { const url = `${this.config.baseUrl}/_matrix/client/v3/login`; const res = await this.request.post(url, { @@ -207,4 +218,30 @@ export class Synapse implements Homeserver, HomeserverInstance { homeServer: json.home_server, }; } + + public async setThreepid(userId: string, medium: string, address: string): Promise { + if (this.adminToken === undefined) { + const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true); + this.adminToken = result.accessToken; + } + + const url = `${this.config.baseUrl}/_synapse/admin/v2/users/${userId}`; + const res = await this.request.put(url, { + data: { + threepids: [ + { + medium, + address, + }, + ], + }, + headers: { + Authorization: `Bearer ${this.adminToken}`, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + } } diff --git a/playwright/plugins/homeserver/synapse/templates/guest-enabled/README.md b/playwright/plugins/homeserver/synapse/templates/guest-enabled/README.md new file mode 100644 index 00000000000..e1fef0b9d41 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/guest-enabled/README.md @@ -0,0 +1 @@ +A synapse configured with guest registration enabled. diff --git a/playwright/plugins/homeserver/synapse/templates/guest-enabled/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/guest-enabled/homeserver.yaml new file mode 100644 index 00000000000..1faa39c3a74 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/guest-enabled/homeserver.yaml @@ -0,0 +1,105 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +allow_guest_access: true +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +oidc_providers: + - idp_id: test + idp_name: "OAuth test" + issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" + authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" + # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. + token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" + userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" + client_id: "synapse" + discover: false + scopes: ["profile"] + skip_verification: true + client_auth_method: none + user_mapping_provider: + config: + display_name_template: "{{ user.name }}" + +# Inhibit background updates as this Synapse isn't long-lived +background_updates: + min_batch_size: 100000 + sleep_duration_ms: 100000 + +experimental_features: + # Needed for e2e/crypto/crypto.spec.ts > Cryptography > decryption failure + # messages > non-joined historical messages. + # Can be removed after Synapse enables it by default + msc4115_membership_on_events: true diff --git a/playwright/plugins/homeserver/synapse/templates/guest-enabled/log.config b/playwright/plugins/homeserver/synapse/templates/guest-enabled/log.config new file mode 100644 index 00000000000..ac232762da3 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/guest-enabled/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png new file mode 100644 index 00000000000..df1a44f523c Binary files /dev/null and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png differ diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png new file mode 100644 index 00000000000..96ad96e3e11 Binary files /dev/null and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png differ diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png new file mode 100644 index 00000000000..ae11ec9eeca Binary files /dev/null and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 98c1ff245d3..2c6160f2a19 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png index 9f46fce516b..bfbfccbaeba 100644 Binary files a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png and b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png differ diff --git a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png index 75a9c353dee..c1880811897 100644 Binary files a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png and b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png new file mode 100644 index 00000000000..891f024bf82 Binary files /dev/null and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png new file mode 100644 index 00000000000..22b4e109c87 Binary files /dev/null and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index c8b8dba45b6..614533956be 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index 852cb85518c..51f365f353f 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png index db91140763a..2214206caba 100644 Binary files a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png and b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index c85c583a19a..9992923226d 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index b6990e727ea..050a82a8af8 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png index e4f6313c97f..fdbec37b706 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png index 7d8884dc4d0..d18266534de 100644 Binary files a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index f383a828e2c..943cc9dfc8b 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/encrypted-room-header-linux.png b/playwright/snapshots/room/room-header.spec.ts/encrypted-room-header-linux.png deleted file mode 100644 index 6dced2e9909..00000000000 Binary files a/playwright/snapshots/room/room-header.spec.ts/encrypted-room-header-linux.png and /dev/null differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png deleted file mode 100644 index c792c4bcf08..00000000000 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png and /dev/null differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png index c5fd90be578..ea591089b43 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png index 0cfb88b5230..ded42af7a17 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png index 5d79ae740c7..7271a50cf9d 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-highlighted-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-highlighted-linux.png deleted file mode 100644 index 6016bb7e7a1..00000000000 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-highlighted-linux.png and /dev/null differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png deleted file mode 100644 index 498853a9730..00000000000 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png and /dev/null differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png deleted file mode 100644 index b7fea971922..00000000000 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and /dev/null differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-11-darwin.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-11-darwin.png deleted file mode 100644 index 12996f4e5b8..00000000000 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-11-darwin.png and /dev/null differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-11-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-11-linux.png deleted file mode 100644 index f5231463488..00000000000 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-11-linux.png and /dev/null differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-21-darwin.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-21-darwin.png deleted file mode 100644 index 6b1e058c6a1..00000000000 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-21-darwin.png and /dev/null differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-21-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-21-linux.png deleted file mode 100644 index 502e60cb1f8..00000000000 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/font-slider-21-linux.png and /dev/null differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png deleted file mode 100644 index 3247abd7c1b..00000000000 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png and /dev/null differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png new file mode 100644 index 00000000000..12ea3aa847f Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png new file mode 100644 index 00000000000..1ce0ffd520a Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png new file mode 100644 index 00000000000..d44c1073072 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png new file mode 100644 index 00000000000..76a0befd339 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png new file mode 100644 index 00000000000..3b9c2431380 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png new file mode 100644 index 00000000000..ca909171166 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png new file mode 100644 index 00000000000..1aed777c8d6 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png differ diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png index af35cc8bb4a..4cf8df1c326 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png differ diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png index d59d2946da7..d0380e6a4fc 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index e5d1ddef4f8..ca2e75dfbb8 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index 281f1cebe5b..d30d8e98a8d 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png index 5703f384498..f8e94f92696 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png index 0a3d265a601..9d59bfa56fc 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png index 0453bf932a9..1c07ac36974 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png index 077ecbf7176..48e82f72af1 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png index 194ecd07fbc..9f510402e62 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png index 2b990bb3b86..0430d95ac5c 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png index 294cd3ec7ab..80666c5ccfd 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png index f0064c81e19..c08856b44e1 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png index 001ac64f2a6..eb5a0fca3bc 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png index f7a276d2f72..594ac521e0e 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png index de056e0fa56..1c83d343d0a 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png index 077ecbf7176..48e82f72af1 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png index e1cd5ab19c3..6ac01ff51be 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png index 4fb29024560..2149c46220c 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png deleted file mode 100644 index ceddab3312b..00000000000 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png and /dev/null differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png deleted file mode 100644 index 5fba124a929..00000000000 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png and /dev/null differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png index ff75b3473fe..2685052da94 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png index dfc55550aae..1d05e321052 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png index 800ceefc6e4..88264291980 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png index 9d2fcdf272d..937bcb34170 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png index f85715b0765..a11b81693d0 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png new file mode 100644 index 00000000000..fe96a9e6be2 Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png deleted file mode 100644 index 64d44a9778b..00000000000 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png and /dev/null differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png index 4c553cfdafb..b83fe895f73 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png differ diff --git a/playwright/snapshots/user-view/user-view.spec.ts/UserView-should-render-the-user-view-as-expected-1-linux.png b/playwright/snapshots/user-view/user-view.spec.ts/UserView-should-render-the-user-view-as-expected-1-linux.png deleted file mode 100644 index 75b64546d63..00000000000 Binary files a/playwright/snapshots/user-view/user-view.spec.ts/UserView-should-render-the-user-view-as-expected-1-linux.png and /dev/null differ diff --git a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png index 61ab6601577..b6d6d2a2108 100644 Binary files a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png and b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png differ diff --git a/playwright/stale-screenshot-reporter.ts b/playwright/stale-screenshot-reporter.ts new file mode 100644 index 00000000000..3a3d18e28ca --- /dev/null +++ b/playwright/stale-screenshot-reporter.ts @@ -0,0 +1,74 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Test reporter which compares the reported screenshots vs those on disk to find stale screenshots + * Only intended to run from within GitHub Actions + */ + +import path from "node:path"; +import { glob } from "glob"; + +import type { Reporter, TestCase } from "@playwright/test/reporter"; + +const snapshotRoot = path.join(__dirname, "snapshots"); + +class StaleScreenshotReporter implements Reporter { + private screenshots = new Set(); + private success = true; + + public onTestEnd(test: TestCase): void { + for (const annotation of test.annotations) { + if (annotation.type === "_screenshot") { + this.screenshots.add(annotation.description); + } + } + } + + private error(msg: string, file: string) { + if (process.env.GITHUB_ACTIONS) { + console.log(`::error file=${file}::${msg}`); + } + console.error(msg, file); + this.success = false; + } + + public async onExit(): Promise { + const screenshotFiles = new Set(await glob(`**/*.png`, { cwd: snapshotRoot })); + for (const screenshot of screenshotFiles) { + if (screenshot.split("-").at(-1) !== "linux.png") { + this.error( + "Found screenshot belonging to different platform, this should not be checked in", + screenshot, + ); + } + } + for (const screenshot of this.screenshots) { + screenshotFiles.delete(screenshot); + } + if (screenshotFiles.size > 0) { + for (const screenshot of screenshotFiles) { + this.error("Stale screenshot file", screenshot); + } + } + + if (!this.success) { + process.exit(1); + } + } +} + +export default StaleScreenshotReporter; diff --git a/res/css/_common.pcss b/res/css/_common.pcss index d120194491b..8264ccb704c 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -177,9 +177,9 @@ a:visited { color: $accent-alt; } -input[type="text"], -input[type="search"], -input[type="password"] { +:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="text"], +:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="search"], +:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="password"] { padding: 9px; font: var(--cpd-font-body-md-semibold); font-weight: var(--cpd-font-weight-semibold); @@ -378,6 +378,7 @@ legend { font-family: inherit !important; white-space: normal !important; line-height: inherit !important; + background-color: inherit; color: inherit; /* inherit the colour from the dark or light theme by default (but not for code blocks) */ pre, @@ -521,6 +522,8 @@ legend { content: ""; width: 28px; height: 28px; + left: 0; + top: 0; position: absolute; mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); mask-repeat: no-repeat; @@ -603,7 +606,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ), + ):not(.mx_ThemeChoicePanel_CustomTheme button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -623,14 +626,14 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme button):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -642,7 +645,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ), + ):not(.mx_ThemeChoicePanel_CustomTheme button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -653,7 +656,9 @@ legend { .mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]), .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons - button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button), + button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( + .mx_ThemeChoicePanel_CustomTheme button + ), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -669,7 +674,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { @@ -846,7 +851,7 @@ legend { @define-mixin ThreadSummaryIcon { content: ""; display: inline-block; - mask-image: url("$(res)/img/element-icons/thread-summary.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/threads.svg"); mask-position: center; mask-repeat: no-repeat; mask-size: contain; diff --git a/res/css/_components.pcss b/res/css/_components.pcss index a7c79bfbf2e..20a6d2fe54b 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -73,7 +73,6 @@ @import "./structures/_MatrixChat.pcss"; @import "./structures/_MessagePanel.pcss"; @import "./structures/_NonUrgentToastContainer.pcss"; -@import "./structures/_NotificationPanel.pcss"; @import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_RightPanel.pcss"; @import "./structures/_RoomSearch.pcss"; @@ -259,8 +258,10 @@ @import "./views/polls/pollHistory/_PollHistory.pcss"; @import "./views/polls/pollHistory/_PollHistoryList.pcss"; @import "./views/right_panel/_BaseCard.pcss"; +@import "./views/right_panel/_EmptyState.pcss"; @import "./views/right_panel/_EncryptionInfo.pcss"; @import "./views/right_panel/_PinnedMessagesCard.pcss"; +@import "./views/right_panel/_RightPanelTabs.pcss"; @import "./views/right_panel/_RoomSummaryCard.pcss"; @import "./views/right_panel/_ThreadPanel.pcss"; @import "./views/right_panel/_TimelineCard.pcss"; @@ -306,12 +307,11 @@ @import "./views/rooms/_RoomListHeader.pcss"; @import "./views/rooms/_RoomPreviewBar.pcss"; @import "./views/rooms/_RoomPreviewCard.pcss"; +@import "./views/rooms/_RoomSearchAuxPanel.pcss"; @import "./views/rooms/_RoomSublist.pcss"; @import "./views/rooms/_RoomTile.pcss"; @import "./views/rooms/_RoomUpgradeWarningBar.pcss"; -@import "./views/rooms/_SearchBar.pcss"; @import "./views/rooms/_SendMessageComposer.pcss"; -@import "./views/rooms/_SpaceScopeHeader.pcss"; @import "./views/rooms/_Stickers.pcss"; @import "./views/rooms/_ThirdPartyMemberInfo.pcss"; @import "./views/rooms/_ThreadSummary.pcss"; diff --git a/res/css/components/views/messages/_MBeaconBody.pcss b/res/css/components/views/messages/_MBeaconBody.pcss index 82544addaef..1c5a725a0a4 100644 --- a/res/css/components/views/messages/_MBeaconBody.pcss +++ b/res/css/components/views/messages/_MBeaconBody.pcss @@ -74,7 +74,8 @@ limitations under the License. position: absolute; bottom: 0; width: 100%; - background-color: $overlay-background; + background-color: var(--cpd-color-bg-canvas-default); + opacity: 0.85; } .mx_EventTile[data-layout="bubble"] .mx_EventTile_line .mx_MBeaconBody { diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss index 44d0a344264..f0e31285cbc 100644 --- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -17,6 +17,12 @@ limitations under the License. .mx_SettingsSubsection { width: 100%; box-sizing: border-box; + + &.mx_SettingsSubsection_newUi { + display: flex; + flex-direction: column; + gap: var(--cpd-space-8x); + } } .mx_SettingsSubsection_description { @@ -54,4 +60,8 @@ limitations under the License. &.mx_SettingsSubsection_noHeading { margin-top: 0; } + &.mx_SettingsSubsection_content_newUi { + gap: var(--cpd-space-6x); + margin-top: 0; + } } diff --git a/res/css/structures/_FilePanel.pcss b/res/css/structures/_FilePanel.pcss index 1c80cde9013..186893b24bd 100644 --- a/res/css/structures/_FilePanel.pcss +++ b/res/css/structures/_FilePanel.pcss @@ -102,7 +102,3 @@ limitations under the License. padding-inline-start: 0; } } - -.mx_FilePanel_empty::before { - --maskImage: url("$(res)/img/element-icons/room/files.svg"); /* See: _RightPanel.pcss */ -} diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index f8b5cb44085..d21c435b248 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -72,30 +72,3 @@ limitations under the License. order: 2; margin: auto; } - -.mx_RightPanel_empty { - margin-right: -28px; - - h2 { - font-weight: 700; - margin: 16px 0; - } - - h2, - p { - font: var(--cpd-font-body-md-regular); - } - - &::before { - content: ""; - display: block; - margin: 11px auto 29px auto; - height: 42px; - width: 42px; - background-color: $header-panel-text-primary-color; - mask-image: var(--maskImage); - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } -} diff --git a/res/css/views/dialogs/_UserSettingsDialog.pcss b/res/css/views/dialogs/_UserSettingsDialog.pcss index 1e27bb4b6aa..6c09e90886b 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.pcss +++ b/res/css/views/dialogs/_UserSettingsDialog.pcss @@ -22,6 +22,15 @@ limitations under the License. justify-content: center; } +.mx_UserSettingsDialog_title { + /* Override default dialog font style */ + font: var(--cpd-font-heading-md-regular) !important; + + .mx_UserSettingsDialog_title_strong { + font: var(--cpd-font-heading-md-semibold); + } +} + /* ICONS */ /* ========================================================== */ diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index 67eb9b7e49c..692f7d23b39 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -27,7 +27,7 @@ limitations under the License. .mx_BaseCard_header { height: 64px; - padding: var(--cpd-space-3x); + padding: var(--cpd-space-4x); box-sizing: border-box; /* changing the color from $separator to transparent as it is the best visual output during the transition period. This will be @@ -36,8 +36,13 @@ limitations under the License. display: flex; align-items: center; justify-content: space-between; - gap: var(--cpd-space-2x); + gap: var(--cpd-space-3x); flex-shrink: 0; + border-block-end: var(--cpd-border-width-1) solid $separator; + + .mx_BaseCard_header_spacer { + flex: 1; + } > h2 { margin: 0 44px; @@ -155,52 +160,6 @@ limitations under the License. } } -.mx_BaseCard_back, -.mx_BaseCard_close { - flex-shrink: 0; - position: relative; - /* @TODO(kerrya) background colours here are not semantic - these buttons to be replaced with IconButton after secondary variant is added - https://github.com/vector-im/compound/issues/279 */ - background-color: var(--cpd-color-bg-subtle-secondary); - width: var(--BaseCard_header-button-size); - height: var(--BaseCard_header-button-size); - border-radius: 50%; - - &:hover { - background-color: var(--cpd-color-bg-subtle-primary); - } - - &::before { - content: ""; - position: absolute; - height: inherit; - width: inherit; - top: 0; - left: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 20px; - background-color: var(--cpd-color-icon-secondary); - } -} - -.mx_BaseCard_back { - order: 0; /* always first! */ - &::before { - transform: rotate(90deg); - mask-size: 22px; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); - } -} - -.mx_BaseCard_close { - order: 999; /* always last */ - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); - } -} - .mx_ContextualMenu_wrapper.mx_BaseCard_header_title { .mx_ContextualMenu { position: initial; @@ -235,7 +194,3 @@ limitations under the License. } } } - -.mx_BaseCard_headerProp { - flex: 1 1 100%; -} diff --git a/res/css/views/right_panel/_EmptyState.pcss b/res/css/views/right_panel/_EmptyState.pcss new file mode 100644 index 00000000000..4bf620bae33 --- /dev/null +++ b/res/css/views/right_panel/_EmptyState.pcss @@ -0,0 +1,45 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EmptyState { + height: 100%; + box-sizing: border-box; + padding: var(--cpd-space-4x); + text-align: center; + + svg { + width: 56px; + height: 56px; + box-sizing: border-box; + border-radius: 8px; + padding: var(--cpd-space-3x); + background-color: $panel-actions; + } + + &::before { + /* Bloom using magic numbers directly out of Figma */ + content: ""; + position: absolute; + z-index: -1; + width: 642px; + height: 775px; + right: -253.77px; + top: 0; + background: radial-gradient(49.95% 49.95% at 50% 50%, rgba(13, 189, 139, 0.12) 0%, rgba(18, 115, 235, 0) 100%); + transform: rotate(-89.69deg); + overflow: hidden; + } +} diff --git a/res/css/structures/_NotificationPanel.pcss b/res/css/views/right_panel/_RightPanelTabs.pcss similarity index 75% rename from res/css/structures/_NotificationPanel.pcss rename to res/css/views/right_panel/_RightPanelTabs.pcss index 7a3ede9e503..afaae6c6575 100644 --- a/res/css/structures/_NotificationPanel.pcss +++ b/res/css/views/right_panel/_RightPanelTabs.pcss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_NotificationPanel_empty::before { - --maskImage: url("$(res)/img/element-icons/notifications.svg"); /* See: _RightPanel.pcss */ +.mx_RightPanelTabs { + margin: 0; + height: 64px; + box-sizing: border-box; + + ul { + margin-left: 16px; + } } diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index 4c3ff2f8886..549eb69ee43 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -235,28 +235,15 @@ limitations under the License. } .mx_RoomSummaryCard_header { - padding: 15px 12px; + padding: 24px 12px 15px; } -.mx_RoomSummaryCard_search input { - /* Overriding very broad CSS rules */ - border: 0 !important; - margin: 0 !important; - cursor: pointer; -} +.mx_RoomSummaryCard_search { + flex-grow: 1; + min-width: 0; -.mx_RoomSummaryCard_searchBtn { - background: var(--cpd-color-bg-canvas-default); - color: var(--cpd-color-icon-primary); - border: 1px solid var(--cpd-color-gray-400); - border-radius: 50%; - width: 36px; - height: 36px; - padding: var(--cpd-space-2x); - cursor: pointer; - - &:hover { - background: var(--cpd-color-bg-subtle-primary); + input[type="search"]::-webkit-search-cancel-button { + display: unset; /* override _common.pcss which inhibits this */ } } diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index 104430c190e..fc1d39c9cae 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -18,12 +18,13 @@ limitations under the License. height: 100px; overflow: visible; + /* Unset flex on the thread list, but not the thread view */ + &:not(.mx_ThreadView) .mx_BaseCard_header .mx_BaseCard_header_title { + flex: unset; + } + .mx_BaseCard_header { .mx_BaseCard_header_title { - .mx_BaseCard_header_title_heading { - margin-right: auto; - } - .mx_AccessibleButton { font-size: 12px; color: $secondary-content; @@ -106,10 +107,17 @@ limitations under the License. } .mx_RoomView_messagePanel { - /* To avoid the rule from being applied to .mx_ThreadPanel_empty */ + &.mx_RoomView_messageListWrapper { + position: initial; + } + .mx_RoomView_messageListWrapper { width: calc(100% + 6px); /* 8px - 2px */ } + + .mx_RoomView_empty { + display: contents; + } } .mx_RoomView_MessageList { @@ -168,72 +176,6 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/link.svg"); } -.mx_ThreadPanel_empty { - border-radius: 8px; - background: $background; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: absolute; - top: 0; - bottom: 0; - left: 0; - padding: 20px; - box-sizing: border-box; /* Include padding and border */ - width: 100%; - - h2 { - color: $primary-content; - font-weight: var(--cpd-font-weight-semibold); - font-size: $font-18px; - margin-top: 24px; - margin-bottom: 10px; - } - - p { - font-size: $font-15px; - color: $secondary-content; - margin: 10px 0; - } - - button { - border: none; - background: none; - color: $accent; - font-size: $font-15px; - - &:hover, - &:active { - text-decoration: underline; - cursor: pointer; - } - } - - .mx_ThreadPanel_empty_tip { - font-size: $font-12px; - line-height: $font-15px; - - > b { - font-weight: var(--cpd-font-weight-semibold); - } - } -} - -.mx_ThreadPanel_largeIcon { - width: 28px; - height: 28px; - padding: 18px; - background: $system; - border-radius: 50%; - - &::after { - @mixin ThreadSummaryIcon; - width: inherit; - height: inherit; - } -} - .mx_ContextualMenu_wrapper { .mx_ThreadPanel_Header_FilterOptionItem { display: flex; diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index 1dfd39dd250..a2e156e0e58 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -41,35 +41,17 @@ limitations under the License. } } - h2 { - font-size: $font-18px; - font-weight: var(--cpd-font-weight-semibold); - margin: 18px 0 0 0; - } - .mx_UserInfo_container { - padding: $spacing-8 $spacing-16; - - &:not(.mx_UserInfo_separator) { - padding-top: $spacing-16; - padding-bottom: 0; - - > :not(h3) { - margin-inline-start: $spacing-8; - display: flex; - flex-flow: column; - align-items: flex-start; - row-gap: $spacing-8; - } - } + padding: var(--cpd-space-4x) 0; + margin: 0 var(--cpd-space-4x); .mx_UserInfo_container_verifyButton { margin-top: $spacing-8; } - } - .mx_UserInfo_separator { - border-bottom: 1px solid $separator; + & + .mx_UserInfo_container { + border-top: 1px solid $separator; + } } .mx_UserInfo_memberDetailsContainer { @@ -94,7 +76,7 @@ limitations under the License. margin: $spacing-24 $spacing-32 0 $spacing-32; .mx_UserInfo_avatar_transition { - max-width: 30vh; + max-width: 120px; aspect-ratio: 1 / 1; margin: 0 auto; transition: 0.5s; @@ -112,7 +94,7 @@ limitations under the License. } } - h3 { + h2 { text-transform: uppercase; color: $tertiary-content; font: var(--cpd-font-heading-sm-semibold); @@ -125,41 +107,36 @@ limitations under the License. } .mx_UserInfo_profile { - text-align: center; - - h2 { - display: flex; - font-size: $font-17px; + h1 { + font-size: $font-20px; line-height: $font-25px; - flex: 1; - justify-content: center; - /* We reverse things here so for accessible technologies the name comes before the e2e shield */ - flex-direction: row-reverse; - - span { - /* limit to 2 lines, show an ellipsis if it overflows */ - /* this looks webkit specific but is supported by Firefox 68+ */ - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - - overflow: hidden; - word-break: break-all; - text-overflow: ellipsis; - } - .mx_E2EIcon { - margin-top: 3px; /* visual vertical centering to the top line of text. */ - margin-inline-end: $spacing-4; /* margin from displayName */ - min-width: 18px; /* convince flexbox to not collapse it */ + /* limit to 2 lines, show an ellipsis if it overflows */ + /* this looks webkit specific but is supported by Firefox 68+ */ + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + overflow: hidden; + word-break: break-all; + text-overflow: ellipsis; + + /* E2E icon wrapper */ + .mx_Flex > span { + display: inline-block; } } .mx_UserInfo_profileStatus { - margin-top: $spacing-12; + margin: var(--cpd-space-1x) 0; } } + .mx_PresenceLabel { + font: var(--cpd-font-body-sm-regular); + opacity: 1; + } + .mx_UserInfo_memberDetails { .mx_UserInfo_profileField { display: flex; @@ -184,10 +161,6 @@ limitations under the License. .mx_UserInfo_field { line-height: $font-16px; - - &.mx_UserInfo_destructive { - color: $alert; - } } .mx_UserInfo_statusMessage { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index ad88a7c7863..66c60f5f155 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -689,6 +689,7 @@ $left-gutter: 64px; font-family: inherit !important; white-space: normal !important; line-height: inherit !important; + background-color: inherit; color: inherit; /* inherit the colour from the dark or light theme by default (but not for code blocks) */ pre, @@ -701,7 +702,7 @@ $left-gutter: 64px; background-color: $inlinecode-background-color; border: 1px solid $inlinecode-border-color; border-radius: 4px; - /* The horizontal padding is added by gfm.css .markdown-body */ + /* The horizontal padding is added by github-markdown-css .markdown-body */ padding: $spacing-2 0; /* Avoid inline code blocks to be sticked when on multiple lines */ line-height: $font-22px; @@ -1023,7 +1024,7 @@ $left-gutter: 64px; $notification-dot-size: 8px; /* notification dot next to the timestamp */ margin: calc(var(--topOffset) + $hrHeight) 0 var(--topOffset); /* include the height of horizontal line */ - padding: $padding $spacing-24 $padding $padding; + padding: $padding; border-radius: $borderRadius; display: flex; @@ -1038,7 +1039,7 @@ $left-gutter: 64px; &::after { $inset-block-start: auto; - $inset-inline-end: calc(32px - $padding); + $inset-inline-end: calc(-1 * var(--cpd-space-2x)); $inset-block-end: calc(-1 * var(--topOffset) - $hrHeight); /* exclude the height of horizontal line */ $inset-inline-start: calc(var(--leftOffset) + $padding); inset: $inset-block-start $inset-inline-end $inset-block-end $inset-inline-start; diff --git a/res/css/views/rooms/_MemberList.pcss b/res/css/views/rooms/_MemberList.pcss index 086a60810fd..6e2e5a43a4d 100644 --- a/res/css/views/rooms/_MemberList.pcss +++ b/res/css/views/rooms/_MemberList.pcss @@ -19,6 +19,7 @@ limitations under the License. display: flex; flex-direction: column; min-height: 0; + margin-top: 24px; .mx_Spinner { flex: 1 0 auto; diff --git a/res/css/views/rooms/_MessageComposer.pcss b/res/css/views/rooms/_MessageComposer.pcss index 2c8fe592c42..d88350e946f 100644 --- a/res/css/views/rooms/_MessageComposer.pcss +++ b/res/css/views/rooms/_MessageComposer.pcss @@ -163,7 +163,7 @@ limitations under the License. word-break: break-word; /* FIXME: rather unpleasant hack to get rid of

margins. */ - /* really we should be mixing in markdown-body from gfm.css instead */ + /* really we should be mixing in markdown-body from github-markdown-css instead */ > :first-child { margin-top: 0 !important; } diff --git a/res/css/views/rooms/_PresenceLabel.pcss b/res/css/views/rooms/_PresenceLabel.pcss index 5be83c77d7c..e775fb08ea8 100644 --- a/res/css/views/rooms/_PresenceLabel.pcss +++ b/res/css/views/rooms/_PresenceLabel.pcss @@ -18,3 +18,7 @@ limitations under the License. font-size: $font-11px; opacity: 0.5; } + +.mx_PresenceLabel_online { + color: var(--cpd-color-text-success-primary); +} diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index ca1f8ec48ac..5409c050bb8 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -20,7 +20,7 @@ limitations under the License. padding: 0 var(--cpd-space-3x); border-bottom: 1px solid $separator; background-color: $background; - transition: all 0.3s ease; + transition: all 0.2s ease; } .mx_RoomHeader:hover { @@ -74,14 +74,17 @@ limitations under the License. } } -.mx_RoomHeader:hover .mx_RoomHeader_topic { - /* height needed to compute the transition, it equals to the `line-height` +.mx_RoomHeader:hover, +.mx_RoomHeader:focus-within { + .mx_RoomHeader_topic { + /* height needed to compute the transition, it equals to the `line-height` value in pixels */ - height: calc($font-13px * 1.5); - opacity: 1; + height: calc($font-13px * 1.5); + opacity: 1; - a:hover { - text-decoration: underline; + a:hover { + text-decoration: underline; + } } } diff --git a/res/css/views/rooms/_RoomSearchAuxPanel.pcss b/res/css/views/rooms/_RoomSearchAuxPanel.pcss new file mode 100644 index 00000000000..a47616a6857 --- /dev/null +++ b/res/css/views/rooms/_RoomSearchAuxPanel.pcss @@ -0,0 +1,72 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RoomSearchAuxPanel { + /* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */ + min-height: 84px; + display: flex; + align-items: center; + border-color: var(--cpd-color-bg-canvas-default); + border-style: solid; + border-width: 1px 0; + padding: var(--cpd-space-3x); + box-sizing: border-box; + gap: var(--cpd-space-2x); + + .mx_RoomSearchAuxPanel_summary { + flex-grow: 1; + display: inherit; /* flex */ + gap: var(--cpd-space-2x); + align-items: center; + overflow: hidden; + + > svg { + padding: var(--cpd-space-2x); + border-radius: var(--cpd-space-2x); + background-color: var(--cpd-color-bg-subtle-secondary); + color: var(--cpd-color-icon-secondary); + flex-shrink: 0; + } + + .mx_RoomSearchAuxPanel_summary_text { + display: flex; + flex-direction: column; + font-size: $font-15px; + line-height: $font-22px; + overflow: hidden; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .mx_SearchWarning { + display: contents; + font-size: $font-13px; + line-height: $font-20px; + color: var(--cpd-color-text-secondary); + } + } + + .mx_RoomSearchAuxPanel_buttons { + display: inherit; /* flex */ + gap: var(--cpd-space-6x); + align-items: center; + flex-shrink: 0; + } +} diff --git a/res/css/views/rooms/_SearchBar.pcss b/res/css/views/rooms/_SearchBar.pcss deleted file mode 100644 index ca999c7beaf..00000000000 --- a/res/css/views/rooms/_SearchBar.pcss +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_SearchBar { - /* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */ - min-height: 56px; - display: flex; - align-items: center; - border-bottom: 1px solid $primary-hairline-color; - - .mx_SearchBar_input { - --size-button-search: 37px; /* size of the search button inside `input` element */ - - /* border: 1px solid $input-border-color; */ - /* font-size: $font-15px; */ - flex: 1 1 0; - margin-left: 22px; - - /* do not allow the input element to shrink below the width needed for the placeholder 'Search…' - and the search button */ - min-width: calc(7em + var(--size-button-search)); - - input { - box-sizing: border-box; /* include padding value into width calculation */ - } - } - - .mx_SearchBar_searchButton { - cursor: pointer; - width: var(--size-button-search); - height: var(--size-button-search); - background-color: $accent; - mask: url("$(res)/img/feather-customised/search-input.svg"); - mask-repeat: no-repeat; - mask-position: center; - } - - .mx_SearchBar_buttons { - display: inherit; /* flex */ - min-width: 0; /* have the close button displayed even on a very narrow timeline */ - } - - .mx_SearchBar_button { - border: 0; - margin: 0 0 0 22px; - padding: 5px; - font-size: $font-15px; - cursor: pointer; - color: $primary-content; - border-bottom: 2px solid $accent; - font-weight: var(--cpd-font-weight-semibold); - word-break: break-all; /* prevent the input area and cancel button from being overlapped by BaseCard */ - } - - .mx_SearchBar_unselected { - color: $input-darker-fg-color; - border-color: transparent; - } - - .mx_SearchBar_cancel { - background-color: $alert; - mask: url("$(res)/img/cancel.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 14px; - padding: 9px; - margin: 0 12px 0 3px; - cursor: pointer; - } -} diff --git a/res/css/views/settings/_LayoutSwitcher.pcss b/res/css/views/settings/_LayoutSwitcher.pcss index 571b9a1cf1c..96fe27b420c 100644 --- a/res/css/views/settings/_LayoutSwitcher.pcss +++ b/res/css/views/settings/_LayoutSwitcher.pcss @@ -15,79 +15,80 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_LayoutSwitcher_RadioButtons { +.mx_LayoutSwitcher_LayoutSelector { display: flex; - flex-direction: row; - gap: 24px; - width: 100%; - - color: $primary-content; - - > .mx_LayoutSwitcher_RadioButton { - flex-grow: 0; - flex-shrink: 1; - display: flex; - flex-direction: column; - overflow: hidden; - - flex-basis: 33%; - min-width: 0; - - border: 1px solid $quinary-content; - border-radius: 10px; - - .mx_EventTile_msgOption, - .mx_MessageActionBar { - display: none; - } - - .mx_LayoutSwitcher_RadioButton_preview { - flex-grow: 1; + flex-direction: column; + /** + * The settings form has a default gap of 10px + * We want to have a bigger gap between the layout options + */ + gap: var(--cpd-space-4x) !important; + + .mxLayoutSwitcher_LayoutSelector_LayoutRadio { + border: 1px solid var(--cpd-color-border-interactive-primary); + border-radius: var(--cpd-space-2x); + + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline { display: flex; + /* + * 10px + */ + gap: calc(var(--cpd-space-2x) + var(--cpd-space-0-5x)); align-items: center; - padding: 10px; - pointer-events: none; - - .mx_EventTile[data-layout="bubble"] .mx_EventTile_line { - padding-right: 11px; - } } - .mx_StyledRadioButton { - flex-grow: 0; - padding: 10px; + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline, + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview { + margin: var(--cpd-space-3x); } - .mx_EventTile_content { - margin-right: 0; - } - - &.mx_LayoutSwitcher_RadioButton_selected { - border-color: var(--cpd-color-bg-accent-rest); - } - } - - .mx_StyledRadioButton { - border-top: 1px solid $quinary-content; - } - - .mx_StyledRadioButton_checked { - background-color: var(--cpd-color-bg-subtle-secondary); - } + /** + * Override the event tile style to make it fit in the selector + * Tweak also hover style and remove action bar + */ + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview { + pointer-events: none; - .mx_EventTile { - margin: 0; - &[data-layout="bubble"] { - margin-right: 40px; - flex-shrink: 1; - } - &[data-layout="irc"] { - > a { - display: none; + .mx_EventTile { + margin: 0; + + /** + * Hide the message options and message action bar in the preview + */ + .mx_EventTile_msgOption, + .mx_MessageActionBar { + display: none; + } + + .mx_EventTile_content { + margin-right: 0; + } + + &[data-layout="group"] { + margin-top: calc(var(--cpd-space-3x) * -1); + } + + /** + * Add margin to center the bubble + */ + &[data-layout="bubble"] { + /** + * Add the layout margin and the margin to vertically center the bubble + */ + margin-top: var(--cpd-space-6x); + margin-right: 34px; + flex-shrink: 1; + } + + .mx_EventTile_line { + max-width: 100%; + } } } - .mx_EventTile_line { - max-width: 90%; + + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_separator { + border-top: 0; + border-bottom: 1px solid var(--cpd-color-border-interactive-secondary); } } } diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index 8616668224d..f70cdf92e3c 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -14,48 +14,72 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ThemeChoicePanel_themeSelectors { - color: $primary-content; +.mx_ThemeChoicePanel_ThemeSelectors { display: flex; - flex-direction: row; flex-wrap: wrap; + /* Override form default style */ + flex-direction: row !important; + gap: var(--cpd-space-4x) !important; - > .mx_StyledRadioButton { - align-items: center; - padding: $font-16px; - box-sizing: border-box; - border-radius: 10px; - width: 180px; + .mx_ThemeChoicePanel_themeSelector { + border: 1px solid var(--cpd-color-border-interactive-secondary); + border-radius: var(--cpd-space-1-5x); + padding: var(--cpd-space-3x) var(--cpd-space-5x) var(--cpd-space-3x) var(--cpd-space-3x); + gap: var(--cpd-space-2x); + background-color: var(--cpd-color-bg-canvas-default); - background: $accent-200; - opacity: 0.4; + &.mx_ThemeChoicePanel_themeSelector_enabled { + border-color: var(--cpd-color-border-interactive-primary); + } - flex-shrink: 1; - flex-grow: 0; + &.mx_ThemeChoicePanel_themeSelector_disabled { + border-color: var(--cpd-color-border-disabled); + } - margin-right: 15px; - margin-top: 10px; + .mx_ThemeChoicePanel_themeSelector_Label { + color: var(--cpd-color-text-primary); + font: var(--cpd-font-body-md-semibold); + } + } +} - font-weight: var(--cpd-font-weight-semibold); +.mx_ThemeChoicePanel_CustomTheme { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); - > span { - justify-content: center; - } + .mx_ThemeChoicePanel_CustomTheme_EditInPlace input:focus { + /* + * When the input is focused, the border is growing + * We need to move it a bit to avoid the left border to be under the left panel + */ + margin-left: var(--cpd-space-0-5x); } - > .mx_StyledRadioButton_enabled { - opacity: 1; + .mx_ThemeChoicePanel_CustomThemeList { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + /* + * Override the default padding/margin of the list + */ + padding: 0; + margin: 0; - /* These colors need to be hardcoded because they don't change with the theme */ - &.mx_ThemeSelector_light { - background-color: #f3f8fd; - color: #2e2f32; - } + .mx_ThemeChoicePanel_CustomThemeList_theme { + display: flex; + justify-content: space-between; + align-items: center; + background-color: var(--cpd-color-gray-200); + padding: var(--cpd-space-2x) var(--cpd-space-2x) var(--cpd-space-2x) var(--cpd-space-4x); - &.mx_ThemeSelector_dark { - /* 5% lightened version of 181b21 */ - background-color: #25282e; - color: #f3f8fd; + .mx_ThemeChoicePanel_CustomThemeList_name { + font: var(--cpd-font-body-sm-semibold); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } } diff --git a/res/css/views/settings/_UserProfileSettings.pcss b/res/css/views/settings/_UserProfileSettings.pcss index 3a9dc7dcc74..31c1ef628f9 100644 --- a/res/css/views/settings/_UserProfileSettings.pcss +++ b/res/css/views/settings/_UserProfileSettings.pcss @@ -21,12 +21,11 @@ limitations under the License. display: flex; margin-top: var(--cpd-space-6x); gap: 16px; - /* This is temporary until the 'Remove' link is replaced by a context menu. */ - margin-bottom: 20px; .mx_UserProfileSettings_profile_displayName { flex-grow: 1; width: 100%; + gap: 0; } } @@ -36,6 +35,7 @@ limitations under the License. .mx_UserProfileSettings_profile_controls_userId { width: 100%; + margin-top: var(--cpd-space-4x); .mx_CopyableText { margin-top: var(--cpd-space-1x); width: 100%; @@ -47,6 +47,15 @@ limitations under the License. font-size: 15px; font-weight: 500; } + + .mx_UserProfileSettings_profile_buttons { + margin-top: var(--cpd-space-8x); + margin-bottom: var(--cpd-space-8x); + } + + .mx_UserProfileSettings_accountmanageIcon { + margin-right: var(--cpd-space-2x); + } } @media (max-width: 768px) { diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss index 76c5834fa83..a59f64b3911 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss @@ -34,3 +34,8 @@ limitations under the License. margin-right: $spacing-8; margin-bottom: 2px; } + +.mx_GeneralUserSettingsTab_section_hint { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); +} diff --git a/res/css/views/terms/_InlineTermsAgreement.pcss b/res/css/views/terms/_InlineTermsAgreement.pcss index d7732b2a0dd..162d1341e4d 100644 --- a/res/css/views/terms/_InlineTermsAgreement.pcss +++ b/res/css/views/terms/_InlineTermsAgreement.pcss @@ -15,6 +15,7 @@ limitations under the License. */ .mx_InlineTermsAgreement_cbContainer { + margin-top: var(--cpd-space-4x); margin-bottom: 10px; font: var(--cpd-font-body-md-regular); diff --git a/res/img/element-icons/thread-summary.svg b/res/img/element-icons/thread-summary.svg deleted file mode 100644 index 2c4f0ead0cf..00000000000 --- a/res/img/element-icons/thread-summary.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/themes/dark-custom/css/dark-custom.pcss b/res/themes/dark-custom/css/dark-custom.pcss index 3acf914e69d..166d9cb2921 100644 --- a/res/themes/dark-custom/css/dark-custom.pcss +++ b/res/themes/dark-custom/css/dark-custom.pcss @@ -5,3 +5,4 @@ @import "../../legacy-dark/css/_legacy-dark.pcss"; @import "../../light-custom/css/_custom.pcss"; @import "../../../../res/css/_components.pcss"; +@import url("github-markdown-css/github-markdown-dark.css"); diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index 326debc0628..d398fb59677 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -14,7 +14,7 @@ $overlay-background: var(--cpd-color-alpha-gray-1300); $panels: var(--cpd-color-bg-subtle-secondary); $panel-actions: var(--cpd-color-alpha-gray-300); -$separator: var(--cpd-color-alpha-gray-400); +$separator: var(--cpd-color-gray-400); /* ******************** */ diff --git a/res/themes/dark/css/dark.pcss b/res/themes/dark/css/dark.pcss index d71f544fe21..51262598bea 100644 --- a/res/themes/dark/css/dark.pcss +++ b/res/themes/dark/css/dark.pcss @@ -6,3 +6,4 @@ @import "../../light/css/_mods.pcss"; @import "../../../../res/css/_components.pcss"; @import url("highlight.js/styles/atom-one-dark.min.css"); +@import url("github-markdown-css/github-markdown-dark.css"); diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss index 7e14e85f10a..c6840f5b907 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.pcss +++ b/res/themes/legacy-dark/css/_legacy-dark.pcss @@ -105,7 +105,7 @@ $overlay-background: rgba($background, 0.85); $panels: rgba($system, 0.9); $panel-actions: $roomtile-selected-bg-color; -$separator: var(--cpd-color-alpha-gray-400); +$separator: var(--cpd-color-gray-400); /** * Creating a `semantic` color scale. This will not be needed with the new diff --git a/res/themes/legacy-dark/css/legacy-dark.pcss b/res/themes/legacy-dark/css/legacy-dark.pcss index 061934d890a..ffb7e8d0aa3 100644 --- a/res/themes/legacy-dark/css/legacy-dark.pcss +++ b/res/themes/legacy-dark/css/legacy-dark.pcss @@ -5,3 +5,4 @@ @import "_legacy-dark.pcss"; @import "../../../../res/css/_components.pcss"; @import url("highlight.js/styles/atom-one-dark.min.css"); +@import url("github-markdown-css/github-markdown-dark.css"); diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 5f9b8fd4520..e40fbde72b6 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -163,7 +163,7 @@ $overlay-background: rgba($background, 0.85); $panels: rgba($system, 0.9); $panel-actions: $roomtile-selected-bg-color; -$separator: var(--cpd-color-alpha-gray-400); +$separator: var(--cpd-color-gray-400); /* Legacy theme backports */ diff --git a/res/themes/legacy-light/css/legacy-light.pcss b/res/themes/legacy-light/css/legacy-light.pcss index f44651e6892..4b3905a1f98 100644 --- a/res/themes/legacy-light/css/legacy-light.pcss +++ b/res/themes/legacy-light/css/legacy-light.pcss @@ -4,3 +4,4 @@ @import "_legacy-light.pcss"; @import "../../../../res/css/_components.pcss"; @import url("highlight.js/styles/atom-one-light.min.css"); +@import url("github-markdown-css/github-markdown-light.css"); diff --git a/res/themes/light-custom/css/_custom.pcss b/res/themes/light-custom/css/_custom.pcss index d43740a8d97..0c5758f7290 100644 --- a/res/themes/light-custom/css/_custom.pcss +++ b/res/themes/light-custom/css/_custom.pcss @@ -18,7 +18,7 @@ $font-family: var(--font-family, $font-family); $monospace-font-family: var(--font-family-monospace, $monospace-font-family); /* Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A741 */ -$accent: var(--accent, $accent); +$accent: var(--accent-color, $accent); $alert: var(--alert, $alert); $links: var(--links, $links); $primary-content: var(--primary-content, $primary-content); diff --git a/res/themes/light-custom/css/light-custom.pcss b/res/themes/light-custom/css/light-custom.pcss index eeba1002e58..0bf3bd9ba2b 100644 --- a/res/themes/light-custom/css/light-custom.pcss +++ b/res/themes/light-custom/css/light-custom.pcss @@ -4,3 +4,4 @@ @import "../../legacy-light/css/_legacy-light.pcss"; @import "_custom.pcss"; @import "../../../../res/css/_components.pcss"; +@import url("github-markdown-css/github-markdown-light.css"); diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss index 2ba97c5a722..a306e769b00 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -64,14 +64,7 @@ $accent-1400: var(--cpd-color-green-1400); outline-offset: 2px; } -/* Add padding (and remove margin to compensate), so the outline is not */ -/* chopped off on the left */ -.mx_TabbedView_tabPanel { - margin-left: 236px !important; /* Remove 4 to allow 4 in mx_SettingsTab */ -} -.mx_SettingsTab { - padding-left: 4px !important; -} +/* Add padding, so the outline is not chopped off on the left */ .mx_BaseCard { padding-left: 4px !important; /* Remove 4 to allow 4 in mx_BaseCard_Group */ } diff --git a/res/themes/light-high-contrast/css/light-high-contrast.pcss b/res/themes/light-high-contrast/css/light-high-contrast.pcss index 4d05078f787..a843809e510 100644 --- a/res/themes/light-high-contrast/css/light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/light-high-contrast.pcss @@ -6,3 +6,4 @@ @import "../../light/css/_mods.pcss"; @import "../../../../res/css/_components.pcss"; @import url("highlight.js/styles/atom-one-light.min.css"); +@import url("github-markdown-css/github-markdown-light.css"); diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index 730c1155143..1a237427f21 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -32,7 +32,7 @@ $overlay-background: var(--cpd-color-alpha-gray-1300); $panels: var(--cpd-color-bg-subtle-secondary); $panel-actions: var(--cpd-color-alpha-gray-300); -$separator: var(--cpd-color-alpha-gray-400); +$separator: var(--cpd-color-gray-400); $accent: var(--cpd-color-text-action-accent); $alert: var(--cpd-color-text-critical-primary); diff --git a/res/themes/light/css/light.pcss b/res/themes/light/css/light.pcss index 361484828f2..d3264e491e7 100644 --- a/res/themes/light/css/light.pcss +++ b/res/themes/light/css/light.pcss @@ -5,3 +5,4 @@ @import "_mods.pcss"; @import "../../../../res/css/_components.pcss"; @import url("highlight.js/styles/atom-one-light.min.css"); +@import url("github-markdown-css/github-markdown-light.css"); diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index c62733c0f07..e42e83d58db 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -224,6 +224,14 @@ 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/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 548555cd1dd..99ad2e8caa1 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -20,6 +20,7 @@ import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescri import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { PosthogAnalytics } from "./PosthogAnalytics"; +import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto"; /** The key that we use to store the `reportedEvents` bloom filter in localstorage */ const DECRYPTION_FAILURE_STORAGE_KEY = "mx_decryption_failure_event_ids"; @@ -77,7 +78,10 @@ export class DecryptionFailureTracker { // Map JS-SDK error codes to tracker codes for aggregation switch (errorCode) { case DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID: + case DecryptionFailureCode.MEGOLM_KEY_WITHHELD: return "OlmKeysNotSentError"; + case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: + return "RoomKeysWithheldForUnverifiedDevice"; case DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX: return "OlmIndexError"; case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP: @@ -207,7 +211,7 @@ export class DecryptionFailureTracker { */ private eventDecrypted(e: MatrixEvent, nowTs: number): void { // for now we only track megolm decryption failures - if (e.getWireContent().algorithm != "m.megolm.v1.aes-sha2") { + if (e.getWireContent().algorithm != MEGOLM_ENCRYPTION_ALGORITHM) { return; } const errCode = e.decryptionFailureReason; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index b63ed1dcf0c..888c30d76cc 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -20,13 +20,11 @@ limitations under the License. import React, { LegacyRef, ReactNode } from "react"; import sanitizeHtml from "sanitize-html"; import classNames from "classnames"; -import EMOJIBASE_REGEX from "emojibase-regex"; import katex from "katex"; import { decode } from "html-entities"; import { IContent } from "matrix-js-sdk/src/matrix"; import { Optional } from "matrix-events-sdk"; import escapeHtml from "escape-html"; -import GraphemeSplitter from "graphemer"; import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings"; import { IExtendedSanitizeOptions } from "./@types/sanitize-html"; @@ -34,6 +32,7 @@ import SettingsStore from "./settings/SettingsStore"; import { stripHTMLReply, stripPlainReply } from "./utils/Reply"; import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils"; import { sanitizeHtmlParams, transformTags } from "./Linkify"; +import { graphemeSegmenter } from "./utils/strings"; export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify"; @@ -46,10 +45,35 @@ const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; const SYMBOL_PATTERN = /([\u2100-\u2bff])/; // Regex pattern for non-emoji characters that can appear in an "all-emoji" message -// (Zero-Width Joiner, Zero-Width Space, Emoji presentation character, other whitespace) -const EMOJI_SEPARATOR_REGEX = /[\u200D\u200B\s]|\uFE0F/g; +// (Zero-Width Space, other whitespace) +const EMOJI_SEPARATOR_REGEX = /[\u200B\s]/g; + +// Regex for emoji. This includes any RGI_Emoji sequence followed by an optional +// emoji presentation VS (U+FE0F), but not those sequences that are followed by +// a text presentation VS (U+FE0E). We also count lone regional indicators +// (U+1F1E6-U+1F1FF). Technically this regex produces false negatives for emoji +// followed by U+FE0E when the emoji doesn't have a text variant, but in +// practice this doesn't matter. +export const EMOJI_REGEX = (() => { + try { + // Per our support policy, v mode is available to us, but we still don't + // want the app to completely crash on older platforms. We use the + // constructor here to avoid a syntax error on such platforms. + return new RegExp("\\p{RGI_Emoji}(?!\\uFE0E)(?:(? { + try { + return new RegExp(`^(${EMOJI_REGEX.source})+$`, "iv"); + } catch (_e) { + // Fall back, just like for EMOJI_REGEX + return /(?!)/; + } +})(); /* * Return true if the given string contains emoji @@ -124,6 +148,7 @@ const topicSanitizeHtmlParams: IExtendedSanitizeOptions = { allowedTags: [ "font", // custom to matrix for IRC-style font coloring "del", // for markdown + "s", "a", "sup", "sub", @@ -264,17 +289,16 @@ export function formatEmojis(message: string | undefined, isHtmlMessage?: boolea let text = ""; let key = 0; - const splitter = new GraphemeSplitter(); - for (const char of splitter.iterateGraphemes(message)) { - if (EMOJIBASE_REGEX.test(char)) { + for (const data of graphemeSegmenter.segment(message)) { + if (EMOJI_REGEX.test(data.segment)) { if (text) { result.push(text); text = ""; } - result.push(emojiToSpan(char, key)); + result.push(emojiToSpan(data.segment, key)); key++; } else { - text += char; + text += data.segment; } } if (text) { diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index c3a919648ea..4b57e2098dc 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -219,6 +219,7 @@ export interface IConfigOptions { export interface ISsoRedirectOptions { immediate?: boolean; on_welcome_page?: boolean; + on_login_page?: boolean; } /* :tchap: tchap-features-from-config diff --git a/src/Keyboard.ts b/src/Keyboard.ts index 7b1ea4031be..33ce4e4e72a 100644 --- a/src/Keyboard.ts +++ b/src/Keyboard.ts @@ -29,6 +29,7 @@ export const Key = { ARROW_DOWN: "ArrowDown", ARROW_LEFT: "ArrowLeft", ARROW_RIGHT: "ArrowRight", + F6: "F6", TAB: "Tab", ESCAPE: "Escape", ENTER: "Enter", @@ -77,6 +78,7 @@ export const Key = { }; export const IS_MAC = navigator.platform.toUpperCase().includes("MAC"); +export const IS_ELECTRON = window.electron; export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean { if (IS_MAC) { diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 7abdb236aeb..145784fe0c4 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -66,6 +66,7 @@ import { localNotificationsAreSilenced } from "./utils/notifications"; import { SdkContextClass } from "./contexts/SDKContext"; import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog"; import { isNotNull } from "./Typeguards"; +import { BackgroundAudio } from "./audio/BackgroundAudio"; export const PROTOCOL_PSTN = "m.protocol.pstn"; export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn"; @@ -157,8 +158,6 @@ export default class LegacyCallHandler extends EventEmitter { // Calls started as an attended transfer, ie. with the intention of transferring another // call with a different party to this one. private transferees = new Map(); // callId (target) -> call (transferee) - private audioPromises = new Map>(); - private audioElementsWithListeners = new Map(); private supportsPstnProtocol: boolean | null = null; private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native @@ -170,6 +169,9 @@ export default class LegacyCallHandler extends EventEmitter { private silencedCalls = new Set(); // callIds + private backgroundAudio = new BackgroundAudio(); + private playingSources: Record = {}; // Record them for stopping + public static get instance(): LegacyCallHandler { if (!window.mxLegacyCallHandler) { window.mxLegacyCallHandler = new LegacyCallHandler(); @@ -199,33 +201,11 @@ export default class LegacyCallHandler extends EventEmitter { } public start(): void { - // add empty handlers for media actions, otherwise the media keys - // end up causing the audio elements with our ring/ringback etc - // audio clips in to play. - if (navigator.mediaSession) { - navigator.mediaSession.setActionHandler("play", function () {}); - navigator.mediaSession.setActionHandler("pause", function () {}); - navigator.mediaSession.setActionHandler("seekbackward", function () {}); - navigator.mediaSession.setActionHandler("seekforward", function () {}); - navigator.mediaSession.setActionHandler("previoustrack", function () {}); - navigator.mediaSession.setActionHandler("nexttrack", function () {}); - } - if (SettingsStore.getValue(UIFeature.Voip)) { MatrixClientPeg.safeGet().on(CallEventHandlerEvent.Incoming, this.onCallIncoming); } this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); - - // Add event listeners for the

-

{_t("file_panel|empty_heading")}

-

{_t("file_panel|empty_description")}

-
+ ); const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId); diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 084afdaf8ba..7ef3bc4ba31 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -44,6 +44,7 @@ import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButto import PosthogTrackers from "../../PosthogTrackers"; import PageType from "../../PageTypes"; import { UserOnboardingButton } from "../views/user-onboarding/UserOnboardingButton"; +import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation"; interface IProps { isMinimized: boolean; @@ -308,6 +309,16 @@ export default class LeftPanel extends React.Component { } break; } + + const navAction = getKeyBindingsManager().getNavigationAction(ev); + if (navAction === KeyBindingAction.PreviousLandmark || navAction === KeyBindingAction.NextLandmark) { + ev.stopPropagation(); + ev.preventDefault(); + LandmarkNavigation.findAndFocusNextLandmark( + Landmark.ROOM_SEARCH, + navAction === KeyBindingAction.PreviousLandmark, + ); + } }; private renderBreadcrumbs(): React.ReactNode { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 4481e4f6038..755d2c11568 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -75,6 +75,7 @@ import { PipContainer } from "./PipContainer"; import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushRules"; import { ConfigOptions } from "../../SdkConfig"; import { MatrixClientContextProvider } from "./MatrixClientContextProvider"; +import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -458,9 +459,7 @@ class LoggedInView extends React.Component { handled = true; break; case KeyBindingAction.SearchInRoom: - dis.dispatch({ - action: "focus_search", - }); + dis.fire(Action.FocusMessageSearch); handled = true; break; } @@ -472,6 +471,14 @@ class LoggedInView extends React.Component { const navAction = getKeyBindingsManager().getNavigationAction(ev); switch (navAction) { + case KeyBindingAction.NextLandmark: + case KeyBindingAction.PreviousLandmark: + LandmarkNavigation.findAndFocusNextLandmark( + Landmark.MESSAGE_COMPOSER_OR_HOME, + navAction === KeyBindingAction.PreviousLandmark, + ); + handled = true; + break; case KeyBindingAction.FilterRooms: dis.dispatch({ action: "focus_room_filter", @@ -490,11 +497,15 @@ class LoggedInView extends React.Component { handled = true; break; case KeyBindingAction.GoToHome: + // even if we cancel because there are modals open, we still + // handled it: nothing else should happen. + handled = true; + if (Modal.hasDialogs()) { + return; + } dis.dispatch({ action: Action.ViewHomePage, }); - Modal.closeCurrentModal("homeKeyboardShortcut"); - handled = true; break; case KeyBindingAction.ToggleSpacePanel: dis.fire(Action.ToggleSpacePanel); diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 967d4c6efee..5fea4b15aff 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -31,7 +31,7 @@ import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { throttle } from "lodash"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; -import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; +import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; // what-input helps improve keyboard accessibility import "what-input"; @@ -552,7 +552,10 @@ export default class MatrixChat extends React.PureComponent { .then((loadedSession) => { if (!loadedSession) { // fall back to showing the welcome screen... unless we have a 3pid invite pending - if (ThreepidInviteStore.instance.pickBestInvite()) { + if ( + ThreepidInviteStore.instance.pickBestInvite() && + SettingsStore.getValue(UIFeature.Registration) + ) { dis.dispatch({ action: "start_registration" }); } else { dis.dispatch({ action: "view_welcome_page" }); @@ -648,7 +651,7 @@ export default class MatrixChat extends React.PureComponent { case "logout": LegacyCallHandler.instance.hangupAllCalls(); Promise.all([ - ...[...CallStore.instance.activeCalls].map((call) => call.disconnect()), + ...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), cleanUpBroadcasts(this.stores), ]).finally(() => Lifecycle.logout(this.stores.oidcClientStore)); break; @@ -952,6 +955,11 @@ export default class MatrixChat extends React.PureComponent { } private async startRegistration(params: { [key: string]: string }): Promise { + if (!SettingsStore.getValue(UIFeature.Registration)) { + this.showScreen("welcome"); + return; + } + const newState: Partial = { view: Views.REGISTER, }; @@ -1545,7 +1553,7 @@ export default class MatrixChat extends React.PureComponent { if (Lifecycle.isLoggingOut()) return; // A modal might have been open when we were logged out by the server - Modal.closeCurrentModal("Session.logged_out"); + Modal.forceCloseAllModals(); if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) { logger.warn("Soft logout issued by server - avoiding data deletion"); @@ -1615,7 +1623,7 @@ export default class MatrixChat extends React.PureComponent { }); cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise => { let haveNewVersion: boolean | undefined; - let newVersionInfo: IKeyBackupInfo | null = null; + let newVersionInfo: KeyBackupInfo | null = null; // if key backup is still enabled, there must be a new backup in place if (cli.getKeyBackupEnabled()) { haveNewVersion = true; @@ -1767,11 +1775,6 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({ action: Action.ViewHomePage, }); - } else if (screen === "start") { - this.showScreen("home"); - dis.dispatch({ - action: "require_registration", - }); } else if (screen === "directory") { dis.fire(Action.ViewRoomDirectory); } else if (screen === "start_sso" || screen === "start_cas") { diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 0da27a19b10..dff2716c19b 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import { logger } from "matrix-js-sdk/src/logger"; +import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications"; import { _t } from "../../languageHandler"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -26,6 +27,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import Heading from "../views/typography/Heading"; +import EmptyState from "../views/right_panel/EmptyState"; interface IProps { onClose(): void; @@ -57,10 +59,11 @@ export default class NotificationPanel extends React.PureComponent -

{_t("notif_panel|empty_heading")}

-

{_t("notif_panel|empty_description")}

- + ); let content: JSX.Element; diff --git a/src/components/structures/PictureInPictureDragger.tsx b/src/components/structures/PictureInPictureDragger.tsx index 2456e633030..fc7e620288e 100644 --- a/src/components/structures/PictureInPictureDragger.tsx +++ b/src/components/structures/PictureInPictureDragger.tsx @@ -232,7 +232,7 @@ export default class PictureInPictureDragger extends React.Component { this.mouseHeld = false; // Delaying this to the next event loop tick is necessary for click // event cancellation to work - setImmediate(() => (this.moving = false)); + setTimeout(() => (this.moving = false)); this.snap(true); }; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 68d600ec1de..bc80692459a 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ChangeEvent } from "react"; import { Room, RoomState, RoomStateEvent, RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { throttle } from "lodash"; @@ -42,6 +42,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState"; import { Action } from "../../dispatcher/actions"; import { XOR } from "../../@types/common"; +import { RightPanelTabs } from "../views/right_panel/RightPanelTabs"; interface BaseProps { overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView) @@ -57,7 +58,8 @@ interface RoomlessProps extends BaseProps { interface RoomProps extends BaseProps { room: Room; permalinkCreator: RoomPermalinkCreator; - onSearchClick?: () => void; + onSearchChange?: (e: ChangeEvent) => void; + onSearchCancel?: () => void; } type Props = XOR; @@ -170,6 +172,7 @@ export default class RightPanel extends React.Component { { card = ( ); } @@ -311,6 +315,7 @@ export default class RightPanel extends React.Component { return ( ); diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 2fd883f04e0..5b8270e3f4c 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -24,12 +24,11 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import ScrollPanel from "./ScrollPanel"; -import { SearchScope } from "../views/rooms/SearchBar"; import Spinner from "../views/elements/Spinner"; import { _t } from "../../languageHandler"; import { haveRendererForEvent } from "../../events/EventTileFactory"; import SearchResultTile from "../views/rooms/SearchResultTile"; -import { searchPagination } from "../../Searching"; +import { searchPagination, SearchScope } from "../../Searching"; import Modal from "../../Modal"; import ErrorDialog from "../views/dialogs/ErrorDialog"; import ResizeNotifier from "../../utils/ResizeNotifier"; @@ -50,6 +49,7 @@ if (DEBUG) { interface Props { term: string; scope: SearchScope; + inProgress: boolean; promise: Promise; abortController?: AbortController; resizeNotifier: ResizeNotifier; @@ -60,10 +60,9 @@ interface Props { // XXX: todo: merge overlapping results somehow? // XXX: why doesn't searching on name work? export const RoomSearchView = forwardRef( - ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate }: Props, ref) => { + ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { const client = useContext(MatrixClientContext); const roomContext = useContext(RoomContext); - const [inProgress, setInProgress] = useState(true); const [highlights, setHighlights] = useState(null); const [results, setResults] = useState(null); const aborted = useRef(false); @@ -80,74 +79,72 @@ export const RoomSearchView = forwardRef( const handleSearchResult = useCallback( (searchPromise: Promise): Promise => { - setInProgress(true); - - return searchPromise - .then( - async (results): Promise => { - debuglog("search complete"); - if (aborted.current) { - logger.error("Discarding stale search results"); - return false; - } - - // postgres on synapse returns us precise details of the strings - // which actually got matched for highlighting. - // - // In either case, we want to highlight the literal search term - // whether it was used by the search engine or not. + onUpdate(true, null); - let highlights = results.highlights; - if (!highlights.includes(term)) { - highlights = highlights.concat(term); - } - - // For overlapping highlights, - // favour longer (more specific) terms first - highlights = highlights.sort(function (a, b) { - return b.length - a.length; - }); - - for (const result of results.results) { - for (const event of result.context.getTimeline()) { - const bundledRelationship = - event.getServerAggregatedRelation( - THREAD_RELATION_TYPE.name, - ); - if (!bundledRelationship || event.getThread()) continue; - const room = client.getRoom(event.getRoomId()); - const thread = room?.findThreadForEvent(event); - if (thread) { - event.setThread(thread); - } else { - room?.createThread(event.getId()!, event, [], true); - } - } - } - - setHighlights(highlights); - setResults({ ...results }); // copy to force a refresh + return searchPromise.then( + async (results): Promise => { + debuglog("search complete"); + if (aborted.current) { + logger.error("Discarding stale search results"); return false; - }, - (error) => { - if (aborted.current) { - logger.error("Discarding stale search results"); - return false; + } + + // postgres on synapse returns us precise details of the strings + // which actually got matched for highlighting. + // + // In either case, we want to highlight the literal search term + // whether it was used by the search engine or not. + + let highlights = results.highlights; + if (!highlights.includes(term)) { + highlights = highlights.concat(term); + } + + // For overlapping highlights, + // favour longer (more specific) terms first + highlights = highlights.sort(function (a, b) { + return b.length - a.length; + }); + + for (const result of results.results) { + for (const event of result.context.getTimeline()) { + const bundledRelationship = + event.getServerAggregatedRelation( + THREAD_RELATION_TYPE.name, + ); + if (!bundledRelationship || event.getThread()) continue; + const room = client.getRoom(event.getRoomId()); + const thread = room?.findThreadForEvent(event); + if (thread) { + event.setThread(thread); + } else { + room?.createThread(event.getId()!, event, [], true); + } } - logger.error("Search failed", error); - Modal.createDialog(ErrorDialog, { - title: _t("error_dialog|search_failed|title"), - // :TCHAP: error-tchap-is-down - description: error?.message ?? _t("error_dialog|search_failed|server_unavailable"), - description: error?.message ?? Tchapi18nUtils.getServerDownMessage(), - }); + } + + setHighlights(highlights); + setResults({ ...results }); // copy to force a refresh + onUpdate(false, results); + return false; + }, + (error) => { + if (aborted.current) { + logger.error("Discarding stale search results"); return false; - }, - ) - .finally(() => { - setInProgress(false); - }); + } + logger.error("Search failed", error); + Modal.createDialog(ErrorDialog, { + title: _t("error_dialog|search_failed|title"), + // :TCHAP: error-tchap-is-down - description: error?.message ?? _t("error_dialog|search_failed|server_unavailable"), + description: error?.message ?? Tchapi18nUtils.getServerDownMessage(), + }); + onUpdate(false, null); + return false; + }, + ); }, - [client, term], + [client, term, onUpdate], ); // Mount & unmount effect diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a15ddbf7742..a0555abbf78 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; +import React, { ChangeEvent, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; import classNames from "classnames"; import { IRecommendedVersion, @@ -41,7 +41,7 @@ import { import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; -import { throttle } from "lodash"; +import { debounce, throttle } from "lodash"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; @@ -70,10 +70,9 @@ import TimelinePanel from "./TimelinePanel"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import RoomPreviewCard from "../views/rooms/RoomPreviewCard"; -import SearchBar, { SearchScope } from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import AuxPanel from "../views/rooms/AuxPanel"; -import LegacyRoomHeader, { ISearchInfo } from "../views/rooms/LegacyRoomHeader"; +import LegacyRoomHeader from "../views/rooms/LegacyRoomHeader"; import RoomHeader from "../views/rooms/RoomHeader"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import EffectsOverlay from "../views/elements/EffectsOverlay"; @@ -121,7 +120,7 @@ import { SDKContext } from "../../contexts/SDKContext"; import { CallStore, CallStoreEvent } from "../../stores/CallStore"; import { Call } from "../../models/Call"; import { RoomSearchView } from "./RoomSearchView"; -import eventSearch from "../../Searching"; +import eventSearch, { SearchInfo, SearchScope } from "../../Searching"; import VoipUserMapper from "../../VoipUserMapper"; import { isCallEvent } from "./LegacyCallEventGrouper"; import { WidgetType } from "../../widgets/WidgetType"; @@ -133,6 +132,7 @@ import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoi import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload"; import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; +import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -190,7 +190,7 @@ export interface IRoomState { /** * The state of an ongoing search if there is one. */ - search?: ISearchInfo; + search?: SearchInfo; callState?: CallState; activeCall: Call | null; canPeek: boolean; @@ -400,6 +400,10 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement } export class RoomView extends React.Component { + // We cache the latest computed e2eStatus per room to show as soon as we switch rooms otherwise defaulting to + // unencrypted causes a flicker which can yield confusion/concern in a larger room. + private static e2eStatusCache = new Map(); + private readonly askToJoinEnabled: boolean; private readonly dispatcherRef: string; private settingWatchers: string[]; @@ -490,7 +494,7 @@ export class RoomView extends React.Component { WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate); - CallStore.instance.on(CallStoreEvent.ActiveCalls, this.onActiveCalls); + CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls); this.props.resizeNotifier.on("isResizing", this.onIsResizing); @@ -811,7 +815,7 @@ export class RoomView extends React.Component { } }; - private onActiveCalls = (): void => { + private onConnectedCalls = (): void => { if (this.state.roomId === undefined) return; const activeCall = CallStore.instance.getActiveCall(this.state.roomId); if (activeCall === null) { @@ -1052,7 +1056,7 @@ export class RoomView extends React.Component { ); } - CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls); + CallStore.instance.off(CallStoreEvent.ConnectedCalls, this.onConnectedCalls); this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState); // cancel any pending calls to the throttled updated @@ -1192,9 +1196,6 @@ export class RoomView extends React.Component { ); } break; - case "focus_search": - this.onSearchClick(); - break; case "local_room_event": this.onLocalRoomEvent(payload.roomId); @@ -1286,7 +1287,7 @@ export class RoomView extends React.Component { ]); } } else { - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); } break; case Action.View3pidInvite: @@ -1530,14 +1531,17 @@ export class RoomView extends React.Component { // If crypto is not currently enabled, we aren't tracking devices at all, // so we don't know what the answer is. Let's error on the safe side and show // a warning for this case. - let e2eStatus = E2EStatus.Warning; + let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning; + // set the state immediately then update, so we don't scare the user into thinking the room is unencrypted + this.setState({ e2eStatus }); + if (this.context.client.isCryptoEnabled()) { /* At this point, the user has encryption on and cross-signing on */ e2eStatus = await shieldStatusForRoom(this.context.client, room); + RoomView.e2eStatusCache.set(room.roomId, e2eStatus); + if (this.unmounted) return; + this.setState({ e2eStatus }); } - - if (this.unmounted) return; - this.setState({ e2eStatus }); } private onUrlPreviewsEnabledChange = (): void => { @@ -1718,13 +1722,14 @@ export class RoomView extends React.Component { }); } - private onSearch = (term: string, scope: SearchScope): void => { + private onSearch = (term: string, scope = SearchScope.Room): void => { const roomId = scope === SearchScope.Room ? this.getRoomId() : undefined; debuglog("sending search request"); const abortController = new AbortController(); const promise = eventSearch(this.context.client!, term, roomId, abortController.signal); this.setState({ + timelineRenderingType: TimelineRenderingType.Search, search: { // make sure that we don't end up showing results from // an aborted search by keeping a unique id. @@ -1738,6 +1743,10 @@ export class RoomView extends React.Component { }); }; + private onSearchScopeChange = (scope: SearchScope): void => { + this.onSearch(this.state.search?.term ?? "", scope); + }; + private onSearchUpdate = (inProgress: boolean, searchResults: ISearchResults | null): void => { this.setState({ search: { @@ -1832,15 +1841,14 @@ export class RoomView extends React.Component { }; private onSearchClick = (): void => { - if (this.state.timelineRenderingType === TimelineRenderingType.Search) { - this.onCancelSearchClick(); - } else { - this.setState({ - timelineRenderingType: TimelineRenderingType.Search, - }); - } + dis.fire(Action.FocusMessageSearch); }; + private onSearchChange = debounce((e: ChangeEvent): void => { + const term = (e.target as HTMLInputElement).value; + this.onSearch(term); + }, 300); + private onCancelSearchClick = (): Promise => { return new Promise((resolve) => { this.setState( @@ -2321,10 +2329,10 @@ export class RoomView extends React.Component { let previewBar; if (this.state.timelineRenderingType === TimelineRenderingType.Search) { aux = ( - ); @@ -2431,6 +2439,7 @@ export class RoomView extends React.Component { scope={this.state.search.scope} promise={this.state.search.promise} abortController={this.state.search.abortController} + inProgress={!!this.state.search.inProgress} resizeNotifier={this.props.resizeNotifier} className={this.messagePanelClassNames} onUpdate={this.onSearchUpdate} @@ -2500,7 +2509,8 @@ export class RoomView extends React.Component { resizeNotifier={this.props.resizeNotifier} permalinkCreator={this.permalinkCreator} e2eStatus={this.state.e2eStatus} - onSearchClick={this.onSearchClick} + onSearchChange={this.onSearchChange} + onSearchCancel={this.onCancelSearchClick} /> ) : undefined; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index c745d9cf5d9..ecbe7fa1813 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -24,6 +24,7 @@ import AutoHideScrollbar from "./AutoHideScrollbar"; import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers"; import { NonEmptyArray } from "../../@types/common"; import { RovingAccessibleButton, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; +import { useWindowWidth } from "../../hooks/useWindowWidth"; /** * Represents a tab for the TabbedView. @@ -87,10 +88,11 @@ function TabPanel({ tab }: ITabPanelProps): JSX.Element { interface ITabLabelProps { tab: Tab; isActive: boolean; + showToolip: boolean; onClick: () => void; } -function TabLabel({ tab, isActive, onClick }: ITabLabelProps): JSX.Element { +function TabLabel({ tab, isActive, showToolip, onClick }: ITabLabelProps): JSX.Element { const classes = classNames("mx_TabbedView_tabLabel", { mx_TabbedView_tabLabel_active: isActive, }); @@ -112,6 +114,7 @@ function TabLabel({ tab, isActive, onClick }: ITabLabelProps {tabIcon} @@ -152,12 +155,16 @@ export default function TabbedView(props: IProps): JSX.Elem return props.tabs.find((tab) => tab.id === id); }; + const windowWidth = useWindowWidth(); + const labels = props.tabs.map((tab) => ( props.onChange(tab.id)} + // This should be the same as the the CSS breakpoint at which the tab labels are hidden + showToolip={windowWidth < 1024 && tabLocation == TabLocation.LEFT} /> )); const tab = getTabById(props.activeTabId); diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index d1e83601747..8951cfcb910 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -19,6 +19,7 @@ import React, { useContext, useEffect, useRef, useState } from "react"; import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix"; import { IconButton, Tooltip } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; +import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads"; import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg"; import BaseCard from "../views/right_panel/BaseCard"; @@ -35,11 +36,8 @@ import Measured from "../views/elements/Measured"; import PosthogTrackers from "../../PosthogTrackers"; import { ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; -import Heading from "../views/typography/Heading"; import { clearRoomNotification } from "../../utils/notifications"; -import { useDispatcher } from "../../hooks/useDispatcher"; -import dis from "../../dispatcher/dispatcher"; -import { Action } from "../../dispatcher/actions"; +import EmptyState from "../views/right_panel/EmptyState"; interface IProps { roomId: string; @@ -76,8 +74,7 @@ export const ThreadPanelHeaderFilterOptionItem: React.FC< export const ThreadPanelHeader: React.FC<{ filterOption: ThreadFilterType; setFilterOption: (filterOption: ThreadFilterType) => void; - empty: boolean; -}> = ({ filterOption, setFilterOption, empty }) => { +}> = ({ filterOption, setFilterOption }) => { const mxClient = useMatrixClientContext(); const roomContext = useRoomContext(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -140,89 +137,24 @@ export const ThreadPanelHeader: React.FC<{ return (
- - {_t("common|threads")} - - {!empty && ( - <> - - - - - -
- { - openMenu(); - PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev); - }} - > - {`${_t("threads|show_thread_filter")} ${value?.label}`} - - {contextMenu} - - )} -
- ); -}; - -interface EmptyThreadIProps { - hasThreads: boolean; - filterOption: ThreadFilterType; - showAllThreadsCallback: () => void; -} - -const EmptyThread: React.FC = ({ hasThreads, filterOption, showAllThreadsCallback }) => { - let body: JSX.Element; - if (hasThreads) { - body = ( - <> -

- {_t("threads|empty_has_threads_tip", { - replyInThread: _t("action|reply_in_thread"), - })} -

-

- {/* Always display that paragraph to prevent layout shift when hiding the button */} - {filterOption === ThreadFilterType.My ? ( - - ) : ( - <>  - )} -

- - ); - } else { - body = ( - <> -

{_t("threads|empty_explainer")}

-

- {_t( - "threads|empty_tip", - { - replyInThread: _t("action|reply_in_thread"), - }, - { - b: (sub) => {sub}, - }, - )} -

- - ); - } - - return ( -
-
-

{_t("threads|empty_heading")}

- {body} + + + + + +
+ { + openMenu(); + PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev); + }} + > + {`${_t("threads|show_thread_filter")} ${value?.label}`} + + {contextMenu}
); }; @@ -232,7 +164,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => const roomContext = useContext(RoomContext); const timelinePanel = useRef(null); const card = useRef(null); - const closeButonRef = useRef(null); + const closeButonRef = useRef(null); const [filterOption, setFilterOption] = useState(ThreadFilterType.All); const [room, setRoom] = useState(null); @@ -259,14 +191,6 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => } }, [timelineSet, timelinePanel]); - useDispatcher(dis, (payload) => { - // This actually foucses the close button on the threads panel, as its the only interactive element, - // but at least it puts the user in the right area of the app. - if (payload.action === Action.FocusThreadsPanel) { - closeButonRef.current?.focus(); - } - }); - return ( = ({ roomId, onClose, permalinkCreator }) => }} > + hasThreads && } + id="thread-panel" className="mx_ThreadPanel" + ariaLabelledBy="thread-panel-tab" + role="tabpanel" onClose={onClose} withoutScrollContainer={true} ref={card} @@ -302,10 +226,12 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => timelineSet={timelineSet} showUrlPreview={false} // No URL previews at the threads list level empty={ - setFilterOption(ThreadFilterType.All)} + } alwaysShowTimestamps={true} diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 1b61abb3bb8..7bd176c6122 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -55,7 +55,6 @@ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resize { return currentFloor !== nextFloor; } - private calculateDuration(seconds: number): string { - return new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds) + private calculateDuration(seconds: number): string | undefined { + if (isNaN(seconds)) return undefined; + return new Temporal.Duration(0, 0, 0, 0, 0, 0, Math.round(seconds)) .round({ smallestUnit: "seconds", largestUnit: "hours" }) .toString(); } diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index c49fd2e74c7..2ffe621309e 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, ReactNode } from "react"; +import React, { ReactNode } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { Playback, PlaybackState } from "../../../audio/Playback"; -import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../elements/AccessibleButton"; -type Props = Omit, "title" | "onClick" | "disabled" | "element" | "ref"> & { +type Props = Omit, "title" | "onClick" | "disabled" | "element" | "ref"> & { // Playback instance to manipulate. Cannot change during the component lifecycle. playback: Playback; diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 4f97122370b..dc0d61ccfb5 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -59,7 +59,6 @@ interface IState { checkCode?: string; failureReason?: FailureReason; lastScannedCode?: Buffer; - homeserverBaseUrl?: string; } export enum LoginWithQRFailureReason { @@ -312,7 +311,6 @@ export default class LoginWithQR extends React.Component { failureReason: undefined, userCode: undefined, checkCode: undefined, - homeserverBaseUrl: undefined, lastScannedCode: undefined, mediaPermissionError: false, }); diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 036dc1b451f..c2f8d3c4065 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -22,7 +22,7 @@ import { } from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; import { Icon as CheckCircleSolidIcon } from "@vector-im/compound-design-tokens/icons/check-circle-solid.svg"; -import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; import { Heading, MFAInput, Text } from "@vector-im/compound-web"; import classNames from "classnames"; diff --git a/src/components/views/context_menus/KebabContextMenu.tsx b/src/components/views/context_menus/KebabContextMenu.tsx index 7a6b09668dd..f963933bdb6 100644 --- a/src/components/views/context_menus/KebabContextMenu.tsx +++ b/src/components/views/context_menus/KebabContextMenu.tsx @@ -18,7 +18,7 @@ import React from "react"; import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg"; import { ChevronFace, ContextMenuButton, MenuProps, useContextMenu } from "../../structures/ContextMenu"; -import AccessibleButton from "../elements/AccessibleButton"; +import { ButtonProps } from "../elements/AccessibleButton"; import IconizedContextMenu, { IconizedContextMenuOptionList } from "./IconizedContextMenu"; const contextMenuBelow = (elementRect: DOMRect): MenuProps => { @@ -29,10 +29,10 @@ const contextMenuBelow = (elementRect: DOMRect): MenuProps => { return { left, top, chevronFace }; }; -interface KebabContextMenuProps extends Partial> { +type KebabContextMenuProps = Partial> & { options: React.ReactNode[]; title: string; -} +}; export const KebabContextMenu: React.FC = ({ options, title, ...props }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 66f6c9e0953..653c6327fa1 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -128,6 +128,8 @@ export default class BaseDialog extends React.Component { onClick={this.onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("dialog_close_label")} + title={_t("action|close")} + placement="bottom" /> ); } @@ -184,8 +186,8 @@ export default class BaseDialog extends React.Component { )} {this.props.headerButton}
- {cancelButton} {this.props.children} + {cancelButton} ); diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx index 0064e2541ec..e387545b18c 100644 --- a/src/components/views/dialogs/BulkRedactDialog.tsx +++ b/src/components/views/dialogs/BulkRedactDialog.tsx @@ -126,7 +126,7 @@ const BulkRedactDialog: React.FC = (props) => { primaryButtonClass="danger" primaryDisabled={count === 0} onPrimaryButtonClick={() => { - setImmediate(redact); + setTimeout(redact, 0); onFinished(true); }} onCancel={() => onFinished(false)} diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index 1415f3befac..4e05c26b82b 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -23,7 +23,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import InteractiveAuth, { ERROR_USER_CANCELLED, InteractiveAuthCallback } from "../../structures/InteractiveAuth"; -import { ContinueKind, DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; +import { ContinueKind, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import StyledCheckbox from "../elements/StyledCheckbox"; import BaseDialog from "./BaseDialog"; import defaultDispatcher from "../../../dispatcher/dispatcher"; @@ -88,11 +88,6 @@ export default class DeactivateAccountDialog extends React.Component = ({ matrixClient: cli, event, permalinkCr placeholder={_t("forward|filter_placeholder")} onSearch={(query: string): void => { setQuery(query); - setImmediate(() => { + setTimeout(() => { const ref = context.state.refs[0]; if (ref) { context.dispatch({ diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 71587aee3d4..42b3148ef30 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -22,7 +22,6 @@ import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { uniqBy } from "lodash"; -import { Icon as InfoIcon } from "../../../../res/img/element-icons/info.svg"; import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg"; import { _t, _td } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -624,7 +623,7 @@ export default class InviteDialog extends React.PureComponent; const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); @@ -1395,21 +1393,6 @@ export default class InviteDialog extends React.PureComponent - - {" " + _t("invite|key_share_warning")} -

- ); - } - } } else if (this.props.kind === InviteKind.CallTransfer) { title = _t("action|transfer"); @@ -1475,7 +1458,6 @@ export default class InviteDialog extends React.PureComponent
- {keySharingWarning} {this.renderIdentityServerWarning()}
{this.state.errorText}
{onlyOneThreepidNote} diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 9aafeca2fd3..1ab39acb5ce 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -50,7 +50,7 @@ interface IProps { function titleForTabID(tabId: UserTab): React.ReactNode { const subs = { - strong: (sub: string) => {sub}, + strong: (sub: string) => {sub}, }; switch (tabId) { case UserTab.General: @@ -230,6 +230,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { hasCancel={true} onFinished={props.onFinished} title={titleForTabID(activeTabId)} + titleClass="mx_UserSettingsDialog_title" >
= ({ onBack }) => { const cli = useContext(MatrixClientContext); - const capabilities = useAsyncMemo(() => cli.getCapabilities(true).catch(() => FAILED_TO_LOAD), [cli]); + const capabilities = useAsyncMemo(() => cli.fetchCapabilities().catch(() => FAILED_TO_LOAD), [cli]); const clientVersions = useAsyncMemo(() => cli.getVersions().catch(() => FAILED_TO_LOAD), [cli]); const serverVersions = useAsyncMemo(async (): Promise => { try { diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index c535245e61d..c9aade7b9e3 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -17,8 +17,9 @@ limitations under the License. import React, { ChangeEvent } from "react"; import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix"; -import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup"; +import { IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup"; import { logger } from "matrix-js-sdk/src/logger"; +import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t } from "../../../../languageHandler"; @@ -51,7 +52,7 @@ interface IProps { } interface IState { - backupInfo: IKeyBackupInfo | null; + backupInfo: KeyBackupInfo | null; backupKeyStored: Record | null; loading: boolean; loadError: boolean | null; @@ -246,7 +247,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { + private async restoreWithCachedKey(backupInfo: KeyBackupInfo | null): Promise { if (!backupInfo) return false; try { const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithCache( diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 2ac7681afae..2630c85872e 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -505,7 +505,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n _setQuery(newQuery); }; useEffect(() => { - setImmediate(() => { + setTimeout(() => { const ref = rovingContext.state.refs[0]; if (ref) { rovingContext.dispatch({ diff --git a/src/components/views/dialogs/spotlight/TooltipOption.tsx b/src/components/views/dialogs/spotlight/TooltipOption.tsx index 0deb4b13111..24c97d601f9 100644 --- a/src/components/views/dialogs/spotlight/TooltipOption.tsx +++ b/src/components/views/dialogs/spotlight/TooltipOption.tsx @@ -15,18 +15,23 @@ limitations under the License. */ import classNames from "classnames"; -import React, { ComponentProps, ReactNode } from "react"; +import React, { ReactNode } from "react"; import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; -import AccessibleButton from "../../elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../../elements/AccessibleButton"; import { Ref } from "../../../../accessibility/roving/types"; -interface TooltipOptionProps extends ComponentProps { +type TooltipOptionProps = ButtonProps & { endAdornment?: ReactNode; inputRef?: Ref; -} +}; -export const TooltipOption: React.FC = ({ inputRef, className, ...props }) => { +export const TooltipOption = ({ + inputRef, + className, + element, + ...props +}: TooltipOptionProps): JSX.Element => { const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); return ( = ({ inputRef, classNam tabIndex={-1} aria-selected={isActive} role="option" + element={element as keyof JSX.IntrinsicElements} /> ); }; diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 76b90506dcc..c80f8a018d4 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -113,6 +113,8 @@ type Props = DynamicHtmlElementProps & disableTooltip?: TooltipProps["disabled"]; }; +export type ButtonProps = Props; + /** * Type of the props passed to the element that is rendered by AccessibleButton. */ diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index fd11d372dc3..4b8ccb0ed90 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -37,7 +37,7 @@ interface IProps { /** * classnames to apply to the wrapper of the preview */ - className: string; + className?: string; /** * The ID of the displayed user diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx index 67c11e2f357..d459040f0b1 100644 --- a/src/components/views/elements/FacePile.tsx +++ b/src/components/views/elements/FacePile.tsx @@ -21,7 +21,7 @@ import { AvatarStack, Tooltip } from "@vector-im/compound-web"; import MemberAvatar from "../avatars/MemberAvatar"; import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; -interface IProps extends HTMLAttributes { +interface IProps extends Omit, "onChange"> { members: RoomMember[]; size: string; overflow: boolean; @@ -32,6 +32,11 @@ interface IProps extends HTMLAttributes { onClick?: (e: ButtonEvent) => void | Promise; } +/** + * A component which displays a list of avatars in a row, with a tooltip showing the names of the users. + * + * Any additional props, not named explicitly here, are passed to the underlying {@link AccessibleButton}. + */ const FacePile: FC = ({ members, size, @@ -40,6 +45,7 @@ const FacePile: FC = ({ tooltipShortcut, children, viewUserOnClick = true, + onClick, ...props }) => { const faces = members.map( @@ -47,12 +53,7 @@ const FacePile: FC = ({ ? (m) => : (m) => ( - + ), ); @@ -65,7 +66,7 @@ const FacePile: FC = ({ ); const content = ( - + {pileContents} {children} diff --git a/src/components/views/elements/LearnMore.tsx b/src/components/views/elements/LearnMore.tsx index efce35bfe34..c7c7a1925ee 100644 --- a/src/components/views/elements/LearnMore.tsx +++ b/src/components/views/elements/LearnMore.tsx @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps } from "react"; +import React from "react"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import InfoDialog from "../dialogs/InfoDialog"; -import AccessibleButton from "./AccessibleButton"; +import AccessibleButton, { ButtonProps } from "./AccessibleButton"; -type Props = Omit, "kind" | "onClick" | "className"> & { +type Props = Omit, "element" | "kind" | "onClick" | "className"> & { title: string; description: string | React.ReactNode; }; diff --git a/src/components/views/elements/SearchWarning.tsx b/src/components/views/elements/SearchWarning.tsx index 2cdfe2f1f6e..5891b245f9d 100644 --- a/src/components/views/elements/SearchWarning.tsx +++ b/src/components/views/elements/SearchWarning.tsx @@ -33,9 +33,10 @@ export enum WarningKind { interface IProps { isRoomEncrypted?: boolean; kind: WarningKind; + showLogo?: boolean; } -export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.Element { +export default function SearchWarning({ isRoomEncrypted, kind, showLogo = true }: IProps): JSX.Element { if (!isRoomEncrypted) return <>; if (EventIndexPeg.get()) return <>; @@ -121,7 +122,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El return (
- {logo} + {showLogo ? logo : null} {text}
); diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index d44a3323b71..86b82c64e44 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -62,6 +62,13 @@ export default class SettingsFlag extends React.Component { } private getSettingValue(): boolean { + // If a level defined in props is overridden by a level at a high presedence, it gets disabled + // and we should show the overridding value. + if ( + SettingsStore.settingIsOveriddenAtConfigLevel(this.props.name, this.props.roomId ?? null, this.props.level) + ) { + return !!SettingsStore.getValue(this.props.name); + } return !!SettingsStore.getValueAt( this.props.level, this.props.name, diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index 1e94e533cd1..f0bce43b800 100644 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ b/src/components/views/messages/DecryptionFailureBody.tsx @@ -25,8 +25,10 @@ import ExternalLink from "../elements/ExternalLink"; // :TCHAP: better-text-for- import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string { - if (mxEvent.isEncryptedDisabledForUnverifiedDevices) return _t("timeline|decryption_failure|blocked"); switch (mxEvent.decryptionFailureReason) { + case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: + return _t("timeline|decryption_failure|blocked"); + case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP: return _t("timeline|decryption_failure|historical_event_no_key_backup"); diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index df475d7de6d..67ec020aa56 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -16,8 +16,8 @@ limitations under the License. import React, { forwardRef, useContext } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { IRoomEncryption } from "matrix-js-sdk/src/crypto/RoomList"; +import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EventTileBubble from "./EventTileBubble"; @@ -25,30 +25,29 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { objectHasDiff } from "../../../utils/objects"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; +import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../utils/crypto"; interface IProps { mxEvent: MatrixEvent; timestamp?: JSX.Element; } -const ALGORITHM = "m.megolm.v1.aes-sha2"; - const EncryptionEvent = forwardRef(({ mxEvent, timestamp }, ref) => { const cli = useContext(MatrixClientContext); const roomId = mxEvent.getRoomId()!; const isRoomEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId); - const prevContent = mxEvent.getPrevContent() as IRoomEncryption; - const content = mxEvent.getContent(); + const prevContent = mxEvent.getPrevContent() as RoomEncryptionEventContent; + const content = mxEvent.getContent(); // if no change happened then skip rendering this, a shallow check is enough as all known fields are top-level. if (!objectHasDiff(prevContent, content)) return null; // nop - if (content.algorithm === ALGORITHM && isRoomEncrypted) { + if (content.algorithm === MEGOLM_ENCRYPTION_ALGORITHM && isRoomEncrypted) { let subtitle: string; const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); const room = cli?.getRoom(roomId); - if (prevContent.algorithm === ALGORITHM) { + if (prevContent.algorithm === MEGOLM_ENCRYPTION_ALGORITHM) { subtitle = _t("timeline|m.room.encryption|parameters_changed"); } else if (dmPartner) { const displayName = room?.getMember(dmPartner)?.rawDisplayName || dmPartner; diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 12d4c804168..bf4922615f4 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -288,7 +288,7 @@ export default class MFileBody extends React.Component { src={url} onLoad={() => this.downloadFile(this.fileName, this.linkText)} ref={this.iframe} - sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" + sandbox="allow-scripts allow-downloads" />
)} diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 2afae0bc287..aadabeb646f 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -14,29 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef, ReactNode, KeyboardEvent, Ref } from "react"; +import React, { forwardRef, ReactNode, KeyboardEvent, Ref, MouseEvent } from "react"; import classNames from "classnames"; +import { IconButton, Text } from "@vector-im/compound-web"; +import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; +import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { _t } from "../../../languageHandler"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { backLabelForPhase } from "../../../stores/right-panel/RightPanelStorePhases"; import { CardContext } from "./context"; interface IProps { header?: ReactNode | null; + hideHeaderButtons?: boolean; footer?: ReactNode; className?: string; + id?: string; + role?: "tabpanel"; + ariaLabelledBy?: string; withoutScrollContainer?: boolean; closeLabel?: string; - onClose?(ev: ButtonEvent): void; - onBack?(ev: ButtonEvent): void; + onClose?(ev: MouseEvent): void; + onBack?(ev: MouseEvent): void; onKeyDown?(ev: KeyboardEvent): void; cardState?: any; ref?: Ref; // Ref for the 'close' button the the card - closeButtonRef?: Ref; + closeButtonRef?: Ref; children: ReactNode; } @@ -62,6 +68,10 @@ const BaseCard: React.FC = forwardRef( onClose, onBack, className, + id, + ariaLabelledBy, + role, + hideHeaderButtons, header, footer, withoutScrollContainer, @@ -73,26 +83,39 @@ const BaseCard: React.FC = forwardRef( ) => { let backButton; const cardHistory = RightPanelStore.instance.roomPhaseHistory; - if (cardHistory.length > 1) { + if (cardHistory.length > 1 && !hideHeaderButtons) { const prevCard = cardHistory[cardHistory.length - 2]; - const onBackClick = (ev: ButtonEvent): void => { + const onBackClick = (ev: MouseEvent): void => { onBack?.(ev); RightPanelStore.instance.popCard(); }; const label = backLabelForPhase(prevCard.phase) ?? _t("action|back"); - backButton = ; + backButton = ( + + + + ); } let closeButton; - if (onClose) { + if (onClose && !hideHeaderButtons) { closeButton = ( - + tooltip={closeLabel ?? _t("action|close")} + subtleBackground + > + + ); } @@ -100,14 +123,29 @@ const BaseCard: React.FC = forwardRef( children = {children}; } + const shouldRenderHeader = header || !hideHeaderButtons; + return ( -
- {header !== null && ( +
+ {shouldRenderHeader && (
{backButton} + {typeof header === "string" ? ( + + {header} + + ) : ( + header ??
+ )} {closeButton} -
{header}
)} {children} diff --git a/src/components/views/right_panel/EmptyState.tsx b/src/components/views/right_panel/EmptyState.tsx new file mode 100644 index 00000000000..7189cb8b3a7 --- /dev/null +++ b/src/components/views/right_panel/EmptyState.tsx @@ -0,0 +1,42 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { ComponentType } from "react"; +import { Text } from "@vector-im/compound-web"; + +import { Flex } from "../../utils/Flex"; + +interface Props { + Icon: ComponentType>; + title: string; + description: string; +} + +const EmptyState: React.FC = ({ Icon, title, description }) => { + return ( + + + + {title} + + + {description} + + + ); +}; + +export default EmptyState; diff --git a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx b/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx index 0846139f5a4..2263399635f 100644 --- a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx +++ b/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx @@ -215,27 +215,27 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons { const currentPhase = RightPanelStore.instance.currentCard.phase; if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) { if (this.state.phase === currentPhase) { - RightPanelStore.instance.showOrHidePanel(currentPhase); + RightPanelStore.instance.showOrHidePhase(currentPhase); } else { - RightPanelStore.instance.showOrHidePanel(currentPhase, RightPanelStore.instance.currentCard.state); + RightPanelStore.instance.showOrHidePhase(currentPhase, RightPanelStore.instance.currentCard.state); } } else { // This toggles for us, if needed - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary); } }; private onNotificationsClicked = (): void => { // This toggles for us, if needed - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel); }; private onPinnedMessagesClicked = (): void => { // This toggles for us, if needed - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.PinnedMessages); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages); }; private onTimelineCardClicked = (): void => { - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.Timeline); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.Timeline); }; private onThreadsPanelClicked = (ev: ButtonEvent): void => { diff --git a/src/components/views/right_panel/RightPanelTabs.tsx b/src/components/views/right_panel/RightPanelTabs.tsx new file mode 100644 index 00000000000..fc2eeb17fac --- /dev/null +++ b/src/components/views/right_panel/RightPanelTabs.tsx @@ -0,0 +1,86 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useRef } from "react"; +import { NavBar, NavItem } from "@vector-im/compound-web"; + +import { _t } from "../../../languageHandler"; +import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; +import PosthogTrackers from "../../../PosthogTrackers"; +import { useDispatcher } from "../../../hooks/useDispatcher"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; + +function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean { + const tabs = [RightPanelPhases.RoomSummary, RightPanelPhases.RoomMemberList, RightPanelPhases.ThreadPanel]; + return !!phase && tabs.includes(phase); +} + +type Props = { + phase: RightPanelPhases; +}; + +export const RightPanelTabs: React.FC = ({ phase }): JSX.Element | null => { + const threadsTabRef = useRef(null); + + useDispatcher(dispatcher, (payload) => { + // This actually focuses the threads tab, as its the only interactive element, + // but at least it puts the user in the right area of the app. + if (payload.action === Action.FocusThreadsPanel) { + threadsTabRef.current?.focus(); + } + }); + + if (!shouldShowTabsForPhase(phase)) return null; + + return ( + + { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomSummary }, true); + }} + active={phase === RightPanelPhases.RoomSummary} + > + {_t("right_panel|info")} + + ) => { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true); + PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev); + }} + active={phase === RightPanelPhases.RoomMemberList} + > + {_t("common|people")} + + { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true); + }} + active={phase === RightPanelPhases.ThreadPanel} + ref={threadsTabRef} + > + {_t("common|threads")} + + + ); +}; diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 54a6d8ecec8..86c163b9101 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -14,11 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { SyntheticEvent, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import React, { + ChangeEvent, + SyntheticEvent, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import classNames from "classnames"; import { MenuItem, - Tooltip, Separator, ToggleMenuItem, Text, @@ -26,22 +34,22 @@ import { Heading, IconButton, Link, + Search, + Form, } from "@vector-im/compound-web"; -import { Icon as SearchIcon } from "@vector-im/compound-design-tokens/icons/search.svg"; -import { Icon as FavouriteIcon } from "@vector-im/compound-design-tokens/icons/favourite.svg"; +import FavouriteIcon from "@vector-im/compound-design-tokens/assets/web/icons/favourite"; import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg"; -import { Icon as UserProfileSolidIcon } from "@vector-im/compound-design-tokens/icons/user-profile-solid.svg"; -import { Icon as LinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg"; -import { Icon as SettingsIcon } from "@vector-im/compound-design-tokens/icons/settings.svg"; +import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link"; +import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings"; import { Icon as ExportArchiveIcon } from "@vector-im/compound-design-tokens/icons/export-archive.svg"; -import { Icon as LeaveIcon } from "@vector-im/compound-design-tokens/icons/leave.svg"; -import { Icon as FilesIcon } from "@vector-im/compound-design-tokens/icons/files.svg"; -import { Icon as PollsIcon } from "@vector-im/compound-design-tokens/icons/polls.svg"; -import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin.svg"; +import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave"; +import FilesIcon from "@vector-im/compound-design-tokens/assets/web/icons/files"; +import PollsIcon from "@vector-im/compound-design-tokens/assets/web/icons/polls"; +import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin"; import { Icon as LockIcon } from "@vector-im/compound-design-tokens/icons/lock-solid.svg"; import { Icon as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg"; -import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg"; -import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; +import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; import { Icon as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg"; import { EventType, JoinRule, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; @@ -63,7 +71,7 @@ import WidgetAvatar from "../avatars/WidgetAvatar"; import WidgetStore, { IApp } from "../../../stores/WidgetStore"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -import RoomContext from "../../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { UIComponent, UIFeature } from "../../../settings/UIFeature"; import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; @@ -89,6 +97,10 @@ import { useTopic } from "../../../hooks/room/useTopic"; import { Linkify, topicToHtml } from "../../../HtmlUtils"; import { Box } from "../../utils/Box"; import { onRoomTopicLinkClick } from "../elements/RoomTopic"; +import { useDispatcher } from "../../../hooks/useDispatcher"; +import { Action } from "../../../dispatcher/actions"; +import { Key } from "../../../Keyboard"; +import { useTransition } from "../../../hooks/useTransition"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; // :TCHAP: tchap-room-icons @@ -96,8 +108,9 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; // :TCHAP: tch interface IProps { room: Room; permalinkCreator: RoomPermalinkCreator; - onClose(): void; - onSearchClick?: () => void; + onSearchChange?: (e: ChangeEvent) => void; + onSearchCancel?: () => void; + focusRoomSearch?: boolean; } interface IAppsSectionProps { @@ -367,7 +380,13 @@ const RoomTopic: React.FC> = ({ room }): JSX.Element | null ); }; -const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose, onSearchClick }) => { +const RoomSummaryCard: React.FC = ({ + room, + permalinkCreator, + onSearchChange, + onSearchCancel, + focusRoomSearch, +}) => { const cli = useContext(MatrixClientContext); const onShareRoomClick = (): void => { @@ -397,11 +416,6 @@ const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose, on }); }; - const onRoomMembersClick = (ev: Event): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true); - PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev); - }; - const isRoomEncrypted = useIsEncrypted(cli, room); const roomContext = useContext(RoomContext); const e2eStatus = roomContext.e2eStatus; @@ -422,6 +436,26 @@ const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose, on } }, [room, directRoomsList]); + const searchInputRef = useRef(null); + useDispatcher(defaultDispatcher, (payload) => { + if (payload.action === Action.FocusMessageSearch) { + searchInputRef.current?.focus(); + } + }); + // Clear the search field when the user leaves the search view + useTransition( + (prevTimelineRenderingType) => { + if ( + prevTimelineRenderingType === TimelineRenderingType.Search && + roomContext.timelineRenderingType !== TimelineRenderingType.Search && + searchInputRef.current + ) { + searchInputRef.current.value = ""; + } + }, + [roomContext.timelineRenderingType], + ); + const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || ""; const header = (
@@ -499,7 +533,13 @@ const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose, on const isFavorite = roomTags.includes(DefaultTagID.Favourite); return ( - + = ({ room, permalinkCreator, onClose, on align="center" justify="space-between" > - - - - + {onSearchChange && ( + e.preventDefault()}> + { + if (searchInputRef.current && e.key === Key.ESCAPE) { + searchInputRef.current.value = ""; + onSearchCancel?.(); + } + }} + /> + + )} {header} - tagRoom(room, DefaultTagID.Favourite)} - // XXX: https://github.com/element-hq/compound/issues/288 - onSelect={() => {}} - /> - inviteToRoom(room)} - /> - - +
+ tagRoom(room, DefaultTagID.Favourite)} + // XXX: https://github.com/element-hq/compound/issues/288 + onSelect={() => {}} + /> + inviteToRoom(room)} + /> + + - - - {!isVideoRoom && ( - <> - - - {pinningEnabled && ( + + {!isVideoRoom && ( + <> + - - {pinCount} - - - )} - - - )} + Icon={PollsIcon} + label={_t("right_panel|polls_button")} + onSelect={onRoomPollHistoryClick} + /> + {pinningEnabled && ( + + + {pinCount} + + + )} + + + )} - + - + +
{SettingsStore.getValue(UIFeature.Widgets) && !isVideoRoom && diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d9839252f90..6f8fd9790b0 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -34,7 +34,18 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import { UserVerificationStatus, VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; -import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { Heading, MenuItem, Text } from "@vector-im/compound-web"; +import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat"; +import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; +import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share"; +import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/mention"; +import { Icon as InviteIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg"; +import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block"; +import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete"; +import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; +import { Icon as ChatProblemIcon } from "@vector-im/compound-design-tokens/icons/chat-problem.svg"; +import { Icon as VisibilityOffIcon } from "@vector-im/compound-design-tokens/icons/visibility-off.svg"; +import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave"; import dis from "../../../dispatcher/dispatcher"; import Modal from "../../../Modal"; @@ -80,8 +91,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages"; import { SdkContextClass } from "../../../contexts/SDKContext"; import { asyncSome } from "../../../utils/arrays"; -import UIStore from "../../../stores/UIStore"; -import { SpaceScopeHeader } from "../rooms/SpaceScopeHeader"; +import { Flex } from "../../utils/Flex"; +import CopyableText from "../elements/CopyableText"; export interface IDevice extends Device { ambiguous?: boolean; @@ -393,31 +404,29 @@ const MessageButton = ({ member }: { member: Member }): JSX.Element => { const [busy, setBusy] = useState(false); return ( - { + { + ev.preventDefault(); if (busy) return; setBusy(true); await openDmForUser(cli, member); setBusy(false); }} - className="mx_UserInfo_field" disabled={busy} - > - {_t("common|message")} - + label={_t("user_info|send_message")} + Icon={ChatIcon} + /> ); }; export const UserOptionsSection: React.FC<{ member: Member; - isIgnored: boolean; canInvite: boolean; isSpace?: boolean; -}> = ({ member, isIgnored, canInvite, isSpace }) => { +}> = ({ member, canInvite, isSpace, children }) => { const cli = useContext(MatrixClientContext); - let ignoreButton: JSX.Element | undefined; let insertPillButton: JSX.Element | undefined; let inviteUserButton: JSX.Element | undefined; let readReceiptButton: JSX.Element | undefined; @@ -429,42 +438,9 @@ export const UserOptionsSection: React.FC<{ }); }; - const unignore = useCallback(() => { - const ignoredUsers = cli.getIgnoredUsers(); - const index = ignoredUsers.indexOf(member.userId); - if (index !== -1) ignoredUsers.splice(index, 1); - cli.setIgnoredUsers(ignoredUsers); - }, [cli, member]); - - const ignore = useCallback(async () => { - const name = (member instanceof User ? member.displayName : member.name) || member.userId; - const { finished } = Modal.createDialog(QuestionDialog, { - title: _t("user_info|ignore_confirm_title", { user: name }), - description:
{_t("user_info|ignore_confirm_description")}
, - button: _t("action|ignore"), - }); - const [confirmed] = await finished; - - if (confirmed) { - const ignoredUsers = cli.getIgnoredUsers(); - ignoredUsers.push(member.userId); - cli.setIgnoredUsers(ignoredUsers); - } - }, [cli, member]); - // Only allow the user to ignore the user if its not ourselves // same goes for jumping to read receipt if (!isMe) { - ignoreButton = ( - - {isIgnored ? _t("action|unignore") : _t("action|ignore")} - - ); - if (member instanceof RoomMember && member.roomId && !isSpace) { const onReadReceiptButton = function (): void { const room = cli.getRoom(member.roomId); @@ -489,16 +465,28 @@ export const UserOptionsSection: React.FC<{ const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : undefined; if (room?.getEventReadUpTo(member.userId)) { readReceiptButton = ( - - {_t("user_info|jump_to_rr_button")} - + { + ev.preventDefault(); + onReadReceiptButton(); + }} + label={_t("user_info|jump_to_rr_button")} + Icon={CheckIcon} + /> ); } insertPillButton = ( - - {_t("action|mention")} - + { + ev.preventDefault(); + onInsertPillButton(); + }} + label={_t("action|mention")} + Icon={MentionIcon} + /> ); } @@ -509,7 +497,7 @@ export const UserOptionsSection: React.FC<{ shouldShowComponent(UIComponent.InviteUsers) ) { const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId(); - const onInviteUserButton = async (ev: ButtonEvent): Promise => { + const onInviteUserButton = async (ev: Event): Promise => { try { // We use a MultiInviter to re-use the invite logic, even though we're only inviting one user. const inviter = new MultiInviter(cli, roomId || ""); @@ -540,34 +528,43 @@ export const UserOptionsSection: React.FC<{ }; inviteUserButton = ( - - {_t("action|invite")} - + { + ev.preventDefault(); + onInviteUserButton(ev); + }} + label={_t("action|invite")} + Icon={InviteIcon} + /> ); } } const shareUserButton = ( - - {_t("user_info|share_button")} - + { + ev.preventDefault(); + onShareUserClick(); + }} + label={_t("user_info|share_button")} + Icon={ShareIcon} + /> ); const directMessageButton = isMe || !shouldShowComponent(UIComponent.CreateRooms) ? null : ; return ( -
-

{_t("common|options")}

-
- {directMessageButton} - {readReceiptButton} - {shareUserButton} - {insertPillButton} - {inviteUserButton} - {ignoreButton} -
-
+ + {children} + {directMessageButton} + {inviteUserButton} + {readReceiptButton} + {shareUserButton} + {insertPillButton} + ); }; @@ -588,15 +585,10 @@ export const warnSelfDemote = async (isSpace: boolean): Promise => { return !!confirmed; }; -const GenericAdminToolsContainer: React.FC<{ +const Container: React.FC<{ children: ReactNode; }> = ({ children }) => { - return ( -
-

{_t("user_info|admin_tools_section")}

-
{children}
-
- ); + return
{children}
; }; interface IPowerLevelsContent { @@ -758,14 +750,17 @@ export const RoomKickButton = ({ : _t("user_info|kick_button_room"); return ( - { + ev.preventDefault(); + onKick(); + }} disabled={isUpdating} - > - {kickLabel} - + label={kickLabel} + kind="critical" + Icon={LeaveIcon} + /> ); }; @@ -784,13 +779,16 @@ const RedactMessagesButton: React.FC = ({ member }) => { }; return ( - - {_t("user_info|redact_button")} - + { + ev.preventDefault(); + onRedactAllMessages(); + }} + label={_t("user_info|redact_button")} + kind="critical" + Icon={CloseIcon} + /> ); }; @@ -906,14 +904,18 @@ export const BanToggleButton = ({ label = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room"); } - const classes = classNames("mx_UserInfo_field", { - mx_UserInfo_destructive: !isBanned, - }); - return ( - - {label} - + { + ev.preventDefault(); + onBanOrUnban(); + }} + disabled={isUpdating} + label={label} + kind="critical" + Icon={ChatProblemIcon} + /> ); }; @@ -983,15 +985,81 @@ const MuteToggleButton: React.FC = ({ }); }; - const classes = classNames("mx_UserInfo_field", { - mx_UserInfo_destructive: !muted, - }); - const muteLabel = muted ? _t("common|unmute") : _t("common|mute"); return ( - - {muteLabel} - + { + ev.preventDefault(); + onMuteToggle(); + }} + disabled={isUpdating} + label={muteLabel} + kind="critical" + Icon={VisibilityOffIcon} + /> + ); +}; + +const IgnoreToggleButton: React.FC<{ + member: User | RoomMember; +}> = ({ member }) => { + const cli = useContext(MatrixClientContext); + const unignore = useCallback(() => { + const ignoredUsers = cli.getIgnoredUsers(); + const index = ignoredUsers.indexOf(member.userId); + if (index !== -1) ignoredUsers.splice(index, 1); + cli.setIgnoredUsers(ignoredUsers); + }, [cli, member]); + + const ignore = useCallback(async () => { + const name = (member instanceof User ? member.displayName : member.name) || member.userId; + const { finished } = Modal.createDialog(QuestionDialog, { + title: _t("user_info|ignore_confirm_title", { user: name }), + description:
{_t("user_info|ignore_confirm_description")}
, + button: _t("action|ignore"), + }); + const [confirmed] = await finished; + + if (confirmed) { + const ignoredUsers = cli.getIgnoredUsers(); + ignoredUsers.push(member.userId); + cli.setIgnoredUsers(ignoredUsers); + } + }, [cli, member]); + + // Check whether the user is ignored + const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId)); + // Recheck if the user or client changes + useEffect(() => { + setIsIgnored(cli.isUserIgnored(member.userId)); + }, [cli, member.userId]); + // Recheck also if we receive new accountData m.ignored_user_list + const accountDataHandler = useCallback( + (ev) => { + if (ev.getType() === "m.ignored_user_list") { + setIsIgnored(cli.isUserIgnored(member.userId)); + } + }, + [cli, member.userId], + ); + useTypedEventEmitter(cli, ClientEvent.AccountData, accountDataHandler); + + return ( + { + ev.preventDefault(); + if (isIgnored) { + unignore(); + } else { + ignore(); + } + }} + label={isIgnored ? _t("user_info|unignore_button") : _t("user_info|ignore_button")} + kind="critical" + Icon={BlockIcon} + /> ); }; @@ -1072,13 +1140,13 @@ export const RoomAdminToolsContainer: React.FC = ({ if (kickButton || banButton || muteButton || redactButton || children) { return ( - + {muteButton} + {redactButton} {kickButton} {banButton} - {redactButton} {children} - + ); } @@ -1323,7 +1391,7 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => { if (_userId !== userId) return; updateDevices(); }; - const onUserTrustStatusChanged = (_userId: string, trustLevel: UserTrustLevel): void => { + const onUserTrustStatusChanged = (_userId: string, trustLevel: UserVerificationStatus): void => { if (_userId !== userId) return; updateDevices(); }; @@ -1354,23 +1422,6 @@ const BasicUserInfo: React.FC<{ // Load whether or not we are a Synapse Admin const isSynapseAdmin = useIsSynapseAdmin(cli); - // Check whether the user is ignored - const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId)); - // Recheck if the user or client changes - useEffect(() => { - setIsIgnored(cli.isUserIgnored(member.userId)); - }, [cli, member.userId]); - // Recheck also if we receive new accountData m.ignored_user_list - const accountDataHandler = useCallback( - (ev) => { - if (ev.getType() === "m.ignored_user_list") { - setIsIgnored(cli.isUserIgnored(member.userId)); - } - }, - [cli, member.userId], - ); - useTypedEventEmitter(cli, ClientEvent.AccountData, accountDataHandler); - // Count of how many operations are currently in progress, if > 0 then show a Spinner const [pendingUpdateCount, setPendingUpdateCount] = useState(0); const startUpdating = useCallback(() => { @@ -1414,13 +1465,16 @@ const BasicUserInfo: React.FC<{ // someone does figure out how to bypass this check the worst that happens is an error. if (isSynapseAdmin && member.userId.endsWith(`:${cli.getDomain()}`)) { synapseDeactivateButton = ( - - {_t("user_info|deactivate_confirm_action")} - + { + ev.preventDefault(); + onSynapseDeactivate(); + }} + label={_t("user_info|deactivate_confirm_action")} + kind="critical" + Icon={DeleteIcon} + /> ); } @@ -1430,23 +1484,12 @@ const BasicUserInfo: React.FC<{ // hide the Roles section for DMs as it doesn't make sense there if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) { memberDetails = ( -
-

- {_t( - "user_info|role_label", - {}, - { - RoomName: () => {room.name}, - }, - )} -

- -
+ ); } @@ -1463,7 +1506,7 @@ const BasicUserInfo: React.FC<{ ); } else if (synapseDeactivateButton) { - adminToolsContainer = {synapseDeactivateButton}; + adminToolsContainer = {synapseDeactivateButton}; } if (pendingUpdateCount > 0) { @@ -1561,8 +1604,8 @@ const BasicUserInfo: React.FC<{ } const securitySection = ( -
-

{_t("common|security")}

+ +

{_t("common|security")}

{text}

{verifyButton} {cryptoEnabled && ( @@ -1574,23 +1617,29 @@ const BasicUserInfo: React.FC<{ /> )} {editDevices} -
+ ); return ( - {memberDetails} - {securitySection} + + > + {memberDetails} + {adminToolsContainer} + {!isMe && ( + + + + )} + {spinner} ); @@ -1623,24 +1672,6 @@ export const UserInfoHeader: React.FC<{ const avatarUrl = (member as User).avatarUrl; - const avatarElement = ( -
-
-
- -
-
-
- ); - let presenceState: string | undefined; let presenceLastActiveAgo: number | undefined; let presenceCurrentlyActive: boolean | undefined; @@ -1663,36 +1694,52 @@ export const UserInfoHeader: React.FC<{ activeAgo={presenceLastActiveAgo} currentlyActive={presenceCurrentlyActive} presenceState={presenceState} + className="mx_UserInfo_profileStatus" + coloured /> ); } const e2eIcon = e2eStatus ? : null; - + const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, { + roomId, + withDisplayName: true, + }); const displayName = (member as RoomMember).rawDisplayName; return ( - {avatarElement} - -
-
-
-

- - {displayName} - - {e2eIcon} -

+
+
+
+
-
- {UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, { - roomId, - withDisplayName: true, - })} -
-
{presenceLabel}
+ + + + + + {displayName} + {e2eIcon} + + + {presenceLabel} + + userIdentifier} border={false}> + {userIdentifier} + + + + ); }; @@ -1775,10 +1822,11 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha ); + return ( : undefined} + header={_t("common|profile")} onClose={onClose} closeLabel={closeLabel} cardState={cardState} diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 061dfe27039..38576dc2550 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -51,6 +51,7 @@ import { _t } from "../../../languageHandler"; import { linkify } from "../../../linkify-matrix"; import { SdkContextClass } from "../../../contexts/SDKContext"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation"; // matches emoticons which follow the start of a line or whitespace const REGEX_EMOTICON_WHITESPACE = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")\\s|:^$"); @@ -536,6 +537,16 @@ export default class BasicMessageEditor extends React.Component } } + const navAction = getKeyBindingsManager().getNavigationAction(event); + + if (navAction === KeyBindingAction.NextLandmark || navAction === KeyBindingAction.PreviousLandmark) { + LandmarkNavigation.findAndFocusNextLandmark( + Landmark.MESSAGE_COMPOSER_OR_HOME, + navAction === KeyBindingAction.PreviousLandmark, + ); + handled = true; + } + const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(event); if (model.autoComplete?.hasCompletions()) { diff --git a/src/components/views/rooms/CollapsibleButton.tsx b/src/components/views/rooms/CollapsibleButton.tsx index 157e7f7a1a9..d2d5de5eaa8 100644 --- a/src/components/views/rooms/CollapsibleButton.tsx +++ b/src/components/views/rooms/CollapsibleButton.tsx @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, useContext } from "react"; +import React, { useContext } from "react"; import classNames from "classnames"; -import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../elements/AccessibleButton"; import { OverflowMenuContext } from "./MessageComposerButtons"; import { IconizedContextMenuOption } from "../context_menus/IconizedContextMenu"; import { Ref } from "../../../accessibility/roving/types"; -interface Props extends Omit, "element"> { +interface Props extends Omit, "element"> { inputRef?: Ref; title: string; iconClassName: string; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 5682cce846e..669e57a06a4 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -35,8 +35,7 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; -import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; -import { EventShieldColour, EventShieldReason } from "matrix-js-sdk/src/crypto-api"; +import { EventShieldColour, EventShieldReason, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { Tooltip } from "@vector-im/compound-web"; import ReplyChain from "../elements/ReplyChain"; @@ -577,7 +576,7 @@ export class UnwrappedEventTile extends React.Component } }; - private onUserVerificationChanged = (userId: string, _trustStatus: UserTrustLevel): void => { + private onUserVerificationChanged = (userId: string, _trustStatus: UserVerificationStatus): void => { if (userId === this.props.mxEvent.getSender()) { this.verifyEvent(); } diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx index 82080fbd07a..c6fa28fc7ca 100644 --- a/src/components/views/rooms/LegacyRoomHeader.tsx +++ b/src/components/views/rooms/LegacyRoomHeader.tsx @@ -18,7 +18,7 @@ limitations under the License. import React, { FC, useState, useMemo, useCallback } from "react"; import classNames from "classnames"; import { throttle } from "lodash"; -import { RoomStateEvent, ISearchResults } from "matrix-js-sdk/src/matrix"; +import { RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { IconButton, Tooltip } from "@vector-im/compound-web"; import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; @@ -38,7 +38,6 @@ import RoomName from "../elements/RoomName"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; import { RoomKnocksBar } from "./RoomKnocksBar"; -import { SearchScope } from "./SearchBar"; import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import RoomContextMenu from "../context_menus/RoomContextMenu"; import { contextMenuBelow } from "./RoomTile"; @@ -70,6 +69,7 @@ import { SessionDuration } from "../voip/CallDuration"; import RoomCallBanner from "../beacon/RoomCallBanner"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; +import { SearchInfo } from "../../../Searching"; class DisabledWithReason { public constructor(public readonly reason: string) {} @@ -456,18 +456,6 @@ const CallLayoutSelector: FC = ({ call }) => { ); }; -export interface ISearchInfo { - searchId: number; - roomId?: string; - term: string; - scope: SearchScope; - promise: Promise; - abortController?: AbortController; - - inProgress?: boolean; - count?: number; -} - export interface IProps { room: Room; oobData?: IOOBData; @@ -478,7 +466,7 @@ export interface IProps { onAppsClick: (() => void) | null; e2eStatus: E2EStatus; appsShown: boolean; - searchInfo?: ISearchInfo; + searchInfo?: SearchInfo; excludedRightPanelPhaseButtons?: Array; showButtons?: boolean; enableRoomOptionsMenu?: boolean; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 5126425ac5c..85e1f0a9880 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -65,6 +65,7 @@ const SHOW_MORE_INCREMENT = 100; interface IProps { roomId: string; searchQuery: string; + hideHeaderButtons?: boolean; onClose(): void; onSearchQueryChanged: (query: string) => void; } @@ -359,7 +360,14 @@ export default class MemberList extends React.Component { public render(): React.ReactNode { if (this.state.loading) { return ( - + ); @@ -416,12 +424,13 @@ export default class MemberList extends React.Component { /> ); - const scopeHeader = room ? : undefined; - return ( {scopeHeader}} + ariaLabelledBy="memberlist-panel-tab" + role="tabpanel" + hideHeaderButtons={this.props.hideHeaderButtons} footer={footer} onClose={this.props.onClose} > diff --git a/src/components/views/rooms/MemberTile.tsx b/src/components/views/rooms/MemberTile.tsx index 03aa1862818..92c26ddd19d 100644 --- a/src/components/views/rooms/MemberTile.tsx +++ b/src/components/views/rooms/MemberTile.tsx @@ -19,7 +19,7 @@ import React from "react"; import { RoomMember, RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; -import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import dis from "../../../dispatcher/dispatcher"; import { _t } from "../../../languageHandler"; @@ -103,7 +103,7 @@ export default class MemberTile extends React.Component { this.updateE2EStatus(); }; - private onUserTrustStatusChanged = (userId: string, trustStatus: UserTrustLevel): void => { + private onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => { if (userId !== this.props.member.userId) return; this.updateE2EStatus(); }; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 70ea6c23a2a..03ea25664db 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -126,7 +126,7 @@ const NewRoomIntro: React.FC = () => { true, ); // focus the topic field to help the user find it as it'll gain an outline - setImmediate(() => { + setTimeout(() => { window.document.getElementById("profileTopic")?.focus(); }); }; diff --git a/src/components/views/rooms/PresenceLabel.tsx b/src/components/views/rooms/PresenceLabel.tsx index 24e144c8ef0..bdbc7e23e2a 100644 --- a/src/components/views/rooms/PresenceLabel.tsx +++ b/src/components/views/rooms/PresenceLabel.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; +import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { formatDuration } from "../../../DateUtils"; @@ -31,6 +32,9 @@ interface IProps { currentlyActive?: boolean; // offline, online, etc presenceState?: string; + // whether to apply colouring to the label + coloured?: boolean; + className?: string; } export default class PresenceLabel extends React.Component { @@ -62,7 +66,11 @@ export default class PresenceLabel extends React.Component { public render(): React.ReactNode { return ( -
+
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
); diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 950c22e9edb..7c7c8001193 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -18,12 +18,13 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg"; -import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg"; +import CloseCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg"; +import { Icon as RoomInfoIcon } from "@vector-im/compound-design-tokens/icons/info-solid.svg"; import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg"; -import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg"; -import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; -import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg"; +import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; @@ -55,6 +56,10 @@ import { RoomKnocksBar } from "./RoomKnocksBar"; import { isVideoRoom } from "../../../utils/video-rooms"; import { notificationLevelToIndicator } from "../../../utils/notifications"; import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement"; +import { useIsReleaseAnnouncementOpen } from "../../../hooks/useIsReleaseAnnouncementOpen"; +import { ReleaseAnnouncementStore } from "../../../stores/ReleaseAnnouncementStore"; import TchapUIFeature from "../../../../../../src/tchap/util/TchapUIFeature"; // :TCHAP: customize-room-header-bar import TchapExternalRoomHeader from "../../../../../../src/tchap/components/views/rooms/TchapExternalRoomHeader"; // :TCHAP: customize-room-header-bar @@ -86,6 +91,8 @@ export default function RoomHeader({ isConnectedToCall, hasActiveCallSession, callOptions, + showVoiceCallButton, + showVideoCallButton, } = useRoomCall(room); const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); @@ -203,7 +210,7 @@ export default function RoomHeader({ )} ); - const voiceCallButton = ( + let voiceCallButton: JSX.Element | undefined = ( ); - let videoCallButton = startVideoCallButton; + let videoCallButton: JSX.Element | undefined = startVideoCallButton; if (isConnectedToCall) { videoCallButton = toggleCallButton; } else if (isViewingCall) { videoCallButton = closeLobbyButton; } + if (!showVideoCallButton) { + videoCallButton = undefined; + } + if (!showVoiceCallButton) { + voiceCallButton = undefined; + } + + const isReleaseAnnouncementOpen = useIsReleaseAnnouncementOpen("newRoomHeader"); + return ( <> - + + {roomTopicBody} + + )} + + + {additionalButtons?.map((props) => { const label = props.label(); @@ -354,7 +381,20 @@ export default function RoomHeader({ )} + + { + evt.stopPropagation(); + RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary); + }} + aria-label={_t("right_panel|room_summary_card|title")} + > + + + + {/* :TCHAP: extend-remove-thread-buttons + { @@ -401,16 +441,7 @@ export default function RoomHeader({ )} {!isDirectMessage && ( - { - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList); - e.stopPropagation(); - }} - > + { + RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList); + e.stopPropagation(); + }} + aria-label={_t("common|n_members", { count: memberCount })} > {formatCount(memberCount)} diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index fcc269bede6..32f8ed7e5dc 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { Icon as ExternalLinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg"; +import ExternalLinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link"; import { Button, IconButton, Tooltip } from "@vector-im/compound-web"; import React, { useCallback } from "react"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index f6cd324332c..8a0de4d7b2d 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -60,7 +60,10 @@ import IconizedContextMenu, { import ExtraTile from "./ExtraTile"; import RoomSublist, { IAuxButtonProps } from "./RoomSublist"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; +import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import AccessibleButton from "../elements/AccessibleButton"; +import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation"; interface IProps { onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void; @@ -652,7 +655,22 @@ export default class RoomList extends React.PureComponent {
{ + const navAction = getKeyBindingsManager().getNavigationAction(ev); + if ( + navAction === KeyBindingAction.NextLandmark || + navAction === KeyBindingAction.PreviousLandmark + ) { + LandmarkNavigation.findAndFocusNextLandmark( + Landmark.ROOM_LIST, + navAction === KeyBindingAction.PreviousLandmark, + ); + ev.stopPropagation(); + ev.preventDefault(); + return; + } + onKeyDownHandler(ev); + }} className="mx_RoomList" role="tree" aria-label={_t("common|rooms")} diff --git a/src/components/views/rooms/RoomSearchAuxPanel.tsx b/src/components/views/rooms/RoomSearchAuxPanel.tsx new file mode 100644 index 00000000000..0882c4a8bf0 --- /dev/null +++ b/src/components/views/rooms/RoomSearchAuxPanel.tsx @@ -0,0 +1,79 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/search"; +import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; +import { IconButton, Link } from "@vector-im/compound-web"; + +import { _t } from "../../../languageHandler"; +import { PosthogScreenTracker } from "../../../PosthogTrackers"; +import SearchWarning, { WarningKind } from "../elements/SearchWarning"; +import { SearchInfo, SearchScope } from "../../../Searching"; + +interface Props { + searchInfo?: SearchInfo; + isRoomEncrypted: boolean; + onSearchScopeChange(scope: SearchScope): void; + onCancelClick(): void; +} + +const RoomSearchAuxPanel: React.FC = ({ searchInfo, isRoomEncrypted, onSearchScopeChange, onCancelClick }) => { + const scope = searchInfo?.scope ?? SearchScope.Room; + + return ( + <> + +
+
+ +
+ {searchInfo + ? _t( + "room|search|summary", + { count: searchInfo.count ?? 0 }, + { query: () => {searchInfo.term} }, + ) + : undefined} + +
+
+
+ + onSearchScopeChange(scope === SearchScope.Room ? SearchScope.All : SearchScope.Room) + } + kind="primary" + > + {scope === SearchScope.All + ? _t("room|search|this_room_button") + : _t("room|search|all_rooms_button")} + + + + +
+
+ + ); +}; + +export default RoomSearchAuxPanel; diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 6fbccaff6a6..7a25f65959e 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -289,7 +289,7 @@ export default class RoomSublist extends React.Component { if (payload.action === Action.ViewRoom && payload.show_room_tile && this.state.rooms) { // XXX: we have to do this a tick later because we have incorrect intermediate props during a room change // where we lose the room we are changing from temporarily and then it comes back in an update right after. - setImmediate(() => { + setTimeout(() => { const roomIndex = this.state.rooms.findIndex((r) => r.roomId === payload.room_id); if (!this.state.isExpanded && roomIndex > -1) { @@ -300,7 +300,7 @@ export default class RoomSublist extends React.Component { this.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT); this.forceUpdate(); // because the layout doesn't trigger a re-render } - }); + }, 0); } }; @@ -457,9 +457,9 @@ export default class RoomSublist extends React.Component { this.toggleCollapsed(); // if the bottom list is collapsed then scroll it in so it doesn't expand off screen if (!isExpanded && isStickyBottom) { - setImmediate(() => { + setTimeout(() => { sublist.scrollIntoView({ behavior: "smooth" }); - }); + }, 0); } } }; diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index aae949858e1..71ff1ef2961 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -195,7 +195,7 @@ export class RoomTile extends React.PureComponent { payload.room_id === this.props.room.roomId && payload.show_room_tile ) { - setImmediate(() => { + setTimeout(() => { this.scrollIntoView(); }); } diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx deleted file mode 100644 index aa67bc16cf4..00000000000 --- a/src/components/views/rooms/SearchBar.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React, { createRef, RefObject } from "react"; -import classNames from "classnames"; - -import AccessibleButton from "../elements/AccessibleButton"; -import { _t } from "../../../languageHandler"; -import { PosthogScreenTracker } from "../../../PosthogTrackers"; -import { getKeyBindingsManager } from "../../../KeyBindingsManager"; -import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -import SearchWarning, { WarningKind } from "../elements/SearchWarning"; - -interface IProps { - onCancelClick: () => void; - onSearch: (query: string, scope: SearchScope) => void; - searchInProgress?: boolean; - isRoomEncrypted?: boolean; -} - -interface IState { - scope: SearchScope; -} - -export enum SearchScope { - Room = "Room", - All = "All", -} - -export default class SearchBar extends React.Component { - private searchTerm: RefObject = createRef(); - - public constructor(props: IProps) { - super(props); - this.state = { - scope: SearchScope.Room, - }; - } - - private onThisRoomClick = (): void => { - this.setState({ scope: SearchScope.Room }, () => this.searchIfQuery()); - }; - - private onAllRoomsClick = (): void => { - this.setState({ scope: SearchScope.All }, () => this.searchIfQuery()); - }; - - private onSearchChange = (e: React.KeyboardEvent): void => { - const action = getKeyBindingsManager().getAccessibilityAction(e); - switch (action) { - case KeyBindingAction.Enter: - this.onSearch(); - break; - case KeyBindingAction.Escape: - this.props.onCancelClick(); - break; - } - }; - - private searchIfQuery(): void { - if (this.searchTerm.current?.value) { - this.onSearch(); - } - } - - private onSearch = (): void => { - if (!this.searchTerm.current?.value.trim()) return; - this.props.onSearch(this.searchTerm.current.value, this.state.scope); - }; - - public render(): React.ReactNode { - const searchButtonClasses = classNames("mx_SearchBar_searchButton", { - mx_SearchBar_searching: this.props.searchInProgress, - }); - const thisRoomClasses = classNames("mx_SearchBar_button", { - mx_SearchBar_unselected: this.state.scope !== SearchScope.Room, - }); - const allRoomsClasses = classNames("mx_SearchBar_button", { - mx_SearchBar_unselected: this.state.scope !== SearchScope.All, - }); - - return ( - <> - -
-
- - {_t("room|search|this_room")} - - - {_t("room|search|all_rooms")} - -
-
- - -
- -
- - - ); - } -} diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0ea0bdf94c1..c5972ee86ac 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React, { createRef, KeyboardEvent, SyntheticEvent } from "react"; -import EMOJI_REGEX from "emojibase-regex"; import { IContent, MatrixEvent, @@ -70,6 +69,7 @@ import { doMaybeLocalRoomAction } from "../../../utils/local-room"; import { Caret } from "../../../editor/caret"; import { IDiff } from "../../../editor/diff"; import { getBlobSafeMimeType } from "../../../utils/blobs"; +import { EMOJI_REGEX } from "../../../HtmlUtils"; /** * Build the mentions information based on the editor model (and any related events): diff --git a/src/components/views/rooms/SpaceScopeHeader.tsx b/src/components/views/rooms/SpaceScopeHeader.tsx deleted file mode 100644 index 377742dd122..00000000000 --- a/src/components/views/rooms/SpaceScopeHeader.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; -import { Text } from "@vector-im/compound-web"; - -import RoomAvatar from "../avatars/RoomAvatar"; -import { useRoomName } from "../../../hooks/useRoomName"; - -/** - * Scope header used to decorate right panels that are scoped to a space. - * When room is not a space renders nothing. - * Otherwise renders room avatar and name. - */ -export const SpaceScopeHeader: React.FC<{ room: Room }> = ({ room }) => { - const roomName = useRoomName(room); - - if (!room.isSpaceRoom()) { - return null; - } - - return ( - - - {roomName} - - ); -}; diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.tsx b/src/components/views/rooms/ThirdPartyMemberInfo.tsx index 5e02b3b9c81..b510d5e002c 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.tsx +++ b/src/components/views/rooms/ThirdPartyMemberInfo.tsx @@ -28,7 +28,6 @@ import { Action } from "../../../dispatcher/actions"; import ErrorDialog from "../dialogs/ErrorDialog"; import BaseCard from "../right_panel/BaseCard"; import { Flex } from "../../utils/Flex"; -import { SpaceScopeHeader } from "./SpaceScopeHeader"; interface IProps { event: MatrixEvent; @@ -133,10 +132,8 @@ export default class ThirdPartyMemberInfo extends React.Component : undefined; - return ( - + {/* same as userinfo name style */} diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 1b5636cd158..2bc6337945a 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import { Room, RoomEvent, RoomMember, RoomMemberEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { compare } from "matrix-js-sdk/src/utils"; import * as WhoIsTyping from "../../../WhoIsTyping"; import Timer from "../../../utils/Timer"; @@ -208,7 +207,8 @@ export default class WhoIsTypingTile extends React.Component { // sort them so the typing members don't change order when // moved to delayedStopTypingTimers - usersTyping.sort((a, b) => compare(a.name, b.name)); + const collator = new Intl.Collator(); + usersTyping.sort((a, b) => collator.compare(a.name, b.name)); const typingString = WhoIsTyping.whoIsTypingString(usersTyping, this.props.whoIsTypingLimit); if (!typingString) { diff --git a/src/components/views/settings/AvatarSetting.tsx b/src/components/views/settings/AvatarSetting.tsx index 81f1cd5201c..9bb9f736b2a 100644 --- a/src/components/views/settings/AvatarSetting.tsx +++ b/src/components/views/settings/AvatarSetting.tsx @@ -15,9 +15,9 @@ limitations under the License. */ import React, { ReactNode, createRef, useCallback, useEffect, useState } from "react"; -import { Icon as EditIcon } from "@vector-im/compound-design-tokens/icons/edit.svg"; -import { Icon as UploadIcon } from "@vector-im/compound-design-tokens/icons/share.svg"; -import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg"; +import EditIcon from "@vector-im/compound-design-tokens/assets/web/icons/edit"; +import UploadIcon from "@vector-im/compound-design-tokens/assets/web/icons/share"; +import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete"; import { Menu, MenuItem } from "@vector-im/compound-web"; import classNames from "classnames"; @@ -170,6 +170,7 @@ const AvatarSetting: React.FC = ({ aria-labelledby={disabled ? undefined : a11yId} // Inhibit tab stop as we have explicit upload/remove buttons tabIndex={-1} + disabled={disabled} > @@ -184,6 +185,7 @@ const AvatarSetting: React.FC = ({ onClick={uploadAvatar} // Inhibit tab stop as we have explicit upload/remove buttons tabIndex={-1} + disabled={disabled} /> ); } diff --git a/src/components/views/settings/E2eAdvancedPanel.tsx b/src/components/views/settings/E2eAdvancedPanel.tsx deleted file mode 100644 index f63cbefb226..00000000000 --- a/src/components/views/settings/E2eAdvancedPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; - -import { _t } from "../../../languageHandler"; -import { SettingLevel } from "../../../settings/SettingLevel"; -import SettingsStore from "../../../settings/SettingsStore"; -import SettingsFlag from "../elements/SettingsFlag"; -import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection"; - -const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions"; - -const E2eAdvancedPanel: React.FC = () => { - return ( - - - - {_t("settings|security|encryption_individual_verification_mode")} - - - ); -}; - -export default E2eAdvancedPanel; - -export function isE2eAdvancedPanelPossible(): boolean { - return SettingsStore.canSetValue(SETTING_MANUALLY_VERIFY_ALL_SESSIONS, null, SettingLevel.DEVICE); -} diff --git a/src/components/views/settings/LayoutSwitcher.tsx b/src/components/views/settings/LayoutSwitcher.tsx index 101a75fbe89..de3827b63df 100644 --- a/src/components/views/settings/LayoutSwitcher.tsx +++ b/src/components/views/settings/LayoutSwitcher.tsx @@ -1,131 +1,170 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. -Copyright 2021 Šimon Brandner + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +import React, { JSX, useEffect, useState } from "react"; +import { Field, HelpMessage, InlineField, Label, RadioControl, Root, ToggleControl } from "@vector-im/compound-web"; - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import classNames from "classnames"; - -import SettingsStore from "../../../settings/SettingsStore"; -import EventTilePreview from "../elements/EventTilePreview"; -import StyledRadioButton from "../elements/StyledRadioButton"; +import SettingsSubsection from "./shared/SettingsSubsection"; import { _t } from "../../../languageHandler"; -import { Layout } from "../../../settings/enums/Layout"; +import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { useSettingValue } from "../../../hooks/useSettings"; +import { Layout } from "../../../settings/enums/Layout"; +import EventTilePreview from "../elements/EventTilePreview"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -interface IProps { - userId?: string; - displayName?: string; - avatarUrl?: string; - messagePreviewText: string; - onLayoutChanged: (layout: Layout) => void; +/** + * A section to switch between different message layouts. + */ +export function LayoutSwitcher(): JSX.Element { + return ( + + + + + ); } -interface IState { +/** + * A selector to choose the layout of the messages. + */ +function LayoutSelector(): JSX.Element { + return ( + { + // We don't have any file in the form, we can cast it as string safely + const newLayout = new FormData(evt.currentTarget).get("layout") as string | null; + await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, newLayout); + }} + > + + + + + ); +} + +/** + * A radio button to select a layout. + */ +interface LayoutRadioProps { + /** + * The value of the layout. + */ layout: Layout; + /** + * The label to display for the layout. + */ + label: string; } -export default class LayoutSwitcher extends React.Component { - public constructor(props: IProps) { - super(props); +/** + * A radio button to select a layout. + * @param layout + * @param label + */ +function LayoutRadio({ layout, label }: LayoutRadioProps): JSX.Element { + const currentLayout = useSettingValue("layout"); + const eventTileInfo = useEventTileInfo(); - this.state = { - layout: SettingsStore.getValue("layout"), - }; - } + return ( + + + + ); +} + +type EventTileInfo = { + /** + * The ID of the user to display. + */ + userId: string; + /** + * The display name of the user to display. + */ + displayName?: string; + /** + * The avatar URL of the user to display. + */ + avatarUrl?: string; +}; + +/** + * Fetch the information to display in the event tile preview. + */ +function useEventTileInfo(): EventTileInfo { + const matrixClient = useMatrixClientContext(); + const userId = matrixClient.getSafeUserId(); + const [eventTileInfo, setEventTileInfo] = useState({ userId }); - private onLayoutChange = (e: React.ChangeEvent): void => { - const layout = e.target.value as Layout; + useEffect(() => { + const run = async (): Promise => { + const profileInfo = await matrixClient.getProfileInfo(userId); + setEventTileInfo({ + userId, + displayName: profileInfo.displayname, + avatarUrl: profileInfo.avatar_url, + }); + }; - this.setState({ layout: layout }); - SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout); - this.props.onLayoutChanged(layout); - }; + run(); + }, [userId, matrixClient, setEventTileInfo]); + return eventTileInfo; +} - public render(): React.ReactNode { - const ircClasses = classNames("mx_LayoutSwitcher_RadioButton", { - mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.IRC, - }); - const groupClasses = classNames("mx_LayoutSwitcher_RadioButton", { - mx_LayoutSwitcher_RadioButton_selected: this.state.layout == Layout.Group, - }); - const bubbleClasses = classNames("mx_LayoutSwitcher_RadioButton", { - mx_LayoutSwitcher_RadioButton_selected: this.state.layout === Layout.Bubble, - }); +/** + * A toggleable setting to enable or disable the compact layout. + */ +function ToggleCompactLayout(): JSX.Element { + const compactLayoutEnabled = useSettingValue("useCompactLayout"); + const layout = useSettingValue("layout"); - return ( - -
- - - -
-
- ); - } + return ( + { + const checked = new FormData(evt.currentTarget).get("compactLayout") === "on"; + await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, checked); + }} + > + + } + > + + {_t("settings|appearance|compact_layout_description")} + + + ); } diff --git a/src/components/views/settings/PowerLevelSelector.tsx b/src/components/views/settings/PowerLevelSelector.tsx index 5d823c885d2..dcb1590c074 100644 --- a/src/components/views/settings/PowerLevelSelector.tsx +++ b/src/components/views/settings/PowerLevelSelector.tsx @@ -18,7 +18,6 @@ import React, { useState, JSX, PropsWithChildren } from "react"; import { Button } from "@vector-im/compound-web"; -import { compare } from "matrix-js-sdk/src/utils"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import PowerSelector from "../elements/PowerSelector"; @@ -78,9 +77,11 @@ export function PowerLevelSelector({ currentPowerLevel && currentPowerLevel.value !== userLevels[currentPowerLevel?.userId], ); + const collator = new Intl.Collator(); + // We sort the users by power level, then we filter them const users = Object.keys(userLevels) - .sort((userA, userB) => sortUser(userA, userB, userLevels)) + .sort((userA, userB) => sortUser(collator, userA, userB, userLevels)) .filter(filter); // No user to display, we return the children into fragment to convert it to JSX.Element type @@ -136,7 +137,14 @@ export function PowerLevelSelector({ * @param userB * @param userLevels */ -function sortUser(userA: string, userB: string, userLevels: PowerLevelSelectorProps["userLevels"]): number { +function sortUser( + collator: Intl.Collator, + userA: string, + userB: string, + userLevels: PowerLevelSelectorProps["userLevels"], +): number { const powerLevelDiff = userLevels[userA] - userLevels[userB]; - return powerLevelDiff !== 0 ? powerLevelDiff : compare(userA.toLocaleLowerCase(), userB.toLocaleLowerCase()); + return powerLevelDiff !== 0 + ? powerLevelDiff + : collator.compare(userA.toLocaleLowerCase(), userB.toLocaleLowerCase()); } diff --git a/src/components/views/settings/SetIntegrationManager.tsx b/src/components/views/settings/SetIntegrationManager.tsx index 374508d9c66..fe55efd90fb 100644 --- a/src/components/views/settings/SetIntegrationManager.tsx +++ b/src/components/views/settings/SetIntegrationManager.tsx @@ -25,6 +25,7 @@ import { SettingLevel } from "../../../settings/SettingLevel"; import ToggleSwitch from "../elements/ToggleSwitch"; import Heading from "../typography/Heading"; import { SettingsSubsectionText } from "./shared/SettingsSubsection"; +import { UIFeature } from "../../../settings/UIFeature"; interface IProps {} @@ -71,6 +72,8 @@ export default class SetIntegrationManager extends React.Component
- {_t("integration_manager|manage_title")} - {managerName} + {_t("integration_manager|manage_title")} + {managerName}
("feature_custom_themes"); + + return ( + + {themeWatcher.current.isSystemThemeSupported() && ( + + )} + + {customThemeEnabled && } + + ); +} -interface IThemeState { - theme: string; - useSystemTheme: boolean; +/** + * Component to toggle the system theme + */ +interface SystemThemeProps { + /* Whether the system theme is activated */ + systemThemeActivated: boolean; } -export interface CustomThemeMessage { - isError: boolean; - text: string; +/** + * Component to toggle the system theme + */ +function SystemTheme({ systemThemeActivated }: SystemThemeProps): JSX.Element { + return ( + { + const checked = new FormData(evt.currentTarget).get("systemTheme") === "on"; + await SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); + dis.dispatch({ action: Action.RecheckTheme }); + }} + > + } + > + + + + ); } -interface IState extends IThemeState { - customThemeUrl: string; - customThemeMessage: CustomThemeMessage; +/** + * Component to select the theme + */ +interface ThemeSelectorProps { + /* The current theme */ + theme: string; + /* The theme can't be selected */ + disabled: boolean; } -export default class ThemeChoicePanel extends React.Component { - private themeTimer?: number; +/** + * Component to select the theme + */ +function ThemeSelectors({ theme, disabled }: ThemeSelectorProps): JSX.Element { + const themes = useThemes(); + + return ( + { + // We don't have any file in the form, we can cast it as string safely + const newTheme = new FormData(evt.currentTarget).get("themeSelector") as string | null; + + // Do nothing if the same theme is selected + if (!newTheme || theme === newTheme) return; + + // doing getValue in the .catch will still return the value we failed to set, + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => { + dis.dispatch({ action: Action.RecheckTheme }); + }); - public constructor(props: IProps) { - super(props); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now + // XXX: The local echoed value appears to be unreliable, in particular + // when settings custom themes(!) so adding forceTheme to override + // the value from settings. + dis.dispatch({ action: Action.RecheckTheme, forceTheme: newTheme }); + }} + > + {themes.map((_theme) => { + const isChecked = theme === _theme.id; + return ( + + } + > + + + ); + })} + + ); +} - this.state = { - ...ThemeChoicePanel.calculateThemeState(), - customThemeUrl: "", - customThemeMessage: { isError: false, text: "" }, - }; - } +/** + * Return all the available themes + */ +function useThemes(): Array { + const customThemes = useSettingValue("custom_themes"); + return useMemo(() => { + // Put the custom theme into a map + // To easily find the theme by name when going through the themes list + const checkedCustomThemes = customThemes || []; + const customThemeMap = checkedCustomThemes.reduce( + (map, theme) => map.set(theme.name, theme), + new Map(), + ); - public static calculateThemeState(): IThemeState { - // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we - // show the right values for things. + const themes = getOrderedThemes(); + // Separate the built-in themes from the custom themes + // To insert the high contrast theme between them + const builtInThemes = themes.filter((theme) => !customThemeMap.has(theme.name)); + const otherThemes = themes.filter((theme) => customThemeMap.has(theme.name)); - const themeChoice: string = SettingsStore.getValue("theme"); - const systemThemeExplicit: boolean = SettingsStore.getValueAt( - SettingLevel.DEVICE, - "use_system_theme", - null, - false, - true, - ); - const themeExplicit: string = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + const highContrastTheme = makeHighContrastTheme(); + if (highContrastTheme) builtInThemes.push(highContrastTheme); - // If the user has enabled system theme matching, use that. - if (systemThemeExplicit) { - return { - theme: themeChoice, - useSystemTheme: true, - }; - } + const allThemes = builtInThemes.concat(otherThemes); - // If the user has set a theme explicitly, use that (no system theme matching) - if (themeExplicit) { - return { - theme: themeChoice, - useSystemTheme: false, - }; - } + // Check if the themes are dark + return allThemes.map((theme) => { + const customTheme = customThemeMap.get(theme.name); + const isDark = (customTheme ? customTheme.is_dark : theme.id.includes("dark")) || false; + return { ...theme, isDark }; + }); + }, [customThemes]); +} - // Otherwise assume the defaults for the settings +/** + * Create the light high contrast theme + */ +function makeHighContrastTheme(): ITheme | undefined { + const lightHighContrastId = findHighContrastTheme("light"); + if (lightHighContrastId) { return { - theme: themeChoice, - useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), + name: _t("settings|appearance|high_contrast"), + id: lightHighContrastId, }; } +} - private onThemeChange = (newTheme: string): void => { - if (this.state.theme === newTheme) return; - - PosthogTrackers.trackInteraction("WebSettingsAppearanceTabThemeSelector"); - - // doing getValue in the .catch will still return the value we failed to set, - // so remember what the value was before we tried to set it so we can revert - const oldTheme: string = SettingsStore.getValue("theme"); - SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme).catch(() => { - dis.dispatch({ action: Action.RecheckTheme }); - this.setState({ theme: oldTheme }); - }); - this.setState({ theme: newTheme }); - // The settings watcher doesn't fire until the echo comes back from the - // server, so to make the theme change immediately we need to manually - // do the dispatch now - // XXX: The local echoed value appears to be unreliable, in particular - // when settings custom themes(!) so adding forceTheme to override - // the value from settings. - dis.dispatch({ action: Action.RecheckTheme, forceTheme: newTheme }); - }; - - private onUseSystemThemeChanged = (checked: boolean): void => { - this.setState({ useSystemTheme: checked }); - SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); - dis.dispatch({ action: Action.RecheckTheme }); - }; - - private onAddCustomTheme = async (): Promise => { - let currentThemes: string[] = SettingsStore.getValue("custom_themes"); - if (!currentThemes) currentThemes = []; - currentThemes = currentThemes.map((c) => c); // cheap clone - - if (this.themeTimer) { - clearTimeout(this.themeTimer); - } - - try { - const r = await fetch(this.state.customThemeUrl); - // XXX: need some schema for this - const themeInfo = await r.json(); - if (!themeInfo || typeof themeInfo["name"] !== "string" || typeof themeInfo["colors"] !== "object") { - this.setState({ - customThemeMessage: { text: _t("settings|appearance|custom_theme_invalid"), isError: true }, - }); - return; - } - currentThemes.push(themeInfo); - } catch (e) { - logger.error(e); - this.setState({ - customThemeMessage: { text: _t("settings|appearance|custom_theme_error_downloading"), isError: true }, - }); - return; // Don't continue on error - } - - await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); - this.setState({ - customThemeUrl: "", - customThemeMessage: { text: _t("settings|appearance|custom_theme_success"), isError: false }, - }); - - this.themeTimer = window.setTimeout(() => { - this.setState({ customThemeMessage: { text: "", isError: false } }); - }, 3000); - }; - - private onCustomThemeChange = (e: React.ChangeEvent): void => { - this.setState({ customThemeUrl: e.target.value }); - }; - - private renderHighContrastCheckbox(): React.ReactElement | undefined { - if ( - !this.state.useSystemTheme && - (findHighContrastTheme(this.state.theme) || isHighContrastTheme(this.state.theme)) - ) { - return ( -
- this.highContrastThemeChanged(e.target.checked)} - > - {_t("settings|appearance|use_high_contrast")} - -
- ); - } - } +interface CustomThemeProps { + /** + * The current theme + */ + theme: string; +} - private highContrastThemeChanged(checked: boolean): void { - let newTheme: string | undefined; - if (checked) { - newTheme = findHighContrastTheme(this.state.theme); - } else { - newTheme = findNonHighContrastTheme(this.state.theme); - } - if (newTheme) { - this.onThemeChange(newTheme); - } - } +/** + * Add and manager custom themes + */ +function CustomTheme({ theme }: CustomThemeProps): JSX.Element { + const [customTheme, setCustomTheme] = useState(""); + const [error, setError] = useState(); + const clear = useCallback(() => { + setError(undefined); + setCustomTheme(""); + }, [setError, setCustomTheme]); + + return ( +
+ ) => { + setError(undefined); + setCustomTheme(e.target.value); + }} + onSave={async () => { + // The field empty is empty + if (!customTheme) return; + + // Get the custom themes and do a cheap clone + // To avoid to mutate the original array in the settings + const currentThemes = + SettingsStore.getValue("custom_themes").map((t) => t) || []; + + try { + const r = await fetch(customTheme); + // XXX: need some schema for this + const themeInfo = await r.json(); + if ( + !themeInfo || + typeof themeInfo["name"] !== "string" || + typeof themeInfo["colors"] !== "object" + ) { + setError(_t("settings|appearance|custom_theme_invalid")); + return; + } + + // Check if the theme is already existing + const isAlreadyExisting = Boolean(currentThemes.find((t) => t.name === themeInfo.name)); + if (isAlreadyExisting) { + clear(); + return; + } + + currentThemes.push(themeInfo); + } catch (e) { + logger.error(e); + setError(_t("settings|appearance|custom_theme_error_downloading")); + return; + } + + // Reset the error + clear(); + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); + }} + onCancel={clear} + > + {_t("settings|appearance|custom_theme_help")} + {error && {error}} + + +
+ ); +} - public render(): React.ReactElement { - const themeWatcher = new ThemeWatcher(); - let systemThemeSection: JSX.Element | undefined; - if (themeWatcher.isSystemThemeSupported()) { - systemThemeSection = ( -
- this.onUseSystemThemeChanged(e.target.checked)} - > - {SettingsStore.getDisplayName("use_system_theme")} - -
- ); - } +interface CustomThemeListProps { + /* + * The current theme + */ + theme: string; +} - let customThemeForm: JSX.Element | undefined; - if (SettingsStore.getValue("feature_custom_themes")) { - let messageElement: JSX.Element | undefined; - if (this.state.customThemeMessage.text) { - if (this.state.customThemeMessage.isError) { - messageElement =
{this.state.customThemeMessage.text}
; - } else { - messageElement =
{this.state.customThemeMessage.text}
; - } - } - customThemeForm = ( -
-
- - ("custom_themes") || []; + + return ( +
    + {customThemes.map((theme) => { + return ( +
  • + {theme.name} + { + // Get the custom themes and do a cheap clone + // To avoid to mutate the original array in the settings + const currentThemes = + SettingsStore.getValue("custom_themes").map((t) => t) || []; + + // Remove the theme from the list + const newThemes = currentThemes.filter((t) => t.name !== theme.name); + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, newThemes); + + // If the delete custom theme is the current theme, reset the theme to the default theme + // By settings the theme at null at the device level, we are getting the default theme + if (currentTheme === `custom-${theme.name}`) { + await SettingsStore.setValue("theme", null, SettingLevel.DEVICE, null); + dis.dispatch({ + action: Action.RecheckTheme, + }); + } + }} > - {_t("settings|appearance|custom_theme_add_button")} - - {messageElement} - -
- ); - } - - const orderedThemes = getOrderedThemes(); - return ( - - {systemThemeSection} -
- ({ - value: t.id, - label: t.name, - disabled: this.state.useSystemTheme, - className: "mx_ThemeSelector_" + t.id, - }))} - onChange={this.onThemeChange} - value={this.apparentSelectedThemeId()} - outlined - /> -
- {this.renderHighContrastCheckbox()} - {customThemeForm} -
- ); - } - - public apparentSelectedThemeId(): string | undefined { - if (this.state.useSystemTheme) { - return undefined; - } - const nonHighContrast = findNonHighContrastTheme(this.state.theme); - return nonHighContrast ? nonHighContrast : this.state.theme; - } + + + + ); + })} + + ); } diff --git a/src/components/views/settings/UserPersonalInfoSettings.tsx b/src/components/views/settings/UserPersonalInfoSettings.tsx new file mode 100644 index 00000000000..8e5880a5170 --- /dev/null +++ b/src/components/views/settings/UserPersonalInfoSettings.tsx @@ -0,0 +1,130 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback, useEffect, useState } from "react"; +import { ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { Alert } from "@vector-im/compound-web"; + +import AccountEmailAddresses from "./account/EmailAddresses"; +import AccountPhoneNumbers from "./account/PhoneNumbers"; +import { _t } from "../../../languageHandler"; +import InlineSpinner from "../elements/InlineSpinner"; +import SettingsSubsection from "./shared/SettingsSubsection"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { ThirdPartyIdentifier } from "../../../AddThreepid"; +import SettingsStore from "../../../settings/SettingsStore"; +import { UIFeature } from "../../../settings/UIFeature"; + +type LoadingState = "loading" | "loaded" | "error"; + +interface ThreepidSectionWrapperProps { + error: string; + loadingState: LoadingState; + children: React.ReactNode; +} + +const ThreepidSectionWrapper: React.FC = ({ error, loadingState, children }) => { + if (loadingState === "loading") { + return ; + } else if (loadingState === "error") { + return ( + + {error} + + ); + } else { + return <>{children}; + } +}; + +interface UserPersonalInfoSettingsProps { + canMake3pidChanges: boolean; +} + +/** + * Settings controls allowing the user to set personal information like email addresses. + */ +export const UserPersonalInfoSettings: React.FC = ({ canMake3pidChanges }) => { + const [emails, setEmails] = useState(); + const [phoneNumbers, setPhoneNumbers] = useState(); + const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading"); + + const client = useMatrixClientContext(); + + useEffect(() => { + (async () => { + try { + const threepids = await client.getThreePids(); + setEmails(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Email)); + setPhoneNumbers(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Phone)); + setLoadingState("loaded"); + } catch (e) { + setLoadingState("error"); + } + })(); + }, [client]); + + const onEmailsChange = useCallback((emails: ThirdPartyIdentifier[]) => { + setEmails(emails); + }, []); + + const onMsisdnsChange = useCallback((msisdns: ThirdPartyIdentifier[]) => { + setPhoneNumbers(msisdns); + }, []); + + if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null; + + return ( +
+

{_t("settings|general|personal_info")}

+ + + + + + + + + + + +
+ ); +}; + +export default UserPersonalInfoSettings; diff --git a/src/components/views/settings/UserProfileSettings.tsx b/src/components/views/settings/UserProfileSettings.tsx index f912fa44f53..a5ff4356767 100644 --- a/src/components/views/settings/UserProfileSettings.tsx +++ b/src/components/views/settings/UserProfileSettings.tsx @@ -16,7 +16,8 @@ limitations under the License. import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { EditInPlace, Alert } from "@vector-im/compound-web"; +import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web"; +import { Icon as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg"; import { _t } from "../../../languageHandler"; import { OwnProfileStore } from "../../../stores/OwnProfileStore"; @@ -29,6 +30,7 @@ import UserIdentifierCustomisations from "../../../customisations/UserIdentifier import { useId } from "../../../utils/useId"; import CopyableText from "../elements/CopyableText"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import AccessibleButton from "../elements/AccessibleButton"; const SpinnerToast: React.FC = ({ children }) => ( <> @@ -55,13 +57,44 @@ const UsernameBox: React.FC = ({ username }) => { ); }; +interface ManageAccountButtonProps { + externalAccountManagementUrl: string; +} + +const ManageAccountButton: React.FC = ({ externalAccountManagementUrl }) => ( + + + {_t("settings|general|oidc_manage_button")} + +); + +interface UserProfileSettingsProps { + // The URL to redirect the user to in order to manage their account. + externalAccountManagementUrl?: string; + // Whether the homeserver allows the user to set their display name. + canSetDisplayName: boolean; + // Whether the homeserver allows the user to set their avatar. + canSetAvatar: boolean; +} + /** * A group of settings views to allow the user to set their profile information. */ -const UserProfileSettings: React.FC = () => { +const UserProfileSettings: React.FC = ({ + externalAccountManagementUrl, + canSetDisplayName, + canSetAvatar, +}) => { const [avatarURL, setAvatarURL] = useState(OwnProfileStore.instance.avatarMxc); const [displayName, setDisplayName] = useState(OwnProfileStore.instance.displayName ?? ""); - const [initialDisplayName, setInitialDisplayName] = useState(OwnProfileStore.instance.displayName ?? ""); const [avatarError, setAvatarError] = useState(false); const [maxUploadSize, setMaxUploadSize] = useState(); const [displayNameError, setDisplayNameError] = useState(false); @@ -128,9 +161,9 @@ const UserProfileSettings: React.FC = () => { try { setDisplayNameError(false); await client.setDisplayName(displayName); - setInitialDisplayName(displayName); } catch (e) { setDisplayNameError(true); + throw e; } }, [displayName, client]); @@ -142,10 +175,16 @@ const UserProfileSettings: React.FC = () => { [client], ); + const someFieldsDisabled = !canSetDisplayName || !canSetAvatar; + return (

{_t("common|profile")}

-
{_t("settings|general|profile_subtitle")}
+
+ {someFieldsDisabled + ? _t("settings|general|profile_subtitle_oidc") + : _t("settings|general|profile_subtitle")} +
{ removeAvatar={avatarURL ? onAvatarRemove : undefined} placeholderName={displayName} placeholderId={client.getUserId() ?? ""} + disabled={!canSetAvatar} /> + disabled={!canSetDisplayName} + > + {displayNameError && {_t("settings|general|display_name_error")}} +
{avatarError && ( @@ -177,6 +219,11 @@ const UserProfileSettings: React.FC = () => { )} {userIdentifier && } + {externalAccountManagementUrl && ( +
+ +
+ )}
); }; diff --git a/src/components/views/settings/discovery/DiscoverySettings.tsx b/src/components/views/settings/discovery/DiscoverySettings.tsx new file mode 100644 index 00000000000..8b1a20ac2e1 --- /dev/null +++ b/src/components/views/settings/discovery/DiscoverySettings.tsx @@ -0,0 +1,190 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback, useEffect, useState } from "react"; +import { SERVICE_TYPES, ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import { Alert } from "@vector-im/compound-web"; + +import DiscoveryEmailAddresses from "../discovery/EmailAddresses"; +import DiscoveryPhoneNumbers from "../discovery/PhoneNumbers"; +import { getThreepidsWithBindStatus } from "../../../../boundThreepids"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import { ThirdPartyIdentifier } from "../../../../AddThreepid"; +import SettingsStore from "../../../../settings/SettingsStore"; +import { UIFeature } from "../../../../settings/UIFeature"; +import { _t } from "../../../../languageHandler"; +import SetIdServer from "../SetIdServer"; +import SettingsSubsection from "../shared/SettingsSubsection"; +import InlineTermsAgreement from "../../terms/InlineTermsAgreement"; +import { Service, ServicePolicyPair, startTermsFlow } from "../../../../Terms"; +import IdentityAuthClient from "../../../../IdentityAuthClient"; +import { abbreviateUrl } from "../../../../utils/UrlUtils"; +import { useDispatcher } from "../../../../hooks/useDispatcher"; +import defaultDispatcher from "../../../../dispatcher/dispatcher"; +import { ActionPayload } from "../../../../dispatcher/payloads"; + +type RequiredPolicyInfo = + | { + // This object is passed along to a component for handling + policiesAndServices: null; // From the startTermsFlow callback + agreedUrls: null; // From the startTermsFlow callback + resolve: null; // Promise resolve function for startTermsFlow callback + } + | { + policiesAndServices: ServicePolicyPair[]; + agreedUrls: string[]; + resolve: (values: string[]) => void; + }; + +/** + * Settings controlling how a user's email addreses and phone numbers can be used to discover them + */ +export const DiscoverySettings: React.FC = () => { + const client = useMatrixClientContext(); + + const [emails, setEmails] = useState([]); + const [phoneNumbers, setPhoneNumbers] = useState([]); + const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading"); + const [idServerName, setIdServerName] = useState(abbreviateUrl(client.getIdentityServerUrl())); + const [canMake3pidChanges, setCanMake3pidChanges] = useState(false); + + const [requiredPolicyInfo, setRequiredPolicyInfo] = useState({ + // This object is passed along to a component for handling + policiesAndServices: null, // From the startTermsFlow callback + agreedUrls: null, // From the startTermsFlow callback + resolve: null, // Promise resolve function for startTermsFlow callback + }); + const [hasTerms, setHasTerms] = useState(false); + + const getThreepidState = useCallback(async () => { + const threepids = await getThreepidsWithBindStatus(client); + setEmails(threepids.filter((a) => a.medium === ThreepidMedium.Email)); + setPhoneNumbers(threepids.filter((a) => a.medium === ThreepidMedium.Phone)); + }, [client]); + + useDispatcher( + defaultDispatcher, + useCallback( + (payload: ActionPayload) => { + if (payload.action === "id_server_changed") { + setIdServerName(abbreviateUrl(client.getIdentityServerUrl())); + + getThreepidState().then(); + } + }, + [client, getThreepidState], + ), + ); + + useEffect(() => { + (async () => { + try { + await getThreepidState(); + + const capabilities = await client.getCapabilities(); + setCanMake3pidChanges( + !capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true, + ); + + // By starting the terms flow we get the logic for checking which terms the user has signed + // for free. So we might as well use that for our own purposes. + const idServerUrl = client.getIdentityServerUrl(); + if (!idServerUrl) { + return; + } + + const authClient = new IdentityAuthClient(); + try { + const idAccessToken = await authClient.getAccessToken({ check: false }); + await startTermsFlow( + client, + [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)], + (policiesAndServices, agreedUrls, extraClassNames) => { + return new Promise((resolve) => { + setIdServerName(abbreviateUrl(idServerUrl)); + setHasTerms(true); + setRequiredPolicyInfo({ + policiesAndServices, + agreedUrls, + resolve, + }); + }); + }, + ); + // User accepted all terms + setHasTerms(false); + } catch (e) { + logger.warn( + `Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`, + ); + logger.warn(e); + } + + setLoadingState("loaded"); + } catch (e) { + setLoadingState("error"); + } + })(); + }, [client, getThreepidState]); + + if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null; + + if (hasTerms && requiredPolicyInfo.policiesAndServices) { + const intro = ( + + {_t("settings|general|discovery_needs_terms", { serverName: idServerName })} + + ); + return ( + <> + + {/* has its own heading as it includes the current identity server */} + + + ); + } + + const threepidSection = idServerName ? ( + <> + + + + ) : null; + + return ( + + {threepidSection} + {/* has its own heading as it includes the current identity server */} + + + ); +}; + +export default DiscoverySettings; diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index 035306f5f34..afcf92dea33 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -16,6 +16,7 @@ limitations under the License. import classNames from "classnames"; import React, { HTMLAttributes } from "react"; +import { Separator } from "@vector-im/compound-web"; import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading"; @@ -25,6 +26,11 @@ export interface SettingsSubsectionProps extends HTMLAttributes children?: React.ReactNode; // when true content will be justify-items: stretch, which will make items within the section stretch to full width. stretchContent?: boolean; + /* + * When true, the legacy UI style will be applied to the subsection. + * @default true + */ + legacy?: boolean; } export const SettingsSubsectionText: React.FC> = ({ children, ...rest }) => ( @@ -38,10 +44,16 @@ export const SettingsSubsection: React.FC = ({ description, children, stretchContent, + legacy = true, ...rest }) => ( -
- {typeof heading === "string" ? : <>{heading}} +
+ {typeof heading === "string" ? : <>{heading}} {!!description && (
{description} @@ -52,11 +64,13 @@ export const SettingsSubsection: React.FC = ({ className={classNames("mx_SettingsSubsection_content", { mx_SettingsSubsection_contentStretch: !!stretchContent, mx_SettingsSubsection_noHeading: !heading && !description, + mx_SettingsSubsection_content_newUi: !legacy, })} > {children}
)} + {!legacy && }
); diff --git a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx index 262b9f4d371..936b11dc376 100644 --- a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx +++ b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx @@ -20,14 +20,24 @@ import Heading from "../../typography/Heading"; export interface SettingsSubsectionHeadingProps extends HTMLAttributes { heading: string; + legacy?: boolean; children?: React.ReactNode; } -export const SettingsSubsectionHeading: React.FC = ({ heading, children, ...rest }) => ( -
- - {heading} - - {children} -
-); +export const SettingsSubsectionHeading: React.FC = ({ + heading, + legacy = true, + children, + ...rest +}) => { + const size = legacy ? "4" : "3"; + + return ( +
+ + {heading} + + {children} +
+ ); +}; diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 411e75c9015..5b991be0d5e 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -50,6 +50,7 @@ import SettingsTab from "../SettingsTab"; import SdkConfig from "../../../../../SdkConfig"; import { shouldForceDisableEncryption } from "../../../../../utils/crypto/shouldForceDisableEncryption"; import { Caption } from "../../../typography/Caption"; +import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../../utils/crypto"; interface IProps { room: Room; @@ -176,7 +177,7 @@ export default class SecurityRoomSettingsTab extends React.Component { logger.error(e); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 99f5a51c3b2..cf3db0f5ad9 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -25,15 +25,13 @@ import Field from "../../../elements/Field"; import AccessibleButton from "../../../elements/AccessibleButton"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { UIFeature } from "../../../../../settings/UIFeature"; -import { Layout } from "../../../../../settings/enums/Layout"; -import LayoutSwitcher from "../../LayoutSwitcher"; +import { LayoutSwitcher } from "../../LayoutSwitcher"; import FontScalingPanel from "../../FontScalingPanel"; -import ThemeChoicePanel from "../../ThemeChoicePanel"; +import { ThemeChoicePanel } from "../../ThemeChoicePanel"; import ImageSizePanel from "../../ImageSizePanel"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection from "../../shared/SettingsSubsection"; -import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; interface IProps {} @@ -42,21 +40,9 @@ interface IState { useSystemFont: boolean; systemFont: string; showAdvanced: boolean; - layout: Layout; - // User profile data for the message preview - userId?: string; - displayName?: string; - avatarUrl?: string; } export default class AppearanceUserSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - - private readonly MESSAGE_PREVIEW_TEXT = _t("common|preview_message"); - - private unmounted = false; - public constructor(props: IProps) { super(props); @@ -65,32 +51,9 @@ export default class AppearanceUserSettingsTab extends React.Component { - // Fetch the current user profile for the message preview - const client = this.context; - const userId = client.getUserId()!; - const profileInfo = await client.getProfileInfo(userId); - if (this.unmounted) return; - - this.setState({ - userId, - displayName: profileInfo.displayname, - avatarUrl: profileInfo.avatar_url, - }); - } - - public componentWillUnmount(): void { - this.unmounted = true; - } - - private onLayoutChanged = (layout: Layout): void => { - this.setState({ layout: layout }); - }; - private renderAdvancedSection(): ReactNode { if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null; @@ -156,13 +119,7 @@ export default class AppearanceUserSettingsTab extends React.Component - + {this.renderAdvancedSection()} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 2a42d58dda2..e351e46a915 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -16,159 +16,61 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; -import { SERVICE_TYPES, HTTPError, IThreepid, ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import React from "react"; +import { HTTPError } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { Icon as WarningIcon } from "../../../../../../res/img/feather-customised/warning-triangle.svg"; import { UserFriendlyError, _t } from "../../../../../languageHandler"; import UserProfileSettings from "../../UserProfileSettings"; -import * as languageHandler from "../../../../../languageHandler"; import SettingsStore from "../../../../../settings/SettingsStore"; -import LanguageDropdown from "../../../elements/LanguageDropdown"; -import SpellCheckSettings from "../../SpellCheckSettings"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; -import PlatformPeg from "../../../../../PlatformPeg"; import Modal from "../../../../../Modal"; -import dis from "../../../../../dispatcher/dispatcher"; -import { Service, ServicePolicyPair, startTermsFlow } from "../../../../../Terms"; -import IdentityAuthClient from "../../../../../IdentityAuthClient"; -import { abbreviateUrl } from "../../../../../utils/UrlUtils"; -import { getThreepidsWithBindStatus } from "../../../../../boundThreepids"; -import { SettingLevel } from "../../../../../settings/SettingLevel"; import { UIFeature } from "../../../../../settings/UIFeature"; -import { ActionPayload } from "../../../../../dispatcher/payloads"; import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/ErrorDialog"; -import AccountPhoneNumbers from "../../account/PhoneNumbers"; -import AccountEmailAddresses from "../../account/EmailAddresses"; -import DiscoveryEmailAddresses from "../../discovery/EmailAddresses"; -import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers"; import ChangePassword from "../../ChangePassword"; -import InlineTermsAgreement from "../../../terms/InlineTermsAgreement"; -import SetIdServer from "../../SetIdServer"; -import SetIntegrationManager from "../../SetIntegrationManager"; -import ToggleSwitch from "../../../elements/ToggleSwitch"; -import { IS_MAC } from "../../../../../Keyboard"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; -import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading"; -import Heading from "../../../typography/Heading"; -import InlineSpinner from "../../../elements/InlineSpinner"; -import { ThirdPartyIdentifier } from "../../../../../AddThreepid"; import { SDKContext } from "../../../../../contexts/SDKContext"; import TchapUIFeature from '../../../../../../../../src/tchap/util/TchapUIFeature'; // :TCHAP: hide-discovery-email-phone-settings +import UserPersonalInfoSettings from "../../UserPersonalInfoSettings"; interface IProps { closeSettingsFn: () => void; } interface IState { - language: string; - spellCheckEnabled?: boolean; - spellCheckLanguages: string[]; - haveIdServer: boolean; - idServerHasUnsignedTerms: boolean; - requiredPolicyInfo: - | { - // This object is passed along to a component for handling - hasTerms: false; - policiesAndServices: null; // From the startTermsFlow callback - agreedUrls: null; // From the startTermsFlow callback - resolve: null; // Promise resolve function for startTermsFlow callback - } - | { - hasTerms: boolean; - policiesAndServices: ServicePolicyPair[]; - agreedUrls: string[]; - resolve: (values: string[]) => void; - }; - emails: ThirdPartyIdentifier[]; - msisdns: ThirdPartyIdentifier[]; - loading3pids: boolean; // whether or not the emails and msisdns have been loaded canChangePassword: boolean; idServerName?: string; externalAccountManagementUrl?: string; canMake3pidChanges: boolean; + canSetDisplayName: boolean; + canSetAvatar: boolean; } export default class GeneralUserSettingsTab extends React.Component { public static contextType = SDKContext; public context!: React.ContextType; - private readonly dispatcherRef: string; - public constructor(props: IProps, context: React.ContextType) { super(props); this.context = context; - const cli = this.context.client!; - this.state = { - language: languageHandler.getCurrentLanguage(), - spellCheckEnabled: false, - spellCheckLanguages: [], - haveIdServer: Boolean(cli.getIdentityServerUrl()), - idServerHasUnsignedTerms: false, - requiredPolicyInfo: { - // This object is passed along to a component for handling - hasTerms: false, - policiesAndServices: null, // From the startTermsFlow callback - agreedUrls: null, // From the startTermsFlow callback - resolve: null, // Promise resolve function for startTermsFlow callback - }, - emails: [], - msisdns: [], - loading3pids: true, // whether or not the emails and msisdns have been loaded canChangePassword: false, canMake3pidChanges: false, + canSetDisplayName: false, + canSetAvatar: false, }; - this.dispatcherRef = dis.register(this.onAction); - this.getCapabilities(); - this.getThreepidState(); - } - - public async componentDidMount(): Promise { - const plat = PlatformPeg.get(); - const [spellCheckEnabled, spellCheckLanguages] = await Promise.all([ - plat?.getSpellCheckEnabled(), - plat?.getSpellCheckLanguages(), - ]); - - if (spellCheckLanguages) { - this.setState({ - spellCheckEnabled, - spellCheckLanguages, - }); - } - } - - public componentWillUnmount(): void { - dis.unregister(this.dispatcherRef); } - private onAction = (payload: ActionPayload): void => { - if (payload.action === "id_server_changed") { - this.setState({ haveIdServer: Boolean(this.context.client!.getIdentityServerUrl()) }); - this.getThreepidState(); - } - }; - - private onEmailsChange = (emails: ThirdPartyIdentifier[]): void => { - this.setState({ emails }); - }; - - private onMsisdnsChange = (msisdns: ThirdPartyIdentifier[]): void => { - this.setState({ msisdns }); - }; - private async getCapabilities(): Promise { const cli = this.context.client!; - const capabilities = await cli.getCapabilities(); // this is cached + const capabilities = (await cli.getCapabilities()) ?? {}; const changePasswordCap = capabilities["m.change_password"]; // You can change your password so long as the capability isn't explicitly disabled. The implicit @@ -183,98 +85,19 @@ export default class GeneralUserSettingsTab extends React.Component { - const cli = this.context.client!; - - // Check to see if terms need accepting - this.checkTerms(); + const canSetDisplayName = + !capabilities["m.set_displayname"] || capabilities["m.set_displayname"].enabled === true; + const canSetAvatar = !capabilities["m.set_avatar_url"] || capabilities["m.set_avatar_url"].enabled === true; - // Need to get 3PIDs generally for Account section and possibly also for - // Discovery (assuming we have an IS and terms are agreed). - let threepids: IThreepid[] = []; - try { - threepids = await getThreepidsWithBindStatus(cli); - } catch (e) { - const idServerUrl = cli.getIdentityServerUrl(); - logger.warn( - `Unable to reach identity server at ${idServerUrl} to check ` + `for 3PIDs bindings in Settings`, - ); - logger.warn(e); - } this.setState({ - emails: threepids.filter((a) => a.medium === ThreepidMedium.Email), - msisdns: threepids.filter((a) => a.medium === ThreepidMedium.Phone), - loading3pids: false, + canChangePassword, + externalAccountManagementUrl, + canMake3pidChanges, + canSetDisplayName, + canSetAvatar, }); } - private async checkTerms(): Promise { - // By starting the terms flow we get the logic for checking which terms the user has signed - // for free. So we might as well use that for our own purposes. - const idServerUrl = this.context.client!.getIdentityServerUrl(); - if (!this.state.haveIdServer || !idServerUrl) { - this.setState({ idServerHasUnsignedTerms: false }); - return; - } - - const authClient = new IdentityAuthClient(); - try { - const idAccessToken = await authClient.getAccessToken({ check: false }); - await startTermsFlow( - this.context.client!, - [new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)], - (policiesAndServices, agreedUrls, extraClassNames) => { - return new Promise((resolve, reject) => { - this.setState({ - idServerName: abbreviateUrl(idServerUrl), - requiredPolicyInfo: { - hasTerms: true, - policiesAndServices, - agreedUrls, - resolve, - }, - }); - }); - }, - ); - // User accepted all terms - this.setState({ - requiredPolicyInfo: { - ...this.state.requiredPolicyInfo, // set first so we can override - hasTerms: false, - }, - }); - } catch (e) { - logger.warn(`Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`); - logger.warn(e); - } - } - - private onLanguageChange = (newLanguage: string): void => { - if (this.state.language === newLanguage) return; - - SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage); - this.setState({ language: newLanguage }); - const platform = PlatformPeg.get(); - if (platform) { - platform.setLanguage([newLanguage]); - platform.reload(); - } - }; - - private onSpellCheckLanguagesChange = (languages: string[]): void => { - this.setState({ spellCheckLanguages: languages }); - PlatformPeg.get()?.setSpellCheckLanguages(languages); - }; - - private onSpellCheckEnabledChange = (spellCheckEnabled: boolean): void => { - this.setState({ spellCheckEnabled }); - PlatformPeg.get()?.setSpellCheckEnabled(spellCheckEnabled); - }; - private onPasswordChangeError = (err: Error): void => { logger.error("Failed to change password: " + err); @@ -324,55 +147,16 @@ export default class GeneralUserSettingsTab extends React.Component - ) : ( - - ); - const msisdns = this.state.loading3pids ? ( - - ) : ( - - ); - threepidSection = ( - <> - - {emails} - + private renderAccountSection(): JSX.Element | undefined { + if (!this.state.canChangePassword) return undefined; - - {msisdns} - - - ); - } - - let passwordChangeSection: ReactNode = null; - if (this.state.canChangePassword) { - passwordChangeSection = ( - <> + return ( + <> + {_t("settings|general|password_change_section")} - - ); - } - - let externalAccountManagement: JSX.Element | undefined; - if (this.state.externalAccountManagementUrl) { - const { hostname } = new URL(this.state.externalAccountManagementUrl); - - externalAccountManagement = ( - <> - - {_t( - "settings|general|external_account_management", - { hostname }, - { code: (sub) => {sub} }, - )} - - - {_t("settings|general|oidc_manage_button")} - - - ); - } - return ( - <> - - {externalAccountManagement} - {passwordChangeSection} - {threepidSection} - - ); - } - - private renderLanguageSection(): JSX.Element { - // TODO: Convert to new-styled Field - return ( - - - - ); - } - - private renderSpellCheckSection(): JSX.Element { - const heading = ( - - - - ); - return ( - - {this.state.spellCheckEnabled && !IS_MAC && ( - - )} - - ); - } - - private renderDiscoverySection(): JSX.Element { - if (this.state.requiredPolicyInfo.hasTerms) { - const intro = ( - - {_t("settings|general|discovery_needs_terms", { serverName: this.state.idServerName })} - - ); - return ( - <> - - {/* has its own heading as it includes the current identity server */} - - - ); - } - - const threepidSection = this.state.haveIdServer ? ( - <> - - - - ) : null; - - return ( - <> - {threepidSection} - {/* has its own heading as it includes the current identity server */} - ); } @@ -520,58 +187,24 @@ export default class GeneralUserSettingsTab extends React.Component; - } - public render(): React.ReactNode { - const plaf = PlatformPeg.get(); - const supportsMultiLanguageSpellCheck = plaf?.supportsSpellCheckSettings(); - let accountManagementSection: JSX.Element | undefined; const isAccountManagedExternally = !!this.state.externalAccountManagementUrl; if (SettingsStore.getValue(UIFeature.Deactivate) && !isAccountManagedExternally) { accountManagementSection = this.renderManagementSection(); } - let discoverySection; - // :TCHAP: no need for users to edit the discovery section (TchapUIFeature.showEmailPhoneDiscoverySettings) - if (TchapUIFeature.showEmailPhoneDiscoverySettings && SettingsStore.getValue(UIFeature.IdentityServer)) { - const discoWarning = this.state.requiredPolicyInfo.hasTerms ? ( - - ) : null; - const heading = ( - - {discoWarning} - {_t("settings|general|discovery_section")} - - ); - discoverySection = ( - - {this.renderDiscoverySection()} - - ); - } - return ( - + + {this.renderAccountSection()} - {this.renderLanguageSection()} - {supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null} - {discoverySection} - {this.renderIntegrationManagerSection()} {accountManagementSection} ); diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 6df2a1a03c5..439cc2122fc 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -15,9 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useCallback, useEffect, useState } from "react"; -import { _t } from "../../../../../languageHandler"; +import { _t, getCurrentLanguage } from "../../../../../languageHandler"; import { UseCase } from "../../../../../settings/enums/UseCase"; import SettingsStore from "../../../../../settings/SettingsStore"; import Field from "../../../elements/Field"; @@ -33,6 +33,11 @@ import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingP import SettingsSubsection from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; +import LanguageDropdown from "../../../elements/LanguageDropdown"; +import PlatformPeg from "../../../../../PlatformPeg"; +import { IS_MAC } from "../../../../../Keyboard"; +import SpellCheckSettings from "../../SpellCheckSettings"; +import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; interface IProps { closeSettingsFn(success: boolean): void; @@ -44,6 +49,79 @@ interface IState { readMarkerOutOfViewThresholdMs: string; } +const LanguageSection: React.FC = () => { + const [language, setLanguage] = useState(getCurrentLanguage()); + + const onLanguageChange = useCallback( + (newLanguage: string) => { + if (language === newLanguage) return; + + SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage); + setLanguage(newLanguage); + const platform = PlatformPeg.get(); + if (platform) { + platform.setLanguage([newLanguage]); + platform.reload(); + } + }, + [language], + ); + + return ( +
+ {_t("settings|general|application_language")} + +
+ {_t("settings|general|application_language_reload_hint")} +
+
+ ); +}; + +const SpellCheckSection: React.FC = () => { + const [spellCheckEnabled, setSpellCheckEnabled] = useState(); + const [spellCheckLanguages, setSpellCheckLanguages] = useState(); + + useEffect(() => { + (async () => { + const plaf = PlatformPeg.get(); + const [enabled, langs] = await Promise.all([plaf?.getSpellCheckEnabled(), plaf?.getSpellCheckLanguages()]); + + setSpellCheckEnabled(enabled); + setSpellCheckLanguages(langs || undefined); + })(); + }, []); + + const onSpellCheckEnabledChange = useCallback((enabled: boolean) => { + setSpellCheckEnabled(enabled); + PlatformPeg.get()?.setSpellCheckEnabled(enabled); + }, []); + + const onSpellCheckLanguagesChange = useCallback((languages: string[]): void => { + setSpellCheckLanguages(languages); + PlatformPeg.get()?.setSpellCheckLanguages(languages); + }, []); + + if (!PlatformPeg.get()?.supportsSpellCheckSettings()) return null; + + return ( + <> + + {spellCheckEnabled && spellCheckLanguages !== undefined && !IS_MAC && ( + + )} + + ); +}; + export default class PreferencesUserSettingsTab extends React.Component { private static ROOM_LIST_SETTINGS = ["breadcrumbs", "FTUE.userOnboardingButton"]; @@ -146,6 +224,12 @@ export default class PreferencesUserSettingsTab extends React.Component + {/* The heading string is still 'general' from where it was moved, but this section should become 'general' */} + + + + + {roomListSettings.length > 0 && ( {this.renderGroup(roomListSettings)} diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 47b046c5fbe..94e1b11ea47 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -28,7 +28,6 @@ import { SettingLevel } from "../../../../../settings/SettingLevel"; import SecureBackupPanel from "../../SecureBackupPanel"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; -import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import { ActionPayload } from "../../../../../dispatcher/payloads"; import CryptographyPanel from "../../CryptographyPanel"; import SettingsFlag from "../../../elements/SettingsFlag"; @@ -43,6 +42,8 @@ import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; import { useOwnDevices } from "../../devices/useOwnDevices"; +import DiscoverySettings from "../../discovery/DiscoverySettings"; +import SetIntegrationManager from "../../SetIntegrationManager"; interface IIgnoredUserProps { userId: string; @@ -345,6 +346,7 @@ export default class SecurityUserSettingsTab extends React.Component + : null; // only show the section if there's something to show - if (ignoreUsersPanel || invitesPanel || e2ePanel) { + if (ignoreUsersPanel || invitesPanel) { advancedSection = ( {ignoreUsersPanel} {invitesPanel} - {e2ePanel} ); } @@ -385,6 +385,7 @@ export default class SecurityUserSettingsTab extends React.Component {crossSigning} diff --git a/src/components/views/spaces/QuickThemeSwitcher.tsx b/src/components/views/spaces/QuickThemeSwitcher.tsx index 66f3d2c040e..1a6ee738bac 100644 --- a/src/components/views/spaces/QuickThemeSwitcher.tsx +++ b/src/components/views/spaces/QuickThemeSwitcher.tsx @@ -20,13 +20,13 @@ import { _t } from "../../../languageHandler"; import { Action } from "../../../dispatcher/actions"; import { findNonHighContrastTheme, getOrderedThemes } from "../../../theme"; import Dropdown from "../elements/Dropdown"; -import ThemeChoicePanel from "../settings/ThemeChoicePanel"; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; import dis from "../../../dispatcher/dispatcher"; import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; import PosthogTrackers from "../../../PosthogTrackers"; import { NonEmptyArray } from "../../../@types/common"; +import { useTheme } from "../../../hooks/useTheme"; type Props = { requestClose: () => void; @@ -37,10 +37,10 @@ const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID"; const QuickThemeSwitcher: React.FC = ({ requestClose }) => { const orderedThemes = useMemo(getOrderedThemes, []); - const themeState = ThemeChoicePanel.calculateThemeState(); + const themeState = useTheme(); const nonHighContrast = findNonHighContrastTheme(themeState.theme); const theme = nonHighContrast ? nonHighContrast : themeState.theme; - const { useSystemTheme } = themeState; + const { systemThemeActivated } = themeState; const themeOptions = [ { @@ -50,7 +50,7 @@ const QuickThemeSwitcher: React.FC = ({ requestClose }) => { ...orderedThemes, ]; - const selectedTheme = useSystemTheme ? MATCH_SYSTEM_THEME_ID : theme; + const selectedTheme = systemThemeActivated ? MATCH_SYSTEM_THEME_ID : theme; const onOptionChange = async (newTheme: string): Promise => { PosthogTrackers.trackInteraction("WebQuickSettingsThemeDropdown"); diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 89ebed98eda..6e627196ac5 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -67,6 +67,8 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { ActionPayload } from "../../../dispatcher/payloads"; import { Action } from "../../../dispatcher/actions"; import { NotificationState } from "../../../stores/notifications/NotificationState"; +import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; +import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { ThreadsActivityCentre } from "./threads-activity-centre/"; @@ -74,6 +76,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import TchapUIFeature from "../../../../../../src/tchap/util/TchapUIFeature"; // :TCHAP: extend-remove-thread-buttons import TchapGaufre from "../../../../../../src/tchap/components/views/common/Gaufre"; import QuickFaqButton from "../../../../../../src/tchap/components/views/common/QuickFaq"; // :TCHAP: improve-faq-visibility +import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation"; import { KeyboardShortcut } from "../settings/KeyboardShortcut"; const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => { @@ -386,7 +389,22 @@ const SpacePanel: React.FC = () => { >
-
`; diff --git a/src/utils/exportUtils/exportCSS.ts b/src/utils/exportUtils/exportCSS.ts index bd7ddac01ba..15716ad5446 100644 --- a/src/utils/exportUtils/exportCSS.ts +++ b/src/utils/exportUtils/exportCSS.ts @@ -14,74 +14,80 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type { Rule, StyleSheet } from "css-tree"; + import customCSS from "!!raw-loader!./exportCustomCSS.css"; const cssSelectorTextClassesRegex = /\.[\w-]+/g; function mutateCssText(css: string): string { // replace used fonts so that we don't have to bundle Inter & Inconsalata + const sansFont = `-apple-system, BlinkMacSystemFont, avenir next, + avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`; return css - .replace( - /font-family: ?(Inter|'Inter'|"Inter")/g, - `font-family: -apple-system, BlinkMacSystemFont, avenir next, - avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`, - ) + .replace(/font-family: ?(Inter|'Inter'|"Inter")/g, `font-family: ${sansFont}`) + .replace(/--cpd-font-family-sans: ?(Inter|'Inter'|"Inter")/g, `--cpd-font-family-sans: ${sansFont}`) .replace( /font-family: ?Inconsolata/g, "font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace", ); } -function isLightTheme(sheet: CSSStyleSheet): boolean { - return (sheet.ownerNode)?.dataset.mxTheme?.toLowerCase() === "light"; -} - -async function getRulesFromCssFile(path: string): Promise { - const doc = document.implementation.createHTMLDocument(""); - const styleElement = document.createElement("style"); +function includeRule(rule: Rule, usedClasses: Set): boolean { + if (rule.prelude.type === "Raw") { + // cull empty rules + if (rule.block.children.isEmpty) return false; - const res = await fetch(path); - styleElement.textContent = await res.text(); - // the style will only be parsed once it is added to a document - doc.body.appendChild(styleElement); - - return styleElement.sheet!; + return rule.prelude.value.split(",").some((subselector) => { + const classes = subselector.trim().match(cssSelectorTextClassesRegex); + if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) { + return false; + } + return true; + }); + } + return true; } // naively culls unused css rules based on which classes are present in the html, // doesn't cull rules which won't apply due to the full selector not matching but gets rid of a LOT of cruft anyway. +// We cannot use document.styleSheets as it does not handle variables in shorthand properties sanely, +// see https://github.com/element-hq/element-web/issues/26761 const getExportCSS = async (usedClasses: Set): Promise => { - // only include bundle.css and the data-mx-theme=light styling - const stylesheets = Array.from(document.styleSheets).filter((s) => { - return s.href?.endsWith("bundle.css") || isLightTheme(s); - }); + const csstree = await import("css-tree"); - // If the light theme isn't loaded we will have to fetch & parse it manually - if (!stylesheets.some(isLightTheme)) { - const href = document.querySelector('link[rel="stylesheet"][href$="theme-light.css"]')?.href; - if (href) stylesheets.push(await getRulesFromCssFile(href)); - } + // only include bundle.css and light theme styling + const hrefs = ["bundle.css", "theme-light.css"].map((name) => { + return document.querySelector(`link[rel="stylesheet"][href$="${name}"]`)?.href; + }); let css = ""; - for (const stylesheet of stylesheets) { - for (const rule of stylesheet.cssRules) { - if (rule instanceof CSSFontFaceRule) continue; // we don't want to bundle any fonts - - const selectorText = (rule as CSSStyleRule).selectorText; - - // only skip the rule if all branches (,) of the selector are redundant - if ( - selectorText?.split(",").every((selector) => { - const classes = selector.match(cssSelectorTextClassesRegex); - if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) { - return true; // signal as a redundant selector - } - }) - ) { - continue; // skip this rule as it is redundant + + for (const href of hrefs) { + if (!href) continue; + const res = await fetch(href); + const text = await res.text(); + + const ast = csstree.parse(text, { + context: "stylesheet", + parseAtrulePrelude: false, + parseRulePrelude: false, + parseValue: false, + parseCustomProperty: false, + }) as StyleSheet; + + for (const rule of ast.children) { + if (rule.type === "Atrule") { + if (rule.name === "font-face") { + continue; + } + } + + if (rule.type === "Rule" && !includeRule(rule, usedClasses)) { + continue; } - css += mutateCssText(rule.cssText) + "\n"; + css += mutateCssText(csstree.generate(rule)); } } diff --git a/src/utils/exportUtils/exportCustomCSS.css b/src/utils/exportUtils/exportCustomCSS.css index 4807e316a86..bd0de642658 100644 --- a/src/utils/exportUtils/exportCustomCSS.css +++ b/src/utils/exportUtils/exportCustomCSS.css @@ -18,6 +18,11 @@ limitations under the License. This file is raw-imported (imported as plain text) for the export bundle, which is the reason for the .css format and the colours being hard-coded hard-coded. */ +html, +body { + font-size: var(--cpd-font-size-root) !important; +} + #snackbar { display: flex; visibility: hidden; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 53039cdc8f4..8f7446e491c 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -21,7 +21,6 @@ limitations under the License. * @param text the plaintext to put in the user's clipboard */ import { logger } from "matrix-js-sdk/src/logger"; -import GraphemeSplitter from "graphemer"; export async function copyPlaintext(text: string): Promise { try { @@ -85,6 +84,8 @@ export function getSelectedText(): string { return window.getSelection()!.toString(); } +export const graphemeSegmenter = new Intl.Segmenter(); + /** * Returns the first grapheme in the given string, * especially useful for strings containing emoji, will not break compound emoji up. @@ -92,7 +93,6 @@ export function getSelectedText(): string { * @returns the first grapheme or an empty string if given an empty string */ export function getFirstGrapheme(str: string): string { - const splitter = new GraphemeSplitter(); - const result = splitter.iterateGraphemes(str).next(); - return result.done ? "" : result.value; + const result = graphemeSegmenter.segment(str)[Symbol.iterator]().next(); + return result.done ? "" : result.value.segment; } diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index c36e3f75b3a..da45a50f88c 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -47,6 +47,7 @@ import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; import { createReconnectedListener } from "../../utils/connection"; import { localNotificationsAreSilenced } from "../../utils/notifications"; +import { BackgroundAudio } from "../../audio/BackgroundAudio"; export enum VoiceBroadcastRecordingEvent { StateChanged = "liveness_changed", @@ -75,6 +76,7 @@ export class VoiceBroadcastRecording private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; private roomId: string; private infoEventId: string; + private backgroundAudio = new BackgroundAudio(); /** * Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing. @@ -346,15 +348,7 @@ export class VoiceBroadcastRecording return; } - // Audio files are added to the document in Element Web. - // See
-
" diff --git a/test/utils/leave-behaviour-test.ts b/test/utils/leave-behaviour-test.ts index 48117caf48e..e5c9f820d9d 100644 --- a/test/utils/leave-behaviour-test.ts +++ b/test/utils/leave-behaviour-test.ts @@ -16,6 +16,7 @@ limitations under the License. import { mocked, Mocked } from "jest-mock"; import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { sleep } from "matrix-js-sdk/src/utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import { mkRoom, resetAsyncStoreWithClient, setupAsyncStoreWithClient, stubClient } from "../test-utils"; @@ -78,7 +79,7 @@ describe("leaveRoomBehaviour", () => { const expectDispatch = async (payload: T) => { const dispatcherSpy = jest.fn(); const dispatcherRef = defaultDispatcher.register(dispatcherSpy); - await new Promise((resolve) => setImmediate(resolve)); // Flush the dispatcher + await sleep(0); expect(dispatcherSpy).toHaveBeenCalledWith(payload); defaultDispatcher.unregister(dispatcherRef); }; diff --git a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts index 1298b1ff49e..cac187b6331 100644 --- a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts @@ -30,6 +30,7 @@ import { SyncState, } from "matrix-js-sdk/src/matrix"; import { EncryptedFile } from "matrix-js-sdk/src/types"; +import fetchMock from "fetch-mock-jest"; import { uploadFile } from "../../../src/ContentMessages"; import { createVoiceMessageContent } from "../../../src/utils/createVoiceMessageContent"; @@ -49,6 +50,7 @@ import { import { mkEvent, mkStubRoom, stubClient } from "../../test-utils"; import dis from "../../../src/dispatcher/dispatcher"; import { VoiceRecording } from "../../../src/audio/VoiceRecording"; +import { createAudioContext } from "../../../src/audio/compat"; jest.mock("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder", () => ({ ...(jest.requireActual("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object), @@ -79,6 +81,11 @@ jest.mock("../../../src/utils/createVoiceMessageContent", () => ({ createVoiceMessageContent: jest.fn(), })); +jest.mock("../../../src/audio/compat", () => ({ + ...jest.requireActual("../../../src/audio/compat"), + createAudioContext: jest.fn(), +})); + describe("VoiceBroadcastRecording", () => { const roomId = "!room:example.com"; const uploadedUrl = "mxc://example.com/vb"; @@ -198,6 +205,19 @@ describe("VoiceBroadcastRecording", () => { }); }; + const mockAudioBufferSourceNode = { + addEventListener: jest.fn(), + connect: jest.fn(), + start: jest.fn(), + }; + const mockAudioContext = { + decodeAudioData: jest.fn(), + suspend: jest.fn(), + resume: jest.fn(), + createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode), + currentTime: 1337, + }; + beforeEach(() => { client = stubClient(); room = mkStubRoom(roomId, "Test Room", client); @@ -265,6 +285,8 @@ describe("VoiceBroadcastRecording", () => { return null; }); + + mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext); }); afterEach(() => { @@ -546,12 +568,13 @@ describe("VoiceBroadcastRecording", () => { beforeEach(() => { mocked(client.sendMessage).mockRejectedValue("Error"); emitFirsChunkRecorded(); + fetchMock.get("media/error.mp3", 200); }); itShouldBeInState("connection_error"); it("should play a notification", () => { - expect(audioElement.play).toHaveBeenCalled(); + expect(mockAudioBufferSourceNode.start).toHaveBeenCalled(); }); describe("and the connection is back", () => { diff --git a/test/widgets/ManagedHybrid-test.ts b/test/widgets/ManagedHybrid-test.ts index b91db09dc19..05093ed0d4c 100644 --- a/test/widgets/ManagedHybrid-test.ts +++ b/test/widgets/ManagedHybrid-test.ts @@ -14,38 +14,91 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { isManagedHybridWidgetEnabled } from "../../src/widgets/ManagedHybrid"; -import DMRoomMap from "../../src/utils/DMRoomMap"; +import { Room } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import fetchMock from "fetch-mock-jest"; + +import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../src/widgets/ManagedHybrid"; import { stubClient } from "../test-utils"; import SdkConfig from "../../src/SdkConfig"; +import WidgetUtils from "../../src/utils/WidgetUtils"; +import { WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore"; + +jest.mock("../../src/utils/room/getJoinedNonFunctionalMembers", () => ({ + getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([1, 2]), +})); describe("isManagedHybridWidgetEnabled", () => { - let dmRoomMap: DMRoomMap; + let room: Room; beforeEach(() => { - stubClient(); - dmRoomMap = { - getUserIdForRoomId: jest.fn().mockReturnValue("@user:server"), - } as unknown as DMRoomMap; - DMRoomMap.setShared(dmRoomMap); + const client = stubClient(); + room = new Room("!room:server", client, client.getSafeUserId()); }); it("should return false if widget_build_url is unset", () => { - expect(isManagedHybridWidgetEnabled("!room:server")).toBeFalsy(); + expect(isManagedHybridWidgetEnabled(room)).toBeFalsy(); }); - it("should return true for DMs when widget_build_url_ignore_dm is unset", () => { + it("should return true for 1-1 rooms when widget_build_url_ignore_dm is unset", () => { SdkConfig.put({ widget_build_url: "https://url", }); - expect(isManagedHybridWidgetEnabled("!room:server")).toBeTruthy(); + expect(isManagedHybridWidgetEnabled(room)).toBeTruthy(); }); - it("should return false for DMs when widget_build_url_ignore_dm is true", () => { + it("should return false for 1-1 rooms when widget_build_url_ignore_dm is true", () => { SdkConfig.put({ widget_build_url: "https://url", widget_build_url_ignore_dm: true, }); - expect(isManagedHybridWidgetEnabled("!room:server")).toBeFalsy(); + expect(isManagedHybridWidgetEnabled(room)).toBeFalsy(); + }); +}); + +describe("addManagedHybridWidget", () => { + let room: Room; + + beforeEach(() => { + const client = stubClient(); + room = new Room("!room:server", client, client.getSafeUserId()); + }); + + it("should noop if user lacks permission", async () => { + const logSpy = jest.spyOn(logger, "error").mockImplementation(); + jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(false); + + fetchMock.mockClear(); + await addManagedHybridWidget(room); + expect(logSpy).toHaveBeenCalledWith("User not allowed to modify widgets in !room:server"); + expect(fetchMock).toHaveBeenCalledTimes(0); + }); + + it("should noop if no widget_build_url", async () => { + jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true); + + fetchMock.mockClear(); + await addManagedHybridWidget(room); + expect(fetchMock).toHaveBeenCalledTimes(0); + }); + + it("should add the widget successfully", async () => { + fetchMock.get("https://widget-build-url/?roomId=!room:server", { + widget_id: "WIDGET_ID", + widget: { key: "value" }, + }); + jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true); + jest.spyOn(WidgetLayoutStore.instance, "canCopyLayoutToRoom").mockReturnValue(true); + const setRoomWidgetContentSpy = jest.spyOn(WidgetUtils, "setRoomWidgetContent").mockResolvedValue(); + SdkConfig.put({ + widget_build_url: "https://widget-build-url", + }); + + await addManagedHybridWidget(room); + expect(fetchMock).toHaveBeenCalledWith("https://widget-build-url?roomId=!room:server"); + expect(setRoomWidgetContentSpy).toHaveBeenCalledWith(room.client, room.roomId, "WIDGET_ID", { + "key": "value", + "io.element.managed_hybrid": true, + }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 3d5f96e1c27..3118f598c4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,13 +6,13 @@ "esModuleInterop": true, "module": "es2022", "moduleResolution": "node", - "target": "es2016", + "target": "es2018", "noUnusedLocals": true, "sourceMap": false, "outDir": "./lib", "declaration": true, "jsx": "react", - "lib": ["es2021", "dom", "dom.iterable"], + "lib": ["es2022", "dom", "dom.iterable"], "strict": true }, "include": [ diff --git a/yarn.lock b/yarn.lock index 83117c15e91..4365b802256 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,10 +19,10 @@ resolved "https://registry.yarnpkg.com/@action-validator/core/-/core-0.6.0.tgz#8fbaf45562a5377140815b79cc1ac9f610ff63e5" integrity sha512-tPglwCr8Mm8SWzwnVewwFmqRx91F0WvMsM0BRAqH4CLalyGndm53Xvp+UcUSzswpk1wkjIDYI7RyEhWMLyPkig== -"@adobe/css-tools@^4.3.2": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" - integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== +"@adobe/css-tools@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== "@ampproject/remapping@^2.2.0": version "2.3.0" @@ -55,7 +55,7 @@ "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" chokidar "^3.4.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== @@ -63,7 +63,7 @@ "@babel/highlight" "^7.24.7" picocolors "^1.0.0" -"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13": +"@babel/code-frame@^7.10.4": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== @@ -71,7 +71,7 @@ "@babel/highlight" "^7.23.4" chalk "^2.4.2" -"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": +"@babel/code-frame@^7.12.13": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== @@ -79,19 +79,6 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/code-frame@^7.23.5": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" - integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== - dependencies: - "@babel/highlight" "^7.24.2" - picocolors "^1.0.0" - -"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.5": - version "7.21.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc" - integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== - "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" @@ -165,17 +152,7 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.3.tgz#86e6e83d95903fbe7613f448613b8b319f330a8e" - integrity sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg== - dependencies: - "@babel/types" "^7.23.3" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.24.7": +"@babel/generator@^7.23.3", "@babel/generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== @@ -185,13 +162,6 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== - dependencies: - "@babel/types" "^7.22.5" - "@babel/helper-annotate-as-pure@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" @@ -207,17 +177,6 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-compilation-targets@^7.20.7": - version "7.21.5" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366" - integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w== - dependencies: - "@babel/compat-data" "^7.21.5" - "@babel/helper-validator-option" "^7.21.0" - browserslist "^4.21.3" - lru-cache "^5.1.1" - semver "^6.3.0" - "@babel/helper-compilation-targets@^7.22.15": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" @@ -240,21 +199,6 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.18.6": - version "7.21.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.8.tgz#205b26330258625ef8869672ebca1e0dee5a0f02" - integrity sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.21.5" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.5" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.21.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - semver "^6.3.0" - "@babel/helper-create-class-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" @@ -290,27 +234,14 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.21.5", "@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-environment-visitor@^7.24.7": +"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== dependencies: "@babel/types" "^7.24.7" -"@babel/helper-function-name@^7.21.0", "@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-function-name@^7.24.7": +"@babel/helper-function-name@^7.23.0", "@babel/helper-function-name@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== @@ -318,27 +249,13 @@ "@babel/template" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-hoist-variables@^7.24.7": +"@babel/helper-hoist-variables@^7.22.5", "@babel/helper-hoist-variables@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== dependencies: "@babel/types" "^7.24.7" -"@babel/helper-member-expression-to-functions@^7.21.5": - version "7.21.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.5.tgz#3b1a009af932e586af77c1030fba9ee0bde396c0" - integrity sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg== - dependencies: - "@babel/types" "^7.21.5" - "@babel/helper-member-expression-to-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz#67613d068615a70e4ed5101099affc7a41c5225f" @@ -384,13 +301,6 @@ "@babel/helper-split-export-declaration" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - "@babel/helper-optimise-call-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" @@ -403,11 +313,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== -"@babel/helper-plugin-utils@^7.20.2": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== - "@babel/helper-remap-async-to-generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" @@ -417,18 +322,6 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-wrap-function" "^7.24.7" -"@babel/helper-replace-supers@^7.21.5": - version "7.21.5" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.21.5.tgz#a6ad005ba1c7d9bc2973dfde05a1bba7065dde3c" - integrity sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg== - dependencies: - "@babel/helper-environment-visitor" "^7.21.5" - "@babel/helper-member-expression-to-functions" "^7.21.5" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.5" - "@babel/types" "^7.21.5" - "@babel/helper-replace-supers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" @@ -453,13 +346,6 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== - dependencies: - "@babel/types" "^7.20.0" - "@babel/helper-skip-transparent-expression-wrappers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" @@ -468,52 +354,23 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-split-export-declaration@^7.18.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-split-export-declaration@^7.22.6": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz#b9a67f06a46b0b339323617c8c6213b9055a78b6" - integrity sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q== - dependencies: - "@babel/types" "^7.24.5" - -"@babel/helper-split-export-declaration@^7.24.7": +"@babel/helper-split-export-declaration@^7.22.6", "@babel/helper-split-export-declaration@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== dependencies: "@babel/types" "^7.24.7" -"@babel/helper-string-parser@^7.22.5": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" - integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== - -"@babel/helper-string-parser@^7.24.1", "@babel/helper-string-parser@^7.24.7": +"@babel/helper-string-parser@^7.22.5", "@babel/helper-string-parser@^7.24.1", "@babel/helper-string-parser@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== -"@babel/helper-validator-identifier@^7.22.20": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62" - integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA== - -"@babel/helper-validator-identifier@^7.24.5", "@babel/helper-validator-identifier@^7.24.7": +"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.24.5", "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== -"@babel/helper-validator-option@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" - integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== - "@babel/helper-validator-option@^7.23.5", "@babel/helper-validator-option@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" @@ -555,17 +412,7 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/highlight@^7.23.4": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" - integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== - dependencies: - "@babel/helper-validator-identifier" "^7.22.20" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/highlight@^7.24.2", "@babel/highlight@^7.24.7": +"@babel/highlight@^7.23.4", "@babel/highlight@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== @@ -575,7 +422,7 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.5", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.22.16", "@babel/parser@^7.23.3", "@babel/parser@^7.24.0", "@babel/parser@^7.24.7": +"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.5", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.22.16", "@babel/parser@^7.23.3", "@babel/parser@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== @@ -612,14 +459,6 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-proposal-class-properties@^7.12.1": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-proposal-export-default-from@^7.12.1": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.7.tgz#0b539c46b8ac804f694e338f803c8354c0f788b6" @@ -628,25 +467,6 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-default-from" "^7.24.7" -"@babel/plugin-proposal-numeric-separator@^7.12.7": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.12.1": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" @@ -861,7 +681,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-class-properties@^7.24.7": +"@babel/plugin-transform-class-properties@^7.12.1", "@babel/plugin-transform-class-properties@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== @@ -1051,7 +871,7 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.24.7": +"@babel/plugin-transform-numeric-separator@^7.12.7", "@babel/plugin-transform-numeric-separator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== @@ -1059,7 +879,7 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.24.7": +"@babel/plugin-transform-object-rest-spread@^7.12.1", "@babel/plugin-transform-object-rest-spread@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== @@ -1094,13 +914,6 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-parameters@^7.20.7": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz#18fc4e797cf6d6d972cb8c411dbe8a809fa157db" - integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-transform-parameters@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" @@ -1405,7 +1218,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== @@ -1413,31 +1226,13 @@ regenerator-runtime "^0.14.0" "@babel/runtime@^7.13.10": - version "7.24.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e" - integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" + integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/template@^7.22.15": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" - integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== - dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/parser" "^7.24.0" - "@babel/types" "^7.24.0" - -"@babel/template@^7.24.7": +"@babel/template@^7.22.15", "@babel/template@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== @@ -1455,23 +1250,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.18.5", "@babel/traverse@^7.21.5", "@babel/traverse@^7.22.15", "@babel/traverse@^7.22.20": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.3.tgz#26ee5f252e725aa7aca3474aa5b324eaf7908b5b" - integrity sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.3" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.3" - "@babel/types" "^7.23.3" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.24.7": +"@babel/traverse@^7.18.5", "@babel/traverse@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== @@ -1487,7 +1266,23 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.5", "@babel/types@^7.3.3": +"@babel/traverse@^7.22.15", "@babel/traverse@^7.22.20": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.3.tgz#26ee5f252e725aa7aca3474aa5b324eaf7908b5b" + integrity sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.3" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.3" + "@babel/types" "^7.23.3" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.3.3": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== @@ -1496,7 +1291,7 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.23.0", "@babel/types@^7.24.0", "@babel/types@^7.24.5": +"@babel/types@^7.22.15", "@babel/types@^7.22.19": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.5.tgz#7661930afc638a5383eb0c4aee59b74f38db84d7" integrity sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ== @@ -1505,7 +1300,7 @@ "@babel/helper-validator-identifier" "^7.24.5" to-fast-properties "^2.0.0" -"@babel/types@^7.22.5", "@babel/types@^7.24.7", "@babel/types@^7.4.4": +"@babel/types@^7.23.0", "@babel/types@^7.23.3", "@babel/types@^7.24.0", "@babel/types@^7.24.5", "@babel/types@^7.24.7", "@babel/types@^7.4.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q== @@ -1514,15 +1309,6 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" -"@babel/types@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.3.tgz#d5ea892c07f2ec371ac704420f4dcdb07b5f9598" - integrity sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1569,11 +1355,6 @@ resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b" integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== -"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" - integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== - "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1582,9 +1363,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.10.0": - version "4.10.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.1.tgz#361461e5cb3845d874e61731c11cfedd664d83a0" - integrity sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA== + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== "@eslint-community/regexpp@^4.6.1": version "4.8.0" @@ -1626,41 +1407,41 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@floating-ui/core@^1.0.0": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.2.tgz#d37f3e0ac1f1c756c7de45db13303a266226851a" - integrity sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg== +"@floating-ui/core@^1.6.0": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.4.tgz#0140cf5091c8dee602bff9da5ab330840ff91df6" + integrity sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA== dependencies: - "@floating-ui/utils" "^0.2.0" + "@floating-ui/utils" "^0.2.4" "@floating-ui/dom@^1.0.0": - version "1.6.5" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9" - integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw== + version "1.6.7" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.7.tgz#85d22f731fcc5b209db504478fb1df5116a83015" + integrity sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng== dependencies: - "@floating-ui/core" "^1.0.0" - "@floating-ui/utils" "^0.2.0" + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.4" -"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.0.8", "@floating-ui/react-dom@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff" - integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA== +"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.0.8": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0" + integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg== dependencies: "@floating-ui/dom" "^1.0.0" -"@floating-ui/react@^0.26.9": - version "0.26.16" - resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.16.tgz#3415a087f452165161c2d313d1d57e8142894679" - integrity sha512-HEf43zxZNAI/E781QIVpYSF3K2VH4TTYZpqecjdsFkjsaU1EbaWcM++kw0HXFffj7gDUcBFevX8s0rQGQpxkow== +"@floating-ui/react@0.26.11", "@floating-ui/react@^0.26.9": + version "0.26.11" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.11.tgz#226d3fec890de439443b62f3138ef7de052b0998" + integrity sha512-fo01Cu+jzLDVG/AYAV2OtV6flhXvxP5rDaR1Fk8WWhtsFqwk478Dr2HGtB8s0HqQCsFWVbdHYpPjMiQiR/A9VA== dependencies: - "@floating-ui/react-dom" "^2.1.0" + "@floating-ui/react-dom" "^2.0.0" "@floating-ui/utils" "^0.2.0" tabbable "^6.0.0" -"@floating-ui/utils@^0.2.0": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" - integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.4.tgz#1d459cee5031893a08a0e064c406ad2130cced7c" + integrity sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA== "@humanwhocodes/config-array@^0.11.14": version "0.11.14" @@ -1999,10 +1780,10 @@ resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== -"@matrix-org/analytics-events@^0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.21.0.tgz#1de19a6a765f179c01199e72c9c461dc7120fe1a" - integrity sha512-K0E9eje03o3pYc8C93XFTu6DTgNdsVNvdkH7rsFGiHkc15WQybKFyHR7quuuV42jrzGINWpFou0faCWcDBdNbQ== +"@matrix-org/analytics-events@^0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.24.0.tgz#21a64537ac975b18e1eb13d9fd0bdc7d448a6039" + integrity sha512-3FDdtqZ+5cMqVffWjFNOIQ7RDFN6XS11kqdtN2ps8uvq5ce8gT0yXQvK37WeKWKZZ5QAKeoMzGhud+lsVcb1xg== "@matrix-org/emojibase-bindings@^1.1.2": version "1.1.3" @@ -2012,15 +1793,15 @@ emojibase "^15.0.0" emojibase-data "^15.0.0" -"@matrix-org/matrix-sdk-crypto-wasm@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-5.0.0.tgz#f45a7bccaad218c05bcf9e7c8ca783c9d9a07af4" - integrity sha512-37ASjCKSTU5ycGfkP+LUXG4Ok6OAf6vE+1qU6uwWhe6FwadCS3vVWzJYd/3d9BQFwsx4GhFTIAXrW4iLG85rmQ== +"@matrix-org/matrix-sdk-crypto-wasm@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-6.1.0.tgz#1cedf2bcbd6795e297fd45ea4a33f2c8c5204fdd" + integrity sha512-8Wn4TT9PEJswfE8+6mA60JHrxyiWYXfM4EM5800tLz+Rl9QRGk9JDF0o0cTb26v6bfXTa3/pCGWAkUVk0ROPEw== -"@matrix-org/matrix-wysiwyg@2.17.0": - version "2.17.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.17.0.tgz#68c83da17826fb43828f0c1ddd8d6e0b9d155ae5" - integrity sha512-PZGSrNqKCSdUnyUVglEvHrV8uowU3JuWUlYYKBslYnnIrJHw9aS2nnCpLVqwACFD6N82+L+Net8ME9i3qy7BGQ== +"@matrix-org/matrix-wysiwyg@2.37.4": + version "2.37.4" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.37.4.tgz#bd9b46051a21c9986477e3a83a1417b1ee926d81" + integrity sha512-4OtBWAHNAOu9P5C6jOIeHlu4ChwV2YusxnbGuN20IceC4bT2h38flZQgm0x9/jgHfF0LwnKUwKXsxtRoq8xW0g== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -2035,9 +1816,9 @@ "@babel/runtime" "^7.17.9" "@matrix-org/spec@^1.7.0": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@matrix-org/spec/-/spec-1.10.1.tgz#6c62a7ee4524224c29fe39e67b33983e70c99944" - integrity sha512-ryNSzJkaJi/fwp6AQ6ujS9oqJtw60e+/+llIzwJhbuWTr1V07B6KfUA44+bFrudIFmfghdOuxRfzUnWOQ2K6gw== + version "1.11.0" + resolved "https://registry.yarnpkg.com/@matrix-org/spec/-/spec-1.11.0.tgz#73864eab965c81a4c8c48ca0da8c9d94e8efd5dc" + integrity sha512-80000pCXpUnt3ue910uZY70kLo9b7pNfrUrlXY5smpDjfycEs1oztUAriPAnKxMp31gUqt9/tfjmBM2H/LNCZw== "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" @@ -2089,15 +1870,15 @@ tslib "^2.0.0" "@peculiar/webcrypto@^1.4.3": - version "1.4.6" - resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.4.6.tgz#607af294c4f205efeeb172aa32cb20024fe4aecf" - integrity sha512-YBcMfqNSwn3SujUJvAaySy5tlYbYm6tVt9SKoXu8BaTdKGROiJDgPR3TXpZdAKUfklzm3lRapJEAltiMQtBgZg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz#9e57174c02c1291051c553600347e12b81469e10" + integrity sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg== dependencies: "@peculiar/asn1-schema" "^2.3.8" "@peculiar/json-schema" "^1.1.12" pvtsutils "^1.3.5" tslib "^2.6.2" - webcrypto-core "^1.7.9" + webcrypto-core "^1.8.0" "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -2105,11 +1886,11 @@ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@playwright/test@^1.40.1": - version "1.44.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.44.1.tgz#cc874ec31342479ad99838040e99b5f604299bcb" - integrity sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q== + version "1.45.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.45.2.tgz#e1b8512e20916720de1c5f5e89a362a252ea78ca" + integrity sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ== dependencies: - playwright "1.44.1" + playwright "1.45.2" "@radix-ui/primitive@1.0.1": version "1.0.1" @@ -2118,24 +1899,27 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-arrow@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" - integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA== +"@radix-ui/primitive@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" + integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== + +"@radix-ui/react-arrow@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a" + integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-collection@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" - integrity sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA== +"@radix-ui/react-collection@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed" + integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" "@radix-ui/react-compose-refs@1.0.1": version "1.0.1" @@ -2144,18 +1928,22 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-compose-refs@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" + integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== + "@radix-ui/react-context-menu@^2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.1.5.tgz#1bdbd72761439f9166f75dc4598f276265785c83" - integrity sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g== + version "2.2.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.1.tgz#a2c7812336a40cd22900c888336ad6e1adc6a1bc" + integrity sha512-wvMKKIeb3eOrkJ96s722vcidZ+2ZNfcYZWBPRHIB1VWrF+fiF851Io6LX0kmK5wTDQFKdulCCKJk2c3SBaQHvA== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-menu" "2.0.6" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-menu" "2.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-context@1.0.1": version "1.0.1" @@ -2164,76 +1952,73 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-context@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" + integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== + "@radix-ui/react-dialog@^1.0.4": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" - integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-focus-guards" "1.0.1" - "@radix-ui/react-focus-scope" "1.0.4" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-controllable-state" "1.0.1" + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz#4906507f7b4ad31e22d7dad69d9330c87c431d44" + integrity sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.5" + react-remove-scroll "2.5.7" -"@radix-ui/react-direction@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" - integrity sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA== - dependencies: - "@babel/runtime" "^7.13.10" +"@radix-ui/react-direction@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" + integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== -"@radix-ui/react-dismissable-layer@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" - integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== +"@radix-ui/react-dismissable-layer@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e" + integrity sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-escape-keydown" "1.0.3" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" "@radix-ui/react-dropdown-menu@^2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63" - integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-menu" "2.0.6" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-controllable-state" "1.0.1" - -"@radix-ui/react-focus-guards@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" - integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== - dependencies: - "@babel/runtime" "^7.13.10" + version "2.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz#3dc578488688250dbbe109d9ff2ca28a9bca27ec" + integrity sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-menu" "2.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-focus-guards@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13" + integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw== -"@radix-ui/react-focus-scope@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" - integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== +"@radix-ui/react-focus-scope@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" + integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-form@^0.0.3": version "0.0.3" @@ -2248,13 +2033,12 @@ "@radix-ui/react-label" "2.0.2" "@radix-ui/react-primitive" "1.0.3" -"@radix-ui/react-id@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" - integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ== +"@radix-ui/react-id@1.0.1", "@radix-ui/react-id@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" + integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.1.0" "@radix-ui/react-label@2.0.2": version "2.0.2" @@ -2264,64 +2048,61 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" -"@radix-ui/react-menu@2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e" - integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-collection" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-focus-guards" "1.0.1" - "@radix-ui/react-focus-scope" "1.0.4" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-popper" "1.1.3" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-roving-focus" "1.0.4" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-menu@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.1.tgz#bd623ace0e1ae1ac78023a505fec0541d59fb346" + integrity sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.0" + "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-roving-focus" "1.1.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-callback-ref" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.5" + react-remove-scroll "2.5.7" -"@radix-ui/react-popper@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" - integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== +"@radix-ui/react-popper@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a" + integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg== dependencies: - "@babel/runtime" "^7.13.10" "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-layout-effect" "1.0.1" - "@radix-ui/react-use-rect" "1.0.1" - "@radix-ui/react-use-size" "1.0.1" - "@radix-ui/rect" "1.0.1" - -"@radix-ui/react-portal@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" - integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + "@radix-ui/react-arrow" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-portal@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f" + integrity sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/react-presence@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" - integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg== +"@radix-ui/react-presence@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478" + integrity sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" "@radix-ui/react-primitive@1.0.3": version "1.0.3" @@ -2331,31 +2112,44 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.2" -"@radix-ui/react-roving-focus@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" - integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ== +"@radix-ui/react-primitive@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" + integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-collection" "1.0.3" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-callback-ref" "1.0.1" - "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-slot" "1.1.0" + +"@radix-ui/react-progress@^1.0.3": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2" + integrity sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg== + dependencies: + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + +"@radix-ui/react-roving-focus@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" + integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-separator@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.0.3.tgz#be5a931a543d5726336b112f465f58585c04c8aa" - integrity sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz#ee0f4d86003b0e3ea7bc6ccab01ea0adee32663e" + integrity sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": +"@radix-ui/react-slot@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== @@ -2363,167 +2157,151 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.0.2": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" + integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-tooltip@^1.0.6": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e" - integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.2.tgz#c42db2ffd7dcc6ff3d65407c8cb70490288f518d" + integrity sha512-9XRsLwe6Yb9B/tlnYCPVUd/TFS4J7HuOZW345DCeC6vKIxQGMZdx21RK4VoZauPD5frgkXTYVS5y90L+3YBn4w== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.0" + "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-visually-hidden" "1.1.0" + +"@radix-ui/react-use-callback-ref@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" + integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== + +"@radix-ui/react-use-controllable-state@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" + integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-popper" "1.1.3" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-controllable-state" "1.0.1" - "@radix-ui/react-visually-hidden" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.1.0" -"@radix-ui/react-use-callback-ref@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" - integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ== +"@radix-ui/react-use-escape-keydown@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" + integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== dependencies: - "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.1.0" -"@radix-ui/react-use-controllable-state@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286" - integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA== +"@radix-ui/react-use-layout-effect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" + integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== + +"@radix-ui/react-use-rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" + integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/rect" "1.1.0" -"@radix-ui/react-use-escape-keydown@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" - integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== +"@radix-ui/react-use-size@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" + integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/react-use-layout-effect@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" - integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ== +"@radix-ui/react-visually-hidden@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz#ad47a8572580f7034b3807c8e6740cd41038a5a2" + integrity sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ== dependencies: - "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-use-rect@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" - integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw== +"@radix-ui/rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" + integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== + +"@sentry-internal/browser-utils@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.16.0.tgz#182931f169a586dde50cf255237b129aad00dde7" + integrity sha512-40lzNy5F6dUFCN85AGThBxHPQLSwoNhZM2hWqhAR5rZ3Yed0uBaKlm4aNJCeeUB9l4kd0sH0In+i9Nqu6TGKrw== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/rect" "1.0.1" + "@sentry/core" "8.16.0" + "@sentry/types" "8.16.0" + "@sentry/utils" "8.16.0" -"@radix-ui/react-use-size@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" - integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g== +"@sentry-internal/feedback@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.16.0.tgz#dc8a3b807a37d0df136e62937e87ac23ce2ce6a8" + integrity sha512-BmRazZKl6iiVSg6eybUNOI1ve4eZqYpJYjkX48Jedn+7iZg7z12MNYl6IWPFBcN+sg+clf4wiKDr/SYS0yNemQ== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-use-layout-effect" "1.0.1" + "@sentry/core" "8.16.0" + "@sentry/types" "8.16.0" + "@sentry/utils" "8.16.0" -"@radix-ui/react-visually-hidden@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac" - integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA== +"@sentry-internal/replay-canvas@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.16.0.tgz#c6501dd9f7e5dac1399978cc9e2797eb281a8f70" + integrity sha512-Bjh6pCDLZIPAPU2dNvJfI7BQV16rsRtYcylJgkGamjf8IcaBu7r/Whsvt1q34xO29xc0ISlp+0xG+YAdN1690Q== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/react-primitive" "1.0.3" + "@sentry-internal/replay" "8.16.0" + "@sentry/core" "8.16.0" + "@sentry/types" "8.16.0" + "@sentry/utils" "8.16.0" -"@radix-ui/rect@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f" - integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ== +"@sentry-internal/replay@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.16.0.tgz#5bf564d7293d4fb4993327567e9ad12079ceb951" + integrity sha512-JT/wmYU2JPtl8Ldl9oml/25Yz6C5wG+SpylDeUx4mPh728E/iI9vesIc2652J/0xots/DZXe4K6K5nYjdFtEcQ== dependencies: - "@babel/runtime" "^7.13.10" + "@sentry-internal/browser-utils" "8.16.0" + "@sentry/core" "8.16.0" + "@sentry/types" "8.16.0" + "@sentry/utils" "8.16.0" + +"@sentry/browser@^8.0.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.16.0.tgz#af9b7b7556198d6de03cbc41b7abb5a16ecfc342" + integrity sha512-8Fxmk2aFWRixi2IKixiJR10Du34yb13HYr2iRw1haPKb5ZKa6CFA+XAnSzwpPZxO0RSHuPQR06YNkXaQ8fRAQQ== + dependencies: + "@sentry-internal/browser-utils" "8.16.0" + "@sentry-internal/feedback" "8.16.0" + "@sentry-internal/replay" "8.16.0" + "@sentry-internal/replay-canvas" "8.16.0" + "@sentry/core" "8.16.0" + "@sentry/types" "8.16.0" + "@sentry/utils" "8.16.0" + +"@sentry/core@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.16.0.tgz#cf2f4e572240983ec7e9fa083cc1ffce3147f20b" + integrity sha512-l9mQgm5OqnykvZMh6PmJ/9ygW4qLyEFop+pQH/uM5zQCZQvEa7rvAd9QXKHdbVKq1CxJa/nJiByc8wPWxsftGQ== + dependencies: + "@sentry/types" "8.16.0" + "@sentry/utils" "8.16.0" -"@sentry-internal/feedback@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.116.0.tgz#f1352b1a0d5fd7b7167775330ccf03bcc1b7892b" - integrity sha512-tmfO+RTCrhIWMs3yg8X0axhbjWRZLsldSfoXBgfjNCk/XwkYiVGp7WnYVbb+IO+01mHCsis9uaYOBggLgFRB5Q== - dependencies: - "@sentry/core" "7.116.0" - "@sentry/types" "7.116.0" - "@sentry/utils" "7.116.0" - -"@sentry-internal/replay-canvas@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.116.0.tgz#1cd4a85f99dd3cd61120e087232f5cbea21d5eb2" - integrity sha512-Sy0ydY7A97JY/IFTIj8U25kHqR5rL9oBk3HFE5EK9Phw56irVhHzEwLWae0jlFeCQEWoBYqpPgO5vXsaYzrWvw== - dependencies: - "@sentry/core" "7.116.0" - "@sentry/replay" "7.116.0" - "@sentry/types" "7.116.0" - "@sentry/utils" "7.116.0" - -"@sentry-internal/tracing@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.116.0.tgz#af3e4e264c440aa5525b5877a10b9a0f870b40e3" - integrity sha512-y5ppEmoOlfr77c/HqsEXR72092qmGYS4QE5gSz5UZFn9CiinEwGfEorcg2xIrrCuU7Ry/ZU2VLz9q3xd04drRA== - dependencies: - "@sentry/core" "7.116.0" - "@sentry/types" "7.116.0" - "@sentry/utils" "7.116.0" - -"@sentry/browser@^7.0.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.116.0.tgz#950c1a9672bf886c556c2c7b9198b90189e3f0c2" - integrity sha512-2aosATT5qE+QLKgTmyF9t5Emsluy1MBczYNuPmLhDxGNfB+MA86S8u7Hb0CpxdwjS0nt14gmbiOtJHoeAF3uTw== - dependencies: - "@sentry-internal/feedback" "7.116.0" - "@sentry-internal/replay-canvas" "7.116.0" - "@sentry-internal/tracing" "7.116.0" - "@sentry/core" "7.116.0" - "@sentry/integrations" "7.116.0" - "@sentry/replay" "7.116.0" - "@sentry/types" "7.116.0" - "@sentry/utils" "7.116.0" - -"@sentry/core@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.116.0.tgz#7cff43134878a696b2b3b981ae384ec3db9ac8c3" - integrity sha512-J6Wmjjx+o7RwST0weTU1KaKUAlzbc8MGkJV1rcHM9xjNTWTva+nrcCM3vFBagnk2Gm/zhwv3h0PvWEqVyp3U1Q== - dependencies: - "@sentry/types" "7.116.0" - "@sentry/utils" "7.116.0" - -"@sentry/integrations@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.116.0.tgz#b641342249da76cd2feb2fb5511424b66f967449" - integrity sha512-UZb60gaF+7veh1Yv79RiGvgGYOnU6xA97H+hI6tKgc1uT20YpItO4X56Vhp0lvyEyUGFZzBRRH1jpMDPNGPkqw== - dependencies: - "@sentry/core" "7.116.0" - "@sentry/types" "7.116.0" - "@sentry/utils" "7.116.0" - localforage "^1.8.1" - -"@sentry/replay@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.116.0.tgz#cde921133c8927be92d60baf03c2b0aea73380f1" - integrity sha512-OrpDtV54pmwZuKp3g7PDiJg6ruRMJKOCzK08TF7IPsKrr4x4UQn56rzMOiABVuTjuS8lNfAWDar6c6vxXFz5KA== - dependencies: - "@sentry-internal/tracing" "7.116.0" - "@sentry/core" "7.116.0" - "@sentry/types" "7.116.0" - "@sentry/utils" "7.116.0" - -"@sentry/types@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.116.0.tgz#0be3434e7e53c86db4993e668af1c3a65bfb7519" - integrity sha512-QCCvG5QuQrwgKzV11lolNQPP2k67Q6HHD9vllZ/C4dkxkjoIym8Gy+1OgAN3wjsR0f/kG9o5iZyglgNpUVRapQ== - -"@sentry/utils@7.116.0": - version "7.116.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.116.0.tgz#f32463ab10f76f464274233a9df202e5357d17ff" - integrity sha512-Vn9fcvwTq91wJvCd7WTMWozimqMi+dEZ3ie3EICELC2diONcN16ADFdzn65CQQbYwmUzRjN9EjDN2k41pKZWhQ== - dependencies: - "@sentry/types" "7.116.0" +"@sentry/types@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.16.0.tgz#a9ae39cffd50a0bdba0556a1596fb135d035cf26" + integrity sha512-cIRsn7gWGVaWHgCniBWA0N8PNwzDYibhjyjPRTMxUjuZCT37i7zxByKKmd9u4TpRIJ64MyirNyM0O6T0A26fpg== + +"@sentry/utils@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.16.0.tgz#5d1c9fb6cd562660b507c6647e6437282bef939a" + integrity sha512-tltCf2DVzz5TiYjxu/Rxbc9Qmm04893MFshV97jOTBcQeO2AAZBEl5rAoTCv1P08y7Yg+KiVwCx9Zj2x5U80/g== + dependencies: + "@sentry/types" "8.16.0" "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -2544,98 +2322,6 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@storybook/channels@8.1.5": - version "8.1.5" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-8.1.5.tgz#d00d033d318cf202ece1de728e55e85f82242e74" - integrity sha512-R+puP4tWYzQUbpIp8sX6U5oI+ZUevVOaFxXGaAN3PRXjIRC38oKTVWzj/G6GdziVFzN6rDn+JsYPmiRMYo1sYg== - dependencies: - "@storybook/client-logger" "8.1.5" - "@storybook/core-events" "8.1.5" - "@storybook/global" "^5.0.0" - telejson "^7.2.0" - tiny-invariant "^1.3.1" - -"@storybook/client-logger@8.1.5": - version "8.1.5" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-8.1.5.tgz#aa4a6ce4ca46fdfe12539e571f9059a479c8ae43" - integrity sha512-zd+aENXnOHsxBATppELmhw/UywLzCxQjz/8i/xkUjeTRB4Ggp0hJlOUdJUEdIJz631ydyytfvM70ktBj9gMl1w== - dependencies: - "@storybook/global" "^5.0.0" - -"@storybook/core-events@8.1.5": - version "8.1.5" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-8.1.5.tgz#d921984e12b27aaaa623499a7ac0c3eea5e96264" - integrity sha512-fgwbrHoLtSX6kfmamTGJqD+KfuEgun8cc4mWKZK094ByaqbSjhnOyeYO1sfVk8qst7QTFlOfhLAUe4cz1z149A== - dependencies: - "@storybook/csf" "^0.1.7" - ts-dedent "^2.0.0" - -"@storybook/csf@^0.1.7": - version "0.1.7" - resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.7.tgz#dcc6c16a353bc09c8c619ba1a23ba93b2aab0b9d" - integrity sha512-53JeLZBibjQxi0Ep+/AJTfxlofJlxy1jXcSKENlnKxHjWEYyHQCumMP5yTFjf7vhNnMjEpV3zx6t23ssFiGRyw== - dependencies: - type-fest "^2.19.0" - -"@storybook/global@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" - integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== - -"@storybook/icons@^1.2.5": - version "1.2.9" - resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-1.2.9.tgz#bb4a51a79e186b62e2dd0e04928b8617ac573838" - integrity sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg== - -"@storybook/manager-api@^8.1.1": - version "8.1.5" - resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-8.1.5.tgz#1f1a8875cbc19fad5435f670943207158dc76551" - integrity sha512-iVP7FOKDf9L7zWCb8C2XeZjWSILS3hHeNwILvd9YSX9dg9du41kJYahsAHxDCR/jp/gv0ZM/V0vuHzi+naVPkQ== - dependencies: - "@storybook/channels" "8.1.5" - "@storybook/client-logger" "8.1.5" - "@storybook/core-events" "8.1.5" - "@storybook/csf" "^0.1.7" - "@storybook/global" "^5.0.0" - "@storybook/icons" "^1.2.5" - "@storybook/router" "8.1.5" - "@storybook/theming" "8.1.5" - "@storybook/types" "8.1.5" - dequal "^2.0.2" - lodash "^4.17.21" - memoizerific "^1.11.3" - store2 "^2.14.2" - telejson "^7.2.0" - ts-dedent "^2.0.0" - -"@storybook/router@8.1.5": - version "8.1.5" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-8.1.5.tgz#e1dd831136e874df833286fd76554958af6132fa" - integrity sha512-DCwvAswlbLhQu6REPV04XNRhtPvsrRqHjMHKzjlfs+qYJWY7Egkofy05qlegqjkMDve33czfnRGBm0C16IydkA== - dependencies: - "@storybook/client-logger" "8.1.5" - memoizerific "^1.11.3" - qs "^6.10.0" - -"@storybook/theming@8.1.5": - version "8.1.5" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.1.5.tgz#8eb0718907ec443cfca1b73491f5e99df65930af" - integrity sha512-E4z1t49fMbVvd/t2MSL0Ecp5zbqsU/QfWBX/eorJ+m+Xc9skkwwG5qf/FnP9x4RZ9KaX8U8+862t0eafVvf4Tw== - dependencies: - "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" - "@storybook/client-logger" "8.1.5" - "@storybook/global" "^5.0.0" - memoizerific "^1.11.3" - -"@storybook/types@8.1.5": - version "8.1.5" - resolved "https://registry.yarnpkg.com/@storybook/types/-/types-8.1.5.tgz#627cac55e8034deed4b763327ff938c84c541a05" - integrity sha512-/PfAZh1xtXN2MvAZZKpiL/nPkC3bZj8BQ7P7z5a/aQarP+y7qdXuoitYQ6oOH3rkaiYywmkWzA/y4iW70KXLKg== - dependencies: - "@storybook/channels" "8.1.5" - "@types/express" "^4.7.0" - file-system-cache "2.3.0" - "@testing-library/dom@^8.0.0": version "8.20.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6" @@ -2665,11 +2351,11 @@ pretty-format "^27.0.2" "@testing-library/jest-dom@^6.0.0": - version "6.4.5" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz#badb40296477149136dabef32b572ddd3b56adf1" - integrity sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A== + version "6.4.6" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz#ec1df8108651bed5475534955565bed88c6732ce" + integrity sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w== dependencies: - "@adobe/css-tools" "^4.3.2" + "@adobe/css-tools" "^4.4.0" "@babel/runtime" "^7.9.2" aria-query "^5.0.0" chalk "^3.0.0" @@ -2705,11 +2391,6 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -2798,6 +2479,11 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.4.tgz#e3e331b7e0d5496873d417839f3b2bbcf555bb73" integrity sha512-aqBg5oAGo/qh/+wxUfuMadDu2WO0MEWOblyzwaM1Ske2xilUxBfgPqapAFVAfrVTDMVwa0UMarzGot8m64IAzA== +"@types/css-tree@^2.3.8": + version "2.3.8" + resolved "https://registry.yarnpkg.com/@types/css-tree/-/css-tree-2.3.8.tgz#0eabc115e45051b2f7abe51ee1531074b234ed19" + integrity sha512-zABG3nI2UENsx7AQv63tI5/ptoAG/7kQR1H0OvG+WTWYHOR5pfAT3cGgC8SdyCrgX/TTxJBZNmx82IjCXs1juQ== + "@types/diff-match-patch@^1.0.32": version "1.0.36" resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz#dcef10a69d357fe9d43ac4ff2eca6b85dbf466af" @@ -2823,7 +2509,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@^4.17.21", "@types/express@^4.7.0": +"@types/express@^4.17.21": version "4.17.21" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== @@ -2935,9 +2621,9 @@ integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ== "@types/lodash@^4.14.168": - version "4.17.4" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7" - integrity sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ== + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" + integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== "@types/mapbox__point-geometry@*", "@types/mapbox__point-geometry@^0.1.2": version "0.1.4" @@ -2984,9 +2670,9 @@ undici-types "~5.26.4" "@types/node@18": - version "18.19.33" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.33.tgz#98cd286a1b8a5e11aa06623210240bcc28e95c48" - integrity sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A== + version "18.19.39" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.39.tgz#c316340a5b4adca3aee9dcbf05de385978590593" + integrity sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ== dependencies: undici-types "~5.26.4" @@ -3010,11 +2696,6 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== -"@types/q@^1.5.1": - version "1.5.8" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" - integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== - "@types/qrcode@^1.3.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" @@ -3094,10 +2775,10 @@ resolved "https://registry.yarnpkg.com/@types/sdp-transform/-/sdp-transform-2.4.9.tgz#26ef39f487a6909b0512f580b80920a366b27f52" integrity sha512-bVr+/OoZZy7wrHlNcEAAa6PAgKA4BoXPYVN2EijMC5WnGgQ4ZEuixmKnVs2roiAvr7RhIFVH17QD27cojgIZCg== -"@types/seedrandom@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.4.tgz#e4a8d0fca0168cacc7dba2af0e4a4ea645d3a190" - integrity sha512-/rWdxeiuZenlawrHU+XV6ZHMTKOqrC2hMfeDfLTIWJhDZP5aVqXRysduYHBbhD7CeJO6FJr/D2uBVXB7GT6v7w== +"@types/seedrandom@3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.8.tgz#61cc8ed88f93a3c31289c295e6df8ca40be42bdf" + integrity sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ== "@types/send@*": version "0.17.4" @@ -3136,10 +2817,10 @@ resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz#832c58e460c9435e4e34bb866e85e9146e12cdbb" integrity sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg== -"@types/uuid@^9.0.2": - version "9.0.8" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" - integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== "@types/yargs-parser@*": version "21.0.3" @@ -3154,74 +2835,74 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^7.0.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz#f87a32e8972b8a60024f2f8f12205e7c8108bc41" - integrity sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q== + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz#b3563927341eca15124a18c6f94215f779f5c02a" + integrity sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.12.0" - "@typescript-eslint/type-utils" "7.12.0" - "@typescript-eslint/utils" "7.12.0" - "@typescript-eslint/visitor-keys" "7.12.0" + "@typescript-eslint/scope-manager" "7.16.0" + "@typescript-eslint/type-utils" "7.16.0" + "@typescript-eslint/utils" "7.16.0" + "@typescript-eslint/visitor-keys" "7.16.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^7.0.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.12.0.tgz#8761df3345528b35049353db80010b385719b1c3" - integrity sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ== - dependencies: - "@typescript-eslint/scope-manager" "7.12.0" - "@typescript-eslint/types" "7.12.0" - "@typescript-eslint/typescript-estree" "7.12.0" - "@typescript-eslint/visitor-keys" "7.12.0" + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.16.0.tgz#53fae8112f8c912024aea7b499cf7374487af6d8" + integrity sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw== + dependencies: + "@typescript-eslint/scope-manager" "7.16.0" + "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/typescript-estree" "7.16.0" + "@typescript-eslint/visitor-keys" "7.16.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@7.12.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz#259c014362de72dd34f995efe6bd8dda486adf58" - integrity sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg== +"@typescript-eslint/scope-manager@7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz#6927d6451537ce648c6af67a2327378d4cc18462" + integrity sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng== dependencies: - "@typescript-eslint/types" "7.12.0" - "@typescript-eslint/visitor-keys" "7.12.0" + "@typescript-eslint/types" "7.13.0" + "@typescript-eslint/visitor-keys" "7.13.0" -"@typescript-eslint/scope-manager@7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz#1dd3e63a4411db356a9d040e75864851b5f2619b" - integrity sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ== +"@typescript-eslint/scope-manager@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz#eb0757af5720c9c53c8010d7a0355ae27e17b7e5" + integrity sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw== dependencies: - "@typescript-eslint/types" "7.9.0" - "@typescript-eslint/visitor-keys" "7.9.0" + "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/visitor-keys" "7.16.0" -"@typescript-eslint/type-utils@7.12.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz#9dfaaa1972952f395ec5be4f5bbfc4d3cdc63908" - integrity sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA== +"@typescript-eslint/type-utils@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz#ec52b1932b8fb44a15a3e20208e0bd49d0b6bd00" + integrity sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg== dependencies: - "@typescript-eslint/typescript-estree" "7.12.0" - "@typescript-eslint/utils" "7.12.0" + "@typescript-eslint/typescript-estree" "7.16.0" + "@typescript-eslint/utils" "7.16.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@7.12.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.12.0.tgz#bf208f971a8da1e7524a5d9ae2b5f15192a37981" - integrity sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg== +"@typescript-eslint/types@7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.13.0.tgz#0cca95edf1f1fdb0cfe1bb875e121b49617477c5" + integrity sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA== -"@typescript-eslint/types@7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.9.0.tgz#b58e485e4bfba055659c7e683ad4f5f0821ae2ec" - integrity sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w== +"@typescript-eslint/types@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.16.0.tgz#60a19d7e7a6b1caa2c06fac860829d162a036ed2" + integrity sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw== -"@typescript-eslint/typescript-estree@7.12.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz#e6c1074f248b3db6573ab6a7c47a39c4cd498ff9" - integrity sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ== +"@typescript-eslint/typescript-estree@7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz#4cc24fc155088ebf3b3adbad62c7e60f72c6de1c" + integrity sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw== dependencies: - "@typescript-eslint/types" "7.12.0" - "@typescript-eslint/visitor-keys" "7.12.0" + "@typescript-eslint/types" "7.13.0" + "@typescript-eslint/visitor-keys" "7.13.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -3229,13 +2910,13 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/typescript-estree@7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz#3395e27656060dc313a6b406c3a298b729685e07" - integrity sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg== +"@typescript-eslint/typescript-estree@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz#98ac779d526fab2a781e5619c9250f3e33867c09" + integrity sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw== dependencies: - "@typescript-eslint/types" "7.9.0" - "@typescript-eslint/visitor-keys" "7.9.0" + "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/visitor-keys" "7.16.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -3243,40 +2924,40 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.12.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.12.0.tgz#c6e58fd7f724cdccc848f71e388ad80cbdb95dd0" - integrity sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ== +"@typescript-eslint/utils@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.16.0.tgz#b38dc0ce1778e8182e227c98d91d3418449aa17f" + integrity sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "7.12.0" - "@typescript-eslint/types" "7.12.0" - "@typescript-eslint/typescript-estree" "7.12.0" + "@typescript-eslint/scope-manager" "7.16.0" + "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/typescript-estree" "7.16.0" "@typescript-eslint/utils@^6.0.0 || ^7.0.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.9.0.tgz#1b96a34eefdca1c820cb1bbc2751d848b4540899" - integrity sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA== + version "7.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.13.0.tgz#f84e7e8aeceae945a9a3f40d077fd95915308004" + integrity sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "7.9.0" - "@typescript-eslint/types" "7.9.0" - "@typescript-eslint/typescript-estree" "7.9.0" + "@typescript-eslint/scope-manager" "7.13.0" + "@typescript-eslint/types" "7.13.0" + "@typescript-eslint/typescript-estree" "7.13.0" -"@typescript-eslint/visitor-keys@7.12.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz#c053b55a996679528beeedd8e565710ce1ae1ad3" - integrity sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ== +"@typescript-eslint/visitor-keys@7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz#2eb7ce8eb38c2b0d4a494d1fe1908e7071a1a353" + integrity sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw== dependencies: - "@typescript-eslint/types" "7.12.0" + "@typescript-eslint/types" "7.13.0" eslint-visitor-keys "^3.4.3" -"@typescript-eslint/visitor-keys@7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz#82162656e339c3def02895f5c8546f6888d9b9ea" - integrity sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ== +"@typescript-eslint/visitor-keys@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz#a1d99fa7a3787962d6e0efd436575ef840e23b06" + integrity sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg== dependencies: - "@typescript-eslint/types" "7.9.0" + "@typescript-eslint/types" "7.16.0" eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": @@ -3284,29 +2965,27 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vector-im/compound-design-tokens@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.2.0.tgz#ccb15fffc24cc70d83593bfc5348e6a0198cc08a" - integrity sha512-8LSbb38KxvStcOQZDSi7lI4oqtCuHFEgEQi9Q0KUx+5OnklfdyJ638txM1bznX/Cp9lHgMk4dHrTiQHBOE0ZuA== - dependencies: - svg2vectordrawable "^2.9.1" +"@vector-im/compound-design-tokens@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.6.1.tgz#3f1bb5b2b9f8aff10144aab19dfa11165c3c927b" + integrity sha512-u5xG/8AN7QkPPWhugj0ZrQtWsAjuKHzuOoP0s3bbDg7ZkKTE9l5tM29bdOHnSv9mEYKO+KVMMfsl0W1rlaTmAw== -"@vector-im/compound-web@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-4.4.1.tgz#378c6874888becd4b6dd3541904f63300b9ba09a" - integrity sha512-KLYSU8GxR8EBuz+gKSoLLs4+s5xV4stUDbqJu5GG52OmO3YQlvmz/e5/uHYvzfbqBBU5dMmZhz5bdJJ38qxHPQ== +"@vector-im/compound-web@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-5.4.0.tgz#b95262197199c11931a8c6f5269514eb9461f187" + integrity sha512-+EPbr8HzlGEWSePEcPs2iQEBnjXvHGWK177SKF8IO2C7Z2Ygddxa2VTQ7oqtrUfgT+NB5IBTLyXV4Nx7FLgmMA== dependencies: "@floating-ui/react" "^0.26.9" "@floating-ui/react-dom" "^2.0.8" "@radix-ui/react-context-menu" "^2.1.5" "@radix-ui/react-dropdown-menu" "^2.0.6" "@radix-ui/react-form" "^0.0.3" + "@radix-ui/react-progress" "^1.0.3" "@radix-ui/react-separator" "^1.0.3" "@radix-ui/react-slot" "^1.0.2" "@radix-ui/react-tooltip" "^1.0.6" - "@storybook/manager-api" "^8.1.1" classnames "^2.3.2" - graphemer "^1.4.0" + ts-xor "^1.3.0" vaul "^0.7.0" "@zxcvbn-ts/core@^3.0.4": @@ -3331,11 +3010,6 @@ abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -abs-svg-path@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf" - integrity sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA== - accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -3492,14 +3166,14 @@ aria-hidden@^1.1.1: dependencies: tslib "^2.0.0" -aria-query@5.1.3: +aria-query@5.1.3, aria-query@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: deep-equal "^2.0.5" -aria-query@^5.0.0, aria-query@^5.3.0: +aria-query@^5.0.0: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== @@ -3519,7 +3193,19 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-includes@^3.1.6, array-includes@^3.1.7: +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + +array-includes@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== @@ -3535,7 +3221,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.findlast@^1.2.4: +array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== @@ -3588,15 +3274,15 @@ array.prototype.toreversed@^1.1.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.tosorted@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" - integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.1.0" + es-abstract "^1.23.3" + es-errors "^1.3.0" es-shim-unscopables "^1.0.2" arraybuffer.prototype.slice@^1.0.3: @@ -3632,19 +3318,12 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -asynciterator.prototype@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" - integrity sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg== - dependencies: - has-symbols "^1.0.3" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -available-typed-arrays@^1.0.5, available-typed-arrays@^1.0.6, available-typed-arrays@^1.0.7: +available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== @@ -3656,22 +3335,17 @@ await-lock@^2.1.0: resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== -axe-core@4.9.1, axe-core@~4.9.1: +axe-core@4.9.1, axe-core@^4.9.1, axe-core@~4.9.1: version "4.9.1" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae" integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw== -axe-core@=4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" - integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== - -axobject-query@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" - integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== +axobject-query@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" + integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== dependencies: - dequal "^2.0.3" + deep-equal "^2.0.5" babel-jest@^29.0.0, babel-jest@^29.7.0: version "29.7.0" @@ -3767,21 +3441,16 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== -base-x@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" - integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== +base-x@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.0.tgz#6d835ceae379130e1a4cb846a70ac4746f28ea9b" + integrity sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ== base64-arraybuffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== -big-integer@^1.6.48: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3798,9 +3467,9 @@ blob-polyfill@^7.0.0: integrity sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ== bloom-filters@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/bloom-filters/-/bloom-filters-3.0.1.tgz#13e28ed22febe2489cd00ba5bd98fdc90e820180" - integrity sha512-rU9IU6bgZ1jmqcLWhlKSidrFjbIGjB89CJBsQqUj1+3/11tAJDwn+f7iRu4bbQ2srTjGgNeoWNwcnelumqdi0g== + version "3.0.2" + resolved "https://registry.yarnpkg.com/bloom-filters/-/bloom-filters-3.0.2.tgz#9c386fca1913da554ededf7a7163bbb93a82d1dd" + integrity sha512-QPKiokjBy16SrBh8T/FAWo74VuNwACnJ9t+q15a+9w5CDaOqHTPPBrDUy70U7YE4+DmENRodtlEdeeq1pB4DZQ== dependencies: base64-arraybuffer "^1.0.2" is-buffer "^2.0.5" @@ -3835,11 +3504,6 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3862,16 +3526,6 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.21.3: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== - dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" - browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" @@ -3882,12 +3536,12 @@ browserslist@^4.22.2, browserslist@^4.23.0: node-releases "^2.0.14" update-browserslist-db "^1.0.13" -bs58@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" - integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== +bs58@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== dependencies: - base-x "^4.0.0" + base-x "^5.0.0" bser@2.1.1: version "2.1.1" @@ -3937,11 +3591,6 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001449: - version "1.0.30001486" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz#56a08885228edf62cbe1ac8980f2b5dae159997e" - integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg== - caniuse-lite@^1.0.30001587: version "1.0.30001629" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz#907a36f4669031bd8a1a8dbc2fa08b29e0db297e" @@ -3952,7 +3601,7 @@ chalk@5.2.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== -chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -4061,15 +3710,6 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - collect-v8-coverage@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" @@ -4116,11 +3756,6 @@ commander@^6.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -4265,25 +3900,6 @@ css-functions-list@^3.2.2: resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.2.tgz#9a54c6dd8416ed25c1079cd88234e927526c1922" integrity sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ== -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - css-tree@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" @@ -4292,11 +3908,6 @@ css-tree@^2.3.1: mdn-data "2.0.30" source-map-js "^1.0.1" -css-what@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -4317,13 +3928,6 @@ cssfontparser@^1.2.1: resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg== -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" @@ -4502,7 +4106,7 @@ depd@2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -dequal@^2.0.2, dequal@^2.0.3: +dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -4586,15 +4190,6 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -4604,7 +4199,7 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" -domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: +domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== @@ -4616,13 +4211,6 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" @@ -4630,15 +4218,6 @@ domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - domutils@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" @@ -4663,11 +4242,6 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.284: - version "1.4.385" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.385.tgz#1afd8d6280d510145148777b899ff481c65531ff" - integrity sha512-L9zlje9bIw0h+CwPQumiuVlfMcV4boxRjFIWDcLfFqTZNbkwOExBzfmswytHawObQX4OUhtNv8gIiB21kOurIg== - electron-to-chromium@^1.4.668: version "1.4.792" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.792.tgz#738712f99d02f70c5754ca4264782915fa946849" @@ -4693,10 +4267,10 @@ emojibase-data@^15.0.0: resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-15.2.0.tgz#475a786c091a101ef4bcf57227771c6260ee39b2" integrity sha512-hDiw4ugxnI4pcVQO+73NlKx6aZP/A+BAPfDgK/3A83RVbHZa0Ut6GHpd5r5XUV9G7BZhKejlIRuxhXialpbt6Q== -emojibase-regex@15.3.0: - version "15.3.0" - resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-15.3.0.tgz#98c9683a481ccb1fe6aefddb495b2d692bbf5368" - integrity sha512-EBz/292VBF9naBPBsGzkZUccgIv1xJibTXIINl8SezgVRnTCpKJx7MgZcR+UAd2RwjGkRJJZ/lhP7riOFZLicA== +emojibase-regex@15.3.2: + version "15.3.2" + resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-15.3.2.tgz#5175231715b86d4b437754527288844a6c29318f" + integrity sha512-ue6BVeb2qu33l97MkxcOoyMJlg6Tug3eTv2z1at+M9TjvlWKvdmAPvZIDG1JbT2RH3FSyJNLucO5K5H/yxT03w== emojibase@^15.0.0: version "15.2.0" @@ -4718,11 +4292,6 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - entities@^4.2.0: version "4.4.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" @@ -4743,151 +4312,17 @@ env-paths@^2.2.1: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.17.5: - version "1.22.4" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.4.tgz#26eb2e7538c3271141f5754d31aabfdb215f27bf" - integrity sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg== - dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" - available-typed-arrays "^1.0.6" - call-bind "^1.0.7" - es-define-property "^1.0.0" - es-errors "^1.3.0" - es-set-tostringtag "^2.0.2" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.1" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" - object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.0" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.8" - string.prototype.trimend "^1.0.7" - string.prototype.trimstart "^1.0.7" - typed-array-buffer "^1.0.1" - typed-array-byte-length "^1.0.0" - typed-array-byte-offset "^1.0.0" - typed-array-length "^1.0.4" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.14" - -es-abstract@^1.18.3: - version "1.21.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" - integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== - dependencies: - array-buffer-byte-length "^1.0.0" - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.2.0" - get-symbol-description "^1.0.0" - globalthis "^1.0.3" - gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.5" - is-array-buffer "^3.0.2" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-typed-array "^1.1.10" - is-weakref "^1.0.2" - object-inspect "^1.12.3" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - safe-regex-test "^1.0.0" - string.prototype.trim "^1.2.7" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - typed-array-length "^1.0.4" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.9" - -es-abstract@^1.22.1, es-abstract@^1.22.3: - version "1.22.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.5.tgz#1417df4e97cc55f09bf7e58d1e614bc61cb8df46" - integrity sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w== +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - es-define-property "^1.0.0" - es-errors "^1.3.0" - es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - hasown "^2.0.1" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" - is-callable "^1.2.7" - is-negative-zero "^2.0.3" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" - object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.0" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.8" - string.prototype.trimend "^1.0.7" - string.prototype.trimstart "^1.0.7" - typed-array-buffer "^1.0.2" - typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.5" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.14" + is-arrayish "^0.2.1" -es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: - version "1.23.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.2.tgz#693312f3940f967b8dd3eebacb590b01712622e0" - integrity sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w== +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== dependencies: array-buffer-byte-length "^1.0.1" arraybuffer.prototype.slice "^1.0.3" @@ -4928,11 +4363,11 @@ es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: safe-regex-test "^1.0.3" string.prototype.trim "^1.2.9" string.prototype.trimend "^1.0.8" - string.prototype.trimstart "^1.0.7" + string.prototype.trimstart "^1.0.8" typed-array-buffer "^1.0.2" typed-array-byte-length "^1.0.1" typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.5" + typed-array-length "^1.0.6" unbox-primitive "^1.0.2" which-typed-array "^1.1.15" @@ -4943,7 +4378,7 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== @@ -4963,34 +4398,14 @@ es-get-iterator@^1.1.3: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -es-iterator-helpers@^1.0.15: - version "1.0.15" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" - integrity sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g== - dependencies: - asynciterator.prototype "^1.0.0" - call-bind "^1.0.2" - define-properties "^1.2.1" - es-abstract "^1.22.1" - es-set-tostringtag "^2.0.1" - function-bind "^1.1.1" - get-intrinsic "^1.2.1" - globalthis "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.5" - iterator.prototype "^1.1.2" - safe-array-concat "^1.0.1" - -es-iterator-helpers@^1.0.17: - version "1.0.18" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" - integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== +es-iterator-helpers@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.3" es-errors "^1.3.0" es-set-tostringtag "^2.0.3" function-bind "^1.1.2" @@ -5010,7 +4425,7 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" -es-set-tostringtag@^2.0.1, es-set-tostringtag@^2.0.2, es-set-tostringtag@^2.0.3: +es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== @@ -5126,33 +4541,33 @@ eslint-plugin-import@^2.25.4: tsconfig-paths "^3.15.0" eslint-plugin-jest@^28.0.0: - version "28.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.5.0.tgz#b497b795de37f671eaccd38bd83030186ff5dc8d" - integrity sha512-6np6DGdmNq/eBbA7HOUNV8fkfL86PYwBfwyb8n23FXgJNTR8+ot3smRHjza9LGsBBZRypK3qyF79vMjohIL8eQ== + version "28.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.6.0.tgz#8410588d60bcafa68a91b6ec272e4a415502302a" + integrity sha512-YG28E1/MIKwnz+e2H7VwYPzHUYU4aMa19w0yGcwXnnmJH6EfgHahTJ2un3IyraUxNfnz/KUhJAFXNNwWPo12tg== dependencies: "@typescript-eslint/utils" "^6.0.0 || ^7.0.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" - integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== + version "6.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz#67ab8ff460d4d3d6a0b4a570e9c1670a0a8245c8" + integrity sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g== dependencies: - "@babel/runtime" "^7.23.2" - aria-query "^5.3.0" - array-includes "^3.1.7" + aria-query "~5.1.3" + array-includes "^3.1.8" array.prototype.flatmap "^1.3.2" ast-types-flow "^0.0.8" - axe-core "=4.7.0" - axobject-query "^3.2.1" + axe-core "^4.9.1" + axobject-query "~3.1.1" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - es-iterator-helpers "^1.0.15" - hasown "^2.0.0" + es-iterator-helpers "^1.0.19" + hasown "^2.0.2" jsx-ast-utils "^3.3.5" language-tags "^1.0.9" minimatch "^3.1.2" - object.entries "^1.1.7" - object.fromentries "^2.0.7" + object.fromentries "^2.0.8" + safe-regex-test "^1.0.3" + string.prototype.includes "^2.0.0" eslint-plugin-matrix-org@1.2.1: version "1.2.1" @@ -5165,33 +4580,33 @@ eslint-plugin-react-hooks@^4.3.0: integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== eslint-plugin-react@^7.28.0: - version "7.34.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" - integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== + version "7.34.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b" + integrity sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA== dependencies: - array-includes "^3.1.7" - array.prototype.findlast "^1.2.4" + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" array.prototype.toreversed "^1.1.2" - array.prototype.tosorted "^1.1.3" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.17" + es-iterator-helpers "^1.0.19" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.7" - object.fromentries "^2.0.7" - object.hasown "^1.1.3" - object.values "^1.1.7" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.hasown "^1.1.4" + object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.10" + string.prototype.matchall "^4.0.11" -eslint-plugin-unicorn@^53.0.0: - version "53.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz#df3a5c9ecabeb759e6fd867b2d84198466ac8c4d" - integrity sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw== +eslint-plugin-unicorn@^54.0.0: + version "54.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz#ce3ea853e8fd7ca2bda2fd6065bf065adb5d8b6d" + integrity sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ== dependencies: "@babel/helper-validator-identifier" "^7.24.5" "@eslint-community/eslint-utils" "^4.4.0" @@ -5432,10 +4847,10 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -fake-indexeddb@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-5.0.2.tgz#8e0b6c75c6dc6639cbb50c1aa948772147d7c93e" - integrity sha512-cB507r5T3D55DfclY01GLkninZLfU7HXV/mhVRTnTRm5k2u+fY7Fof2dBkr80p5t7G7dlA/G5dI87QiMdPpMCQ== +fake-indexeddb@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-6.0.0.tgz#3173d5ad141436dace95f8de6e9ecdc3d9787d5d" + integrity sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" @@ -5529,18 +4944,10 @@ file-saver@^2.0.5: resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== -file-system-cache@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-2.3.0.tgz#201feaf4c8cd97b9d0d608e96861bb6005f46fe6" - integrity sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ== - dependencies: - fs-extra "11.1.1" - ramda "0.29.0" - -filesize@10.1.2: - version "10.1.2" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.2.tgz#33bb71c5c134102499f1bc36e6f2863137f6cb0c" - integrity sha512-Dx770ai81ohflojxhU+oG+Z2QGvKdYxgEr9OSA8UVrqhwNHjfH9A8f5NKfg83fEH8ZFA5N5llJo5T3PIoZ4CRA== +filesize@10.1.4: + version "10.1.4" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.4.tgz#184f256063a201f08b6e6b3cc47d21b60f5b8d89" + integrity sha512-ryBwPIIeErmxgPnm6cbESAzXjuEFubs+yKYLBZvg3CaiNcmkJChoOGcBSrZ6IwkMwPABwPpVXE6IlNdGJJrvEg== fill-range@^7.1.1: version "7.1.1" @@ -5640,9 +5047,9 @@ foreachasync@^3.0.0: integrity sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw== foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" @@ -5666,15 +5073,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-extra@11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-extra@^11.0.0: version "11.2.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" @@ -5704,7 +5102,7 @@ fsevents@^2.3.2, fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1, function-bind@^1.1.2: +function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== @@ -5739,7 +5137,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -5765,7 +5163,7 @@ get-stream@^6.0.0, get-stream@^6.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-symbol-description@^1.0.0, get-symbol-description@^1.0.2: +get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== @@ -5774,10 +5172,10 @@ get-symbol-description@^1.0.0, get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" -gfm.css@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/gfm.css/-/gfm.css-1.1.2.tgz#94acfa600672663b9dd0fd4b6ee5d11c8dbc161e" - integrity sha512-KhK3rqxMj+UTLRxWnfUA5n8XZYMWfHrrcCxtWResYR2B3hWIqBM6v9FPGZSlVuX+ScLewizOvNkjYXuPs95ThQ== +github-markdown-css@^5.5.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-5.6.1.tgz#8ca3d5c3d93d79ea429fddafea091347ab374f78" + integrity sha512-DItLFgHd+s7HQmk63YN4/TdvLeRqk1QP7pPKTTPrDTYoI5x7f/luJWSOZxesmuxBI2srHp8RDyoZd+9WF+WK8Q== gl-matrix@^3.4.3: version "3.4.3" @@ -5803,16 +5201,17 @@ glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^10.3.7: - version "10.3.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.15.tgz#e72bc61bc3038c90605f5dd48543dc67aaf3b50d" - integrity sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw== +glob@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" + integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== dependencies: foreground-child "^3.1.0" - jackspeak "^2.3.6" - minimatch "^9.0.1" - minipass "^7.0.4" - path-scurry "^1.11.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: version "7.2.3" @@ -5860,11 +5259,12 @@ globals@^14.0.0: integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globalthis@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - define-properties "^1.1.3" + define-properties "^1.2.1" + gopd "^1.0.1" globby@^11.1.0: version "11.1.0" @@ -5932,7 +5332,7 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.1, has-tostringtag@^1.0.2: +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== @@ -5952,9 +5352,9 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: function-bind "^1.1.2" highlight.js@^11.3.1: - version "11.9.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" - integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== + version "11.10.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92" + integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ== hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" @@ -6111,16 +5511,7 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -internal-slot@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" - integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== - dependencies: - get-intrinsic "^1.2.2" - hasown "^2.0.0" - side-channel "^1.0.4" - -internal-slot@^1.0.5, internal-slot@^1.0.7: +internal-slot@^1.0.4, internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== @@ -6225,7 +5616,14 @@ is-core-module@^2.11.0: dependencies: has "^1.0.3" -is-core-module@^2.13.0, is-core-module@^2.13.1: +is-core-module@^2.13.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" + integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== + dependencies: + hasown "^2.0.2" + +is-core-module@^2.13.1: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== @@ -6289,17 +5687,12 @@ is-ip@^3.1.0: dependencies: ip-regex "^4.0.0" -is-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== - -is-map@^2.0.3: +is-map@^2.0.2, is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== -is-negative-zero@^2.0.2, is-negative-zero@^2.0.3: +is-negative-zero@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== @@ -6346,12 +5739,7 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-set@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== - -is-set@^2.0.3: +is-set@^2.0.2, is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== @@ -6380,11 +5768,6 @@ is-subset@^0.1.1: resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" integrity sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw== -is-svg-path@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-svg-path/-/is-svg-path-1.0.2.tgz#77ab590c12b3d20348e5c7a13d0040c87784dda0" - integrity sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg== - is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -6392,7 +5775,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.13: +is-typed-array@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== @@ -6503,10 +5886,10 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" -jackspeak@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== +jackspeak@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.1.tgz#9fca4ce961af6083e259c376e9e3541431f5287b" + integrity sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: @@ -6902,10 +6285,10 @@ jest@^29.6.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-xxhash@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-xxhash/-/js-xxhash-3.0.1.tgz#e093b53d02cd80a830d61f58290c206aaa877b24" - integrity sha512-Y2NSC77RIxJrvi2NoXjMi2LYsVDTlVqBoQRi8PXQg4PtP29wdtIOhsp8Ujw4EjEkBFheCPx8bMOmI9zoxx/3jQ== +js-xxhash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-xxhash/-/js-xxhash-4.0.0.tgz#866b003c06ba39bebc9e4a47d2756abfef06606c" + integrity sha512-3Q2eIqG6s1KEBBmkj9tGM9lef8LJbuRyTVBdI3GpTnrvtytunjLPO0wqABp5qhtMzfA32jYn1FlnIV7GH1RAHQ== js-yaml@^3.13.1: version "3.14.1" @@ -7046,9 +6429,9 @@ jwt-decode@4.0.0, jwt-decode@^4.0.0: integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== katex@^0.16.0: - version "0.16.10" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.10.tgz#6f81b71ac37ff4ec7556861160f53bc5f058b185" - integrity sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA== + version "0.16.11" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.11.tgz#4bc84d5584f996abece5f01c6ad11304276a33f5" + integrity sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ== dependencies: commander "^8.3.0" @@ -7074,20 +6457,15 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -known-css-properties@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.30.0.tgz#34dd1f39c805c65a6dfa6ea76206b20dc523dd96" - integrity sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ== - known-css-properties@^0.31.0: version "0.31.0" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.31.0.tgz#5c8d9d8777b3ca09482b2397f6a241e5d69a1023" integrity sha512-sBPIUGTNF0czz0mwGGUoKKJC8Q7On1GPbCSFPfyEsfHb2DyBG0Y4QtV+EVWpINSaiGKZblDNuF5AezxSgOhesQ== language-subtag-registry@^0.3.20: - version "0.3.22" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" - integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + version "0.3.23" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== language-tags@^1.0.9: version "1.0.9" @@ -7109,13 +6487,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lie@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" - integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== - dependencies: - immediate "~3.0.5" - lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -7157,13 +6528,6 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -localforage@^1.8.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" - integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== - dependencies: - lie "3.1.1" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -7243,10 +6607,10 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lru-cache@^10.2.0: - version "10.2.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" - integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== +lru-cache@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.0.tgz#15d93a196f189034d7166caf9fe55e7384c98a21" + integrity sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA== lru-cache@^5.1.1: version "5.1.1" @@ -7301,11 +6665,6 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -map-or-similar@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" - integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg== - maplibre-gl@^2.0.0: version "2.4.0" resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-2.4.0.tgz#2b53dbf526626bf4ee92ad4f33f13ef09e5af182" @@ -7351,15 +6710,16 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@33.1.0: - version "33.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-33.1.0.tgz#e6fe3a521955bb2e2b200a5ca2be1a1ea76a8c90" - integrity sha512-Spf+g156eK+SqPd7EuulTVwpyHkVXR0Ikme04fMCKer+SbzuLPPnqV3gL5cSyTMmHbdNejEJaEvzNIMQoD7Xxw== +matrix-js-sdk@34.2.0: + version "34.2.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-34.2.0.tgz#5e7eff9b4c15689d7f07ad3686373f821e2f06bf" + integrity sha512-dygfH/a0C/Q+a5dSfudxxwA0g9peLsBbalC6LaxPa7AEFb4Gg9d8kiGnlqaFb1U9bGUapk8duBsAC526BjXbdA== dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^5.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^6.0.0" + "@matrix-org/olm" "3.2.15" another-json "^0.2.0" - bs58 "^5.0.0" + bs58 "^6.0.0" content-type "^1.0.4" jwt-decode "^4.0.0" loglevel "^1.7.1" @@ -7369,12 +6729,12 @@ matrix-js-sdk@33.1.0: p-retry "4" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" - uuid "9" + uuid "10" matrix-web-i18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/matrix-web-i18n/-/matrix-web-i18n-3.2.1.tgz#34e6b66bec71a52fddbe72db56d9e35dabbaff59" - integrity sha512-pBklE6Q6mAwG6N3Qtpu/e+qX0XuWEdrs4SZ+QmYJWfyLNtKAB6XcSpE5m7aBW/+11ejg8ua8Q5bNcDV2b7C9lg== + version "3.3.0" + resolved "https://registry.yarnpkg.com/matrix-web-i18n/-/matrix-web-i18n-3.3.0.tgz#a9f9d87d18ef96f75171883abbf201952cbfbe22" + integrity sha512-bJPJrBGrCdslkf2wMVHWyZlAEx9zSKnOsJ9rILaaEy195yyNLpXrYoyRIXEk8YWsdwtaK1ImE+r/Gh43J/I4ow== dependencies: "@babel/parser" "^7.18.5" "@babel/traverse" "^7.18.5" @@ -7399,11 +6759,6 @@ md5@^2.3.0: crypt "0.0.2" is-buffer "~1.1.6" -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - mdn-data@2.0.30: version "2.0.30" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" @@ -7429,13 +6784,6 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== -memoizerific@^1.11.3: - version "1.11.3" - resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" - integrity sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog== - dependencies: - map-or-similar "^1.5.0" - meow@^13.2.0: version "13.2.0" resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" @@ -7496,6 +6844,13 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -7503,10 +6858,10 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.1, minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" @@ -7515,12 +6870,12 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8, minimist@~1. resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4: - version "7.1.1" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.1.tgz#f7f85aff59aa22f110b20e27692465cf3bf89481" - integrity sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -mkdirp@1.0.4, mkdirp@^1.0.4: +mkdirp@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -7605,11 +6960,6 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== - normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -7625,13 +6975,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-svg-path@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz#0e614eca23c39f0cffe821d6be6cd17e569a766c" - integrity sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg== - dependencies: - svg-arc-to-cubic-bezier "^3.0.0" - npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -7639,13 +6982,6 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - nwsapi@^2.2.2: version "2.2.7" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" @@ -7656,18 +6992,18 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.3, object-inspect@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" - integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== object-is@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" + call-bind "^1.0.7" + define-properties "^1.2.1" object-keys@^1.1.1: version "1.1.1" @@ -7684,7 +7020,7 @@ object.assign@^4.1.4, object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.7: +object.entries@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== @@ -7693,7 +7029,7 @@ object.entries@^1.1.7: define-properties "^1.2.1" es-object-atoms "^1.0.0" -object.fromentries@^2.0.7: +object.fromentries@^2.0.7, object.fromentries@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== @@ -7713,15 +7049,16 @@ object.groupby@^1.0.1: es-abstract "^1.22.1" get-intrinsic "^1.2.1" -object.hasown@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" - integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== +object.hasown@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== dependencies: - define-properties "^1.2.0" - es-abstract "^1.22.1" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" -object.values@^1.1.6, object.values@^1.1.7: +object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== @@ -7823,6 +7160,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + pako@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" @@ -7855,11 +7197,6 @@ parse-srcset@^1.0.2: resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== -parse-svg-path@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb" - integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ== - parse5@^7.0.0, parse5@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -7897,13 +7234,13 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.11.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + lru-cache "^11.0.0" + minipass "^7.1.2" path-to-regexp@0.1.7: version "0.1.7" @@ -7962,17 +7299,17 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.44.1: - version "1.44.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.44.1.tgz#53ec975503b763af6fc1a7aa995f34bc09ff447c" - integrity sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA== +playwright-core@1.45.2, playwright-core@^1.45.1: + version "1.45.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.2.tgz#c8b8b7f66eda47fb2bd24e5435c92d1163022df8" + integrity sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw== -playwright@1.44.1: - version "1.44.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.44.1.tgz#5634369d777111c1eea9180430b7a184028e7892" - integrity sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg== +playwright@1.45.2: + version "1.45.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.2.tgz#21082072120a2c8a7e3bbb2792e81e8aa367b7a7" + integrity sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g== dependencies: - playwright-core "1.44.1" + playwright-core "1.45.2" optionalDependencies: fsevents "2.3.2" @@ -8023,14 +7360,6 @@ postcss-scss@^4.0.4: resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.9.tgz#a03c773cd4c9623cb04ce142a52afcec74806685" integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A== -postcss-selector-parser@^6.0.15: - version "6.0.16" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04" - integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - postcss-selector-parser@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" @@ -8062,13 +7391,14 @@ postcss@^8.4.38: picocolors "^1.0.0" source-map-js "^1.2.0" -posthog-js@1.135.2: - version "1.135.2" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.135.2.tgz#1da1508760521e6f0fe1ab908bc4ffbe04c2952c" - integrity sha512-kqix067CyrlcNKUhVxrys8Qp0O/8FUtlkp7lfM+tkJFJAMZsKjIDVslz2AjI9y79CvyyZX+pddfA7F3YFYlS0Q== +posthog-js@1.145.0: + version "1.145.0" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.145.0.tgz#5159459f02988b74407a1dd2b19469c422b31feb" + integrity sha512-LQdH6S2Ks3mnCI0q9aD5SZS0Uujc/90nuJuEeGDeGkWkVkYOSQJt4n0UHrIWEsZdmIKZf0a6OIBhTmO+yUiY3w== dependencies: fflate "^0.4.8" preact "^10.19.3" + web-vitals "^4.0.1" potpack@^1.0.2: version "1.0.2" @@ -8085,10 +7415,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" - integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== +prettier@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" + integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== pretty-format@^27.0.2: version "27.5.1" @@ -8130,14 +7460,6 @@ prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -proposal-temporal@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/proposal-temporal/-/proposal-temporal-0.9.0.tgz#4841cf83cf270f85a829e9283843ea8796d3d86f" - integrity sha512-AyNg3NmmBDCDbABQDmsnsY1B8VciwO9wZm+C3rClAgkPre+SpZDcIGje0WLZwroyqUFDySqW7VV6vcvAv8Bi+Q== - dependencies: - big-integer "^1.6.48" - es-abstract "^1.18.3" - protocol-buffers-schema@^3.3.1: version "3.6.0" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" @@ -8183,11 +7505,6 @@ pvutils@^1.1.3: resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== - qrcode@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170" @@ -8205,13 +7522,6 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.10.0: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== - dependencies: - side-channel "^1.0.6" - querystring@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" @@ -8237,11 +7547,6 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== -ramda@0.29.0: - version "0.29.0" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.29.0.tgz#fbbb67a740a754c8a4cbb41e2a6e0eb8507f55fb" - integrity sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA== - range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -8350,7 +7655,7 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" -react-remove-scroll-bar@^2.3.3: +react-remove-scroll-bar@^2.3.4: version "2.3.6" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== @@ -8358,12 +7663,12 @@ react-remove-scroll-bar@^2.3.3: react-style-singleton "^2.2.1" tslib "^2.0.0" -react-remove-scroll@2.5.5: - version "2.5.5" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" - integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== +react-remove-scroll@2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb" + integrity sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA== dependencies: - react-remove-scroll-bar "^2.3.3" + react-remove-scroll-bar "^2.3.4" react-style-singleton "^2.2.1" tslib "^2.1.0" use-callback-ref "^1.3.0" @@ -8497,16 +7802,7 @@ regexp-tree@^0.1.27: resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== -regexp.prototype.flags@^1.4.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" - integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - set-function-name "^2.0.0" - -regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== @@ -8649,12 +7945,12 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^5.0.0: - version "5.0.7" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.7.tgz#27bddf202e7d89cb2e0381656380d1734a854a74" - integrity sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg== +rimraf@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.0.tgz#503bb3d9283272384c121792d40e7ee3ab763cde" + integrity sha512-u+yqhM92LW+89cxUQK0SRyvXYQmyuKHx0jkx4W7KfwLGLqJnQM5031Uv1trE4gB9XEXBM/s6MxKlfW95IidqaA== dependencies: - glob "^10.3.7" + glob "^11.0.0" run-parallel@^1.1.9: version "1.2.0" @@ -8663,17 +7959,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-array-concat@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692" - integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== - dependencies: - call-bind "^1.0.5" - get-intrinsic "^1.2.2" - has-symbols "^1.0.3" - isarray "^2.0.5" - -safe-array-concat@^1.1.0, safe-array-concat@^1.1.2: +safe-array-concat@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== @@ -8693,7 +7979,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-regex-test@^1.0.0, safe-regex-test@^1.0.3: +safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== @@ -8835,7 +8121,7 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -set-function-name@^2.0.0, set-function-name@^2.0.1: +set-function-name@^2.0.1, set-function-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== @@ -8985,11 +8271,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -9009,11 +8290,6 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" -store2@^2.14.2: - version "2.14.3" - resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.3.tgz#24077d7ba110711864e4f691d2af941ec533deb5" - integrity sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg== - string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -9049,20 +8325,31 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.matchall@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" - integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== +string.prototype.includes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" + integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.matchall@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.5" - regexp.prototype.flags "^1.5.0" - set-function-name "^2.0.0" - side-channel "^1.0.4" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" string.prototype.repeat@^1.0.0: version "1.0.0" @@ -9072,16 +8359,7 @@ string.prototype.repeat@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" -string.prototype.trim@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" - integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - -string.prototype.trim@^1.2.8, string.prototype.trim@^1.2.9: +string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== @@ -9091,16 +8369,7 @@ string.prototype.trim@^1.2.8, string.prototype.trim@^1.2.9: es-abstract "^1.23.0" es-object-atoms "^1.0.0" -string.prototype.trimend@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" - integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - -string.prototype.trimend@^1.0.7, string.prototype.trimend@^1.0.8: +string.prototype.trimend@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== @@ -9109,14 +8378,14 @@ string.prototype.trimend@^1.0.7, string.prototype.trimend@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string.prototype.trimstart@^1.0.6, string.prototype.trimstart@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" - integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@~1.1.1: version "1.1.1" @@ -9173,27 +8442,27 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -stylelint-config-recommended@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-14.0.0.tgz#b395c7014838d2aaca1755eebd914d0bb5274994" - integrity sha512-jSkx290CglS8StmrLp2TxAppIajzIBZKYm3IxT89Kg6fGlxbPiTiyH9PS5YUuVAFwaJLl1ikiXX0QWjI0jmgZQ== +stylelint-config-recommended@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz#d25e86409aaf79ee6c6085c2c14b33c7e23c90c6" + integrity sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg== stylelint-config-standard@^36.0.0: - version "36.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-36.0.0.tgz#6704c044d611edc12692d4a5e37b039a441604d4" - integrity sha512-3Kjyq4d62bYFp/Aq8PMKDwlgUyPU4nacXsjDLWJdNPRUgpuxALu1KnlAHIj36cdtxViVhXexZij65yM0uNIHug== + version "36.0.1" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz#727cbb2a1ef3e210f5ce8329cde531129f156609" + integrity sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw== dependencies: - stylelint-config-recommended "^14.0.0" + stylelint-config-recommended "^14.0.1" stylelint-scss@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.3.0.tgz#2020d0f0d21b8b4151f523e303ae8966728a6e54" - integrity sha512-8OSpiuf1xC7f8kllJsBOFAOYp/mR/C1FXMVeOFjtJPw+AFvEmC93FaklHt7MlOqU4poxuQ1TkYMyfI0V+1SxjA== + version "6.3.2" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.3.2.tgz#085072e774e5a31e65aa2acefaad5417a29d6ec1" + integrity sha512-pNk9mXOVKkQtd+SROPC9io8ISSgX+tOVPhFdBE+LaKQnJMLdWPbGKAGYv4Wmf/RrnOjkutunNTN9kKMhkdE5qA== dependencies: - known-css-properties "^0.30.0" + known-css-properties "^0.31.0" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" - postcss-selector-parser "^6.0.15" + postcss-selector-parser "^6.1.0" postcss-value-parser "^4.2.0" stylelint@^16.1.0: @@ -9282,55 +8551,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -svg-arc-to-cubic-bezier@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6" - integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g== - -svg-path-bounds@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz#00312f672b08afc432a66ddfbd06db40cec8d0d0" - integrity sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ== - dependencies: - abs-svg-path "^0.1.1" - is-svg-path "^1.0.1" - normalize-svg-path "^1.0.0" - parse-svg-path "^0.1.2" - svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== -svg2vectordrawable@^2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/svg2vectordrawable/-/svg2vectordrawable-2.9.1.tgz#23186ff7ace7038d09c031176dbca04063a97e5d" - integrity sha512-7WJIh4SzZLyEJtn45y+f8rREkgBiQMWfb0FoYkXuioywESjDWfbSuP0FQEmIiHP2zOi0oOO8pTG4VkeWJyidWw== - dependencies: - coa "^2.0.2" - mkdirp "^1.0.4" - svg-path-bounds "^1.0.1" - svgo "^2.8.0" - svgpath "^2.5.0" - -svgo@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" - picocolors "^1.0.0" - stable "^0.1.8" - -svgpath@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.6.0.tgz#5b160ef3d742b7dfd2d721bf90588d3450d7a90d" - integrity sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -9357,12 +8582,17 @@ tar-js@^0.3.0: resolved "https://registry.yarnpkg.com/tar-js/-/tar-js-0.3.0.tgz#6949aabfb0ba18bb1562ae51a439fd0f30183a17" integrity sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA== -telejson@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/telejson/-/telejson-7.2.0.tgz#3994f6c9a8f8d7f2dba9be2c7c5bbb447e876f32" - integrity sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ== +temporal-polyfill@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/temporal-polyfill/-/temporal-polyfill-0.2.5.tgz#0796c40a50754c69ec0f9a2db3f6c582b9721aaf" + integrity sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA== dependencies: - memoizerific "^1.11.3" + temporal-spec "^0.2.4" + +temporal-spec@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/temporal-spec/-/temporal-spec-0.2.4.tgz#7eb10447a62429ffaaa80b42b869b138ae306a75" + integrity sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ== test-exclude@^6.0.0: version "6.0.0" @@ -9383,11 +8613,6 @@ tiny-invariant@^1.0.6: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== -tiny-invariant@^1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" - integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== - tinyqueue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" @@ -9456,11 +8681,6 @@ ts-api-utils@^1.3.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-dedent@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" - integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== - ts-node@^10.9.1: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" @@ -9480,6 +8700,11 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-xor@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-xor/-/ts-xor-1.3.0.tgz#3e59f24f0321f9f10f350e0cee3b534b89a2c70b" + integrity sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA== + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -9490,7 +8715,12 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +tslib@^2.0.3: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -9527,11 +8757,6 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -9540,7 +8765,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typed-array-buffer@^1.0.1, typed-array-buffer@^1.0.2: +typed-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== @@ -9549,7 +8774,7 @@ typed-array-buffer@^1.0.1, typed-array-buffer@^1.0.2: es-errors "^1.3.0" is-typed-array "^1.1.13" -typed-array-byte-length@^1.0.0, typed-array-byte-length@^1.0.1: +typed-array-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== @@ -9560,7 +8785,7 @@ typed-array-byte-length@^1.0.0, typed-array-byte-length@^1.0.1: has-proto "^1.0.3" is-typed-array "^1.1.13" -typed-array-byte-offset@^1.0.0, typed-array-byte-offset@^1.0.2: +typed-array-byte-offset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== @@ -9572,10 +8797,10 @@ typed-array-byte-offset@^1.0.0, typed-array-byte-offset@^1.0.2: has-proto "^1.0.3" is-typed-array "^1.1.13" -typed-array-length@^1.0.4, typed-array-length@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.5.tgz#57d44da160296d8663fd63180a1802ebf25905d5" - integrity sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA== +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== dependencies: call-bind "^1.0.7" for-each "^0.3.3" @@ -9584,15 +8809,15 @@ typed-array-length@^1.0.4, typed-array-length@^1.0.5: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typescript@5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" + integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== ua-parser-js@^1.0.2: - version "1.0.37" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" - integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== + version "1.0.38" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2" + integrity sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ== unbox-primitive@^1.0.2: version "1.0.2" @@ -9652,14 +8877,6 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.10: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - update-browserslist-db@^1.0.13: version "1.0.16" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" @@ -9718,16 +8935,16 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@10, uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@9, uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -9792,10 +9009,20 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -webcrypto-core@^1.7.9: - version "1.7.9" - resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.9.tgz#a585f0032dbc88d202cff4f266cbef02ba48bd7a" - integrity sha512-FE+a4PPkOmBbgNDIyRmcHhgXn+2ClRl3JzJdDu/P4+B8y81LqKe6RAsI9b3lAOHe1T1BMkSjsRHTYRikImZnVA== +web-streams-polyfill@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0.tgz#74cedf168339ee6e709532f76c49313a8c7acdac" + integrity sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw== + +web-vitals@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.0.tgz#008949ab79717a68ccaaa3c4371cbc7bbbd78a92" + integrity sha512-ohj72kbtVWCpKYMxcbJ+xaOBV3En76hW47j52dG+tEGG36LZQgfFw5yHl9xyjmosy3XUMn8d/GBUAy4YPM839w== + +webcrypto-core@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.8.0.tgz#aaea17f3dd9c77c304e3c494eb27ca07cc72ca37" + integrity sha512-kR1UQNH8MD42CYuLzvibfakG5Ew5seG85dMMoAM/1LqvckxaF6pUiidLuraIu4V+YCIFabYecUZAW0TuxAoaqw== dependencies: "@peculiar/asn1-schema" "^2.3.8" "@peculiar/json-schema" "^1.1.12" @@ -9904,18 +9131,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== -which-typed-array@^1.1.13: - version "1.1.14" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.14.tgz#1f78a111aee1e131ca66164d8bdc3ab062c95a06" - integrity sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg== - dependencies: - available-typed-arrays "^1.0.6" - call-bind "^1.0.5" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.1" - -which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==