Compare commits
1 Commits
renovate/c
...
erikj/join
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a66a22c14 |
2
.github/workflows/docker.yaml
vendored
@@ -132,7 +132,7 @@ jobs:
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
6
.github/workflows/release_prepare.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
repo: matrix-org/matrix-js-sdk
|
||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: "draft / draft"
|
||||
check-name: draft
|
||||
allowed-conclusions: success
|
||||
|
||||
- name: Wait for element-web draft
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
repo: element-hq/element-web
|
||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: "draft / draft"
|
||||
check-name: draft
|
||||
allowed-conclusions: success
|
||||
|
||||
- name: Wait for element-desktop draft
|
||||
@@ -122,5 +122,5 @@ jobs:
|
||||
repo: element-hq/element-desktop
|
||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: "draft / draft"
|
||||
check-name: draft
|
||||
allowed-conclusions: success
|
||||
|
||||
1
.github/workflows/static_analysis.yaml
vendored
@@ -51,7 +51,6 @@ jobs:
|
||||
error|invalid_json
|
||||
error|misconfigured
|
||||
welcome_to_element
|
||||
devtools|settings|elementCallUrl
|
||||
|
||||
rethemendex_lint:
|
||||
name: "Rethemendex Check"
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
|
||||
uses: guibranco/github-status-action-v2@fe98467f9071758c7fc214af9dbac7f301bd23d4
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
3
.github/workflows/triage-assigned.yml
vendored
@@ -11,8 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
contains(github.event.issue.assignees.*.login, 't3chguy') ||
|
||||
contains(github.event.issue.assignees.*.login, 'florianduros') ||
|
||||
contains(github.event.issue.assignees.*.login, 'dbkr') ||
|
||||
contains(github.event.issue.assignees.*.login, 'andybalaam') ||
|
||||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
|
||||
78
CHANGELOG.md
@@ -1,81 +1,3 @@
|
||||
Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros.
|
||||
* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy.
|
||||
* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot.
|
||||
* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr.
|
||||
* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy.
|
||||
* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros.
|
||||
* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR.
|
||||
* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot.
|
||||
* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown.
|
||||
* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure.
|
||||
* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot.
|
||||
* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR.
|
||||
* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot.
|
||||
* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros.
|
||||
* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr.
|
||||
* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR.
|
||||
* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy.
|
||||
* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros.
|
||||
* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy.
|
||||
* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros.
|
||||
* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr.
|
||||
* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy.
|
||||
* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy.
|
||||
* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh.
|
||||
* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR.
|
||||
* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR.
|
||||
* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam.
|
||||
* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123.
|
||||
|
||||
|
||||
Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* RoomListViewModel: Track the index of the active room in the list ([#29519](https://github.com/element-hq/element-web/pull/29519)). Contributed by @MidhunSureshR.
|
||||
* New room list: add empty state ([#29512](https://github.com/element-hq/element-web/pull/29512)). Contributed by @florianduros.
|
||||
* Implement `MessagePreviewViewModel` ([#29514](https://github.com/element-hq/element-web/pull/29514)). Contributed by @MidhunSureshR.
|
||||
* RoomListViewModel: Add functionality to toggle message preview setting ([#29511](https://github.com/element-hq/element-web/pull/29511)). Contributed by @MidhunSureshR.
|
||||
* New room list: add more options menu on room list item ([#29445](https://github.com/element-hq/element-web/pull/29445)). Contributed by @florianduros.
|
||||
* RoomListViewModel: Provide a way to resort the room list and track the active sort method ([#29499](https://github.com/element-hq/element-web/pull/29499)). Contributed by @MidhunSureshR.
|
||||
* Change \*All rooms\* meta space name to \*All Chats\* ([#29498](https://github.com/element-hq/element-web/pull/29498)). Contributed by @florianduros.
|
||||
* Add setting to hide avatars of rooms you have been invited to. ([#29497](https://github.com/element-hq/element-web/pull/29497)). Contributed by @Half-Shot.
|
||||
* Room List Store: Save preferred sorting algorithm and use that on app launch ([#29493](https://github.com/element-hq/element-web/pull/29493)). Contributed by @MidhunSureshR.
|
||||
* Add key storage toggle to Encryption settings ([#29310](https://github.com/element-hq/element-web/pull/29310)). Contributed by @dbkr.
|
||||
* New room list: add primary filters ([#29481](https://github.com/element-hq/element-web/pull/29481)). Contributed by @florianduros.
|
||||
* Implement MSC4142: Remove unintentional intentional mentions in replies ([#28209](https://github.com/element-hq/element-web/pull/28209)). Contributed by @tulir.
|
||||
* White background for 'They do not match' button ([#29470](https://github.com/element-hq/element-web/pull/29470)). Contributed by @andybalaam.
|
||||
* RoomListViewModel: Support secondary filters in the view model ([#29465](https://github.com/element-hq/element-web/pull/29465)). Contributed by @MidhunSureshR.
|
||||
* RoomListViewModel: Support primary filters in the view model ([#29454](https://github.com/element-hq/element-web/pull/29454)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Implement secondary filters ([#29458](https://github.com/element-hq/element-web/pull/29458)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Implement rest of the primary filters ([#29444](https://github.com/element-hq/element-web/pull/29444)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Support filters by implementing just the favourite filter ([#29433](https://github.com/element-hq/element-web/pull/29433)). Contributed by @MidhunSureshR.
|
||||
* Move toggle switch for integration manager for a11y ([#29436](https://github.com/element-hq/element-web/pull/29436)). Contributed by @Half-Shot.
|
||||
* New room list: basic flat list ([#29368](https://github.com/element-hq/element-web/pull/29368)). Contributed by @florianduros.
|
||||
* Improve rageshake upload experience by providing useful error information ([#29378](https://github.com/element-hq/element-web/pull/29378)). Contributed by @Half-Shot.
|
||||
* Add more functionality to the room list vm ([#29402](https://github.com/element-hq/element-web/pull/29402)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* New room list: fix compose menu action in space ([#29500](https://github.com/element-hq/element-web/pull/29500)). Contributed by @florianduros.
|
||||
* Change ToggleHiddenEventVisibility \& GoToHome KeyBindingActions ([#29374](https://github.com/element-hq/element-web/pull/29374)). Contributed by @gy-mate.
|
||||
* Fix Docker Healthcheck ([#29471](https://github.com/element-hq/element-web/pull/29471)). Contributed by @benbz.
|
||||
* Room List Store: Fetch rooms after space store is ready + attach store to window ([#29453](https://github.com/element-hq/element-web/pull/29453)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Fix bug where left rooms appear in room list ([#29452](https://github.com/element-hq/element-web/pull/29452)). Contributed by @MidhunSureshR.
|
||||
* Add space to the bottom of the room summary actions below leave room ([#29270](https://github.com/element-hq/element-web/pull/29270)). Contributed by @langleyd.
|
||||
* Show error screens in group calls ([#29254](https://github.com/element-hq/element-web/pull/29254)). Contributed by @robintown.
|
||||
* Prevent user from accidentally triggering multiple identity resets ([#29388](https://github.com/element-hq/element-web/pull/29388)). Contributed by @uhoreg.
|
||||
* Remove buggy tooltip on room intro \& homepage ([#29406](https://github.com/element-hq/element-web/pull/29406)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
14
Dockerfile
@@ -1,4 +1,4 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.15-labs
|
||||
# syntax=docker.io/docker/dockerfile:1.14-labs
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
|
||||
@@ -19,10 +19,7 @@ RUN /src/scripts/docker-package.sh
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
|
||||
# App
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
# Install jq and moreutils for sponge, both used by our entrypoints
|
||||
RUN apk add jq moreutils
|
||||
@@ -34,6 +31,13 @@ COPY --from=builder /src/webapp /app
|
||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
||||
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
|
||||
|
||||
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
|
||||
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
|
||||
|
||||
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
|
||||
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
|
||||
RUN chmod -R g+w /var/cache/nginx /etc/nginx
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html \
|
||||
&& ln -s /app /usr/share/nginx/html
|
||||
|
||||
|
||||
13
contribute.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Element",
|
||||
"description": "A glossy Matrix collaboration client for the web.",
|
||||
"repository": {
|
||||
"url": "https://github.com/element-hq/element-web",
|
||||
"license": "AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial"
|
||||
},
|
||||
"bugs": {
|
||||
"list": "https://github.com/element-hq/element-web/issues",
|
||||
"report": "https://github.com/element-hq/element-web/issues/new/choose"
|
||||
},
|
||||
"keywords": ["chat", "riot", "matrix"]
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
- [Skinning](skinning.md)
|
||||
- [Cider editor](ciderEditor.md)
|
||||
- [Iconography](icons.md)
|
||||
- [Jitsi](jitsi.md)
|
||||
- [Local echo](local-echo-dev.md)
|
||||
- [Media](media-handling.md)
|
||||
- [Room List Store](room-list-store.md)
|
||||
|
||||
@@ -384,6 +384,8 @@ The VoIP and Jitsi options are:
|
||||
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
||||
at any time without notice.
|
||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
|
||||
and may be removed at any time without notice. Defaults to `https://call.element.io`.
|
||||
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
||||
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
||||
- `participant_limit`: The maximum number of users who can join a call; if
|
||||
|
||||
2
knip.ts
@@ -40,8 +40,6 @@ export default {
|
||||
// Used by webpack
|
||||
"process",
|
||||
"util",
|
||||
// Embedded into webapp
|
||||
"@element-hq/element-call-embedded",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used in scripts & workflows
|
||||
|
||||
54
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.97",
|
||||
"version": "1.11.95",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -22,7 +22,8 @@
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"AUTHORS.rst",
|
||||
"package.json"
|
||||
"package.json",
|
||||
"contribute.json"
|
||||
],
|
||||
"style": "bundle.css",
|
||||
"matrix_i18n_extra_translation_funcs": [
|
||||
@@ -64,18 +65,16 @@
|
||||
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
||||
"coverage": "yarn test --coverage",
|
||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
||||
"postinstall": "patch-package"
|
||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.0",
|
||||
"@playwright/test": "1.51.1",
|
||||
"@types/react": "19.1.1",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"oidc-client-ts": "3.2.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001714",
|
||||
"testcontainers": "10.24.2",
|
||||
"caniuse-lite": "1.0.30001704",
|
||||
"testcontainers": "10.21.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
@@ -93,8 +92,8 @@
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||
"@vector-im/compound-web": "^7.10.1",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.3",
|
||||
"@vector-im/compound-web": "^7.7.2",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.2",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -108,7 +107,6 @@
|
||||
"css-tree": "^3.0.0",
|
||||
"diff-dom": "^5.0.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"domutils": "^3.2.2",
|
||||
"emojibase-regex": "15.3.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -117,12 +115,12 @@
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"html-react-parser": "^5.2.2",
|
||||
"is-ip": "^3.1.0",
|
||||
"js-xxhash": "^4.0.0",
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-element": "4.2.0",
|
||||
"linkify-react": "4.2.0",
|
||||
"linkify-string": "4.2.0",
|
||||
"linkifyjs": "4.2.0",
|
||||
@@ -138,22 +136,21 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.236.1",
|
||||
"posthog-js": "1.157.2",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.15.0",
|
||||
"sanitize-html": "2.14.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"temporal-polyfill": "^0.3.0",
|
||||
"temporal-polyfill": "^0.2.5",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uuid": "^11.0.0",
|
||||
"what-input": "^5.2.10"
|
||||
@@ -180,14 +177,12 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.9.0",
|
||||
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@rrweb/types": "^2.0.0-alpha.18",
|
||||
"@sentry/webpack-plugin": "^3.0.0",
|
||||
"@stylistic/eslint-plugin": "^4.0.0",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@svgr/webpack": "^8.0.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
@@ -212,11 +207,11 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.1.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.15.0",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/tar-js": "^0.3.5",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
@@ -247,7 +242,7 @@
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"express": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
@@ -268,9 +263,8 @@
|
||||
"minimist": "^1.2.6",
|
||||
"modernizr": "^3.12.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright-core": "^1.51.0",
|
||||
"postcss": "8.5.3",
|
||||
"postcss": "8.4.46",
|
||||
"postcss-easings": "^4.0.0",
|
||||
"postcss-hexrgba": "2.1.0",
|
||||
"postcss-import": "16.1.0",
|
||||
@@ -287,13 +281,13 @@
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"stylelint": "^16.13.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^10.20.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript": "5.8.2",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
index 917a7fc..a2710c6 100644
|
||||
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
@@ -37,7 +37,7 @@ export interface ModuleApi {
|
||||
* @returns Whether the user submitted the dialog or closed it, and the model returned by the
|
||||
* dialog component if submitted.
|
||||
*/
|
||||
- openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||
+ openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C | null>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||
didOkOrSubmit: boolean;
|
||||
model: M;
|
||||
}>;
|
||||
@@ -1,31 +0,0 @@
|
||||
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
|
||||
index 2272032..18bd20a 100644
|
||||
--- a/node_modules/@types/react/index.d.ts
|
||||
+++ b/node_modules/@types/react/index.d.ts
|
||||
@@ -134,7 +134,7 @@ declare namespace React {
|
||||
props: P,
|
||||
) => ReactNode | Promise<ReactNode>)
|
||||
// constructor signature must match React.Component
|
||||
- | (new(props: P) => Component<any, any>);
|
||||
+ | (new(props: P, context?: any) => Component<any, any>);
|
||||
|
||||
/**
|
||||
* Created by {@link createRef}, or {@link useRef} when passed `null`.
|
||||
@@ -941,7 +941,7 @@ declare namespace React {
|
||||
context: unknown;
|
||||
|
||||
// Keep in sync with constructor signature of JSXElementConstructor and ComponentClass.
|
||||
- constructor(props: P);
|
||||
+ constructor(props: P, context?: unknown);
|
||||
|
||||
// We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
|
||||
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
|
||||
@@ -1113,7 +1113,7 @@ declare namespace React {
|
||||
*/
|
||||
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
|
||||
// constructor signature must match React.Component
|
||||
- new(props: P): Component<P, S>;
|
||||
+ new(props: P, context?: any): Component<P, S>;
|
||||
/**
|
||||
* Ignored by React.
|
||||
* @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.
|
||||
@@ -1,22 +0,0 @@
|
||||
diff --git a/node_modules/react-blurhash/dist/index.d.ts b/node_modules/react-blurhash/dist/index.d.ts
|
||||
index 3adbd0a..32e8c13 100644
|
||||
--- a/node_modules/react-blurhash/dist/index.d.ts
|
||||
+++ b/node_modules/react-blurhash/dist/index.d.ts
|
||||
@@ -19,7 +19,7 @@ declare class Blurhash extends React.PureComponent<Props$1> {
|
||||
resolutionY: number;
|
||||
};
|
||||
componentDidUpdate(): void;
|
||||
- render(): JSX.Element;
|
||||
+ render(): React.JSX.Element;
|
||||
}
|
||||
|
||||
declare type Props = React.CanvasHTMLAttributes<HTMLCanvasElement> & {
|
||||
@@ -37,7 +37,7 @@ declare class BlurhashCanvas extends React.PureComponent<Props> {
|
||||
componentDidUpdate(): void;
|
||||
handleRef: (canvas: HTMLCanvasElement) => void;
|
||||
draw: () => void;
|
||||
- render(): JSX.Element;
|
||||
+ render(): React.JSX.Element;
|
||||
}
|
||||
|
||||
export { Blurhash, BlurhashCanvas };
|
||||
108
playwright/e2e/crypto/backups.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog } from "./utils.ts";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
version + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
||||
);
|
||||
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
|
||||
}
|
||||
|
||||
test.describe("Backups", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test(
|
||||
"Create, delete and recreate a keys backup",
|
||||
{ tag: "@no-webkit" },
|
||||
async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const securityKey = await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
|
||||
// expand the advanced section to see the active version in the reports
|
||||
await page
|
||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
||||
.locator("..")
|
||||
.click();
|
||||
|
||||
await expectBackupVersionToBe(page, "1");
|
||||
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Delete it
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
||||
|
||||
// Create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// Should be successful
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
|
||||
// expand the advanced section to see the active version in the reports
|
||||
await page
|
||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
||||
.locator("..")
|
||||
.click();
|
||||
|
||||
await expectBackupVersionToBe(page, "2");
|
||||
|
||||
// ==
|
||||
// Ensure that if you don't have the secret storage passphrase the backup won't be created
|
||||
// ==
|
||||
|
||||
// First delete version 2
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Click "Delete Backup"
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click();
|
||||
|
||||
// Try to create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase
|
||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
|
||||
// check that it failed
|
||||
await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
|
||||
// cancel
|
||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||
|
||||
// go back to the settings to check that no backup was created (the setup button should still be there)
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -8,7 +8,14 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||
import {
|
||||
autoJoin,
|
||||
completeCreateSecretStorageDialog,
|
||||
copyAndContinue,
|
||||
createSharedRoomWithUser,
|
||||
enableKeyBackup,
|
||||
verify,
|
||||
} from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
@@ -77,43 +84,85 @@ test.describe("Cryptography", function () {
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
||||
* @param keyType
|
||||
*/
|
||||
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
|
||||
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
|
||||
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
||||
keyType,
|
||||
);
|
||||
for (const isDeviceVerified of [true, false]) {
|
||||
test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => {
|
||||
/**
|
||||
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
||||
* @param keyType
|
||||
*/
|
||||
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
|
||||
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
|
||||
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
||||
keyType,
|
||||
);
|
||||
expect(accountData.encrypted).toBeDefined();
|
||||
const keys = Object.keys(accountData.encrypted);
|
||||
const key = accountData.encrypted[keys[0]];
|
||||
expect(key.ciphertext).toBeDefined();
|
||||
expect(key.iv).toBeDefined();
|
||||
expect(key.mac).toBeDefined();
|
||||
}
|
||||
|
||||
expect(accountData.encrypted).toBeDefined();
|
||||
const keys = Object.keys(accountData.encrypted);
|
||||
const key = accountData.encrypted[keys[0]];
|
||||
expect(key.ciphertext).toBeDefined();
|
||||
expect(key.iv).toBeDefined();
|
||||
expect(key.mac).toBeDefined();
|
||||
test("by recovery code", async ({ page, app, user: aliceCredentials }) => {
|
||||
// Verified the device
|
||||
if (isDeviceVerified) {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
}
|
||||
|
||||
await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => {
|
||||
// We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
await verifyKey(app, "self_signing");
|
||||
await verifyKey(app, "user_signing");
|
||||
});
|
||||
|
||||
test("by passphrase", async ({ page, app, user: aliceCredentials }) => {
|
||||
// Verified the device
|
||||
if (isDeviceVerified) {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
}
|
||||
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
// Select passphrase option
|
||||
await dialog.getByText("Enter a Security Phrase").click();
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Fill passphrase input
|
||||
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
// Confirm passphrase
|
||||
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
|
||||
await copyAndContinue(page);
|
||||
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
await verifyKey(app, "self_signing");
|
||||
await verifyKey(app, "user_signing");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("Setting up key backup by recovery key", async ({ page, app, user: aliceCredentials }) => {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
|
||||
await enableKeyBackup(app);
|
||||
|
||||
// Wait for the cross signing keys to be uploaded
|
||||
// Waiting for "Change the recovery key" button ensure that all the secrets are uploaded and cached locally
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
await expect(encryptionTab.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
await verifyKey(app, "self_signing");
|
||||
await verifyKey(app, "user_signing");
|
||||
});
|
||||
|
||||
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
await enableKeyBackup(app);
|
||||
const secretStorageKey = await enableKeyBackup(app);
|
||||
|
||||
// Fetch the current cross-signing keys
|
||||
async function fetchMasterKey() {
|
||||
@@ -127,15 +176,18 @@ test.describe("Cryptography", function () {
|
||||
return k;
|
||||
});
|
||||
}
|
||||
|
||||
const masterKey1 = await fetchMasterKey();
|
||||
|
||||
// Find "the Reset cryptographic identity" button
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||
// Find the "reset cross signing" button, and click it
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click();
|
||||
|
||||
// Confirm
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
|
||||
|
||||
// Enter the 4S key
|
||||
await page.getByPlaceholder("Recovery Key").fill(secretStorageKey);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Enter the password
|
||||
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
|
||||
@@ -145,6 +197,9 @@ test.describe("Cryptography", function () {
|
||||
const masterKey2 = await fetchMasterKey();
|
||||
expect(masterKey1).not.toEqual(masterKey2);
|
||||
}).toPass();
|
||||
|
||||
// The dialog should have gone away
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -27,22 +27,16 @@ test.use({
|
||||
test.describe("Dehydration", () => {
|
||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||
|
||||
test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
|
||||
// Verify the device by resetting the key (which will create SSSS, and dehydrated device)
|
||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
await app.closeDialog();
|
||||
|
||||
// Verify the device by resetting the key
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Copy" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ test.describe("Cryptography", function () {
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Sender's verified identity was reset",
|
||||
"Sender's verified identity has changed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
||||
/* should show an error for a message from a previously verified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
||||
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,28 +292,17 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the encryption settings and enable key storage and recovery
|
||||
* Assumes that the current device has been verified
|
||||
* Open the security settings and enable secure key backup.
|
||||
*
|
||||
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
|
||||
*
|
||||
* Returns the recovery key
|
||||
*/
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||
if (!(await keyStorageToggle.isChecked())) {
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
}
|
||||
|
||||
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const recoveryKey = await encryptionTab.getByTestId("recoveryKey").innerText();
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
await encryptionTab.getByRole("textbox").fill(recoveryKey);
|
||||
await encryptionTab.getByRole("button", { name: "Finish set up" }).click();
|
||||
await app.settings.closeDialog();
|
||||
return recoveryKey;
|
||||
return await completeCreateSecretStorageDialog(app.page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,82 +22,31 @@ test.describe("Room list filters and sort", () => {
|
||||
return page.getByRole("listbox", { name: "Room list filters" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test.describe("Scroll behaviour", () => {
|
||||
test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({
|
||||
page,
|
||||
app,
|
||||
}) => {
|
||||
const createFavouriteRoom = async (name: string) => {
|
||||
const id = await app.client.createRoom({
|
||||
name,
|
||||
});
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, id);
|
||||
};
|
||||
|
||||
// Create 5 favourite rooms
|
||||
let i = 0;
|
||||
for (; i < 5; i++) {
|
||||
await createFavouriteRoom(`room${i}-fav`);
|
||||
}
|
||||
|
||||
// Create a non-favourite room
|
||||
await app.client.createRoom({ name: `room-non-fav` });
|
||||
|
||||
// Create rest of the favourite rooms
|
||||
for (; i < 20; i++) {
|
||||
await createFavouriteRoom(`room${i}-fav`);
|
||||
}
|
||||
|
||||
// Open the non-favourite room
|
||||
const roomListView = getRoomList(page);
|
||||
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||
await tile.scrollIntoViewIfNeeded();
|
||||
await tile.click();
|
||||
|
||||
// Enable Favourite filter
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(tile).not.toBeVisible();
|
||||
|
||||
// Ensure the room list is not scrolled
|
||||
const isScrolledDown = await page
|
||||
.getByRole("grid", { name: "Room list" })
|
||||
.evaluate((e) => e.scrollTop !== 0);
|
||||
expect(isScrolledDown).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
let unReadDmId: string | undefined;
|
||||
let unReadRoomId: string | undefined;
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
|
||||
unReadDmId = await bot.createRoom({
|
||||
const unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await app.client.joinRoom(unReadDmId);
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
|
||||
unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
@@ -139,30 +88,6 @@ test.describe("Room list filters and sort", () => {
|
||||
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(3);
|
||||
});
|
||||
|
||||
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(unReadDmId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Let's open a room other than unread room or unread dm
|
||||
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
|
||||
|
||||
// Let's make the bot send a new message in both rooms
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show unread room and not unread dm!
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread room" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Empty room list", () => {
|
||||
|
||||
@@ -7,15 +7,12 @@
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "../../../element-web-test";
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -29,267 +26,108 @@ test.describe("Room list", () => {
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
||||
|
||||
// It should make the room favourited
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Check that the room is favourited
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
|
||||
// It should show the invite dialog
|
||||
await page.getByRole("menuitem", { name: "invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should leave the room
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
|
||||
// Default settings should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png");
|
||||
|
||||
// It should make the room muted
|
||||
await page.getByRole("menuitem", { name: "Mute room" }).click();
|
||||
|
||||
// Remove hover on the room list item
|
||||
await roomListView.hover();
|
||||
|
||||
// Scroll to the bottom of the list
|
||||
await page.getByRole("grid", { name: "Room list" }).evaluate((e) => {
|
||||
e.scrollTop = e.scrollHeight;
|
||||
});
|
||||
|
||||
// The room decoration should have the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
|
||||
await roomItem.hover();
|
||||
// On hover, the room should show the muted icon
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png");
|
||||
|
||||
roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
// The Mute room option should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true");
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png");
|
||||
});
|
||||
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
|
||||
test.describe("Avatar decoration", () => {
|
||||
test.use({ labsFlags: ["feature_video_rooms", "feature_new_room_list"] });
|
||||
|
||||
test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
// @ts-ignore Visibility enum is not accessible
|
||||
await app.client.createRoom({ name: "public room", visibility: "public" });
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
|
||||
|
||||
await expect(publicRoom).toBeVisible();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||
});
|
||||
|
||||
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click();
|
||||
await page.getByRole("menuitem", { name: "New video room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("video room");
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
|
||||
await expect(videoRoom).toBeVisible();
|
||||
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
|
||||
});
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Notification decoration", () => {
|
||||
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await bot.createRoom({
|
||||
name: "invited room",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||
await expect(invitedRoom).toBeVisible();
|
||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
||||
|
||||
// It should make the room favourited
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Check that the room is favourited
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
|
||||
// It should show the invite dialog
|
||||
await page.getByRole("menuitem", { name: "invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should leave the room
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Let's create a new room and invite the bot
|
||||
const room1Id = await app.client.createRoom({
|
||||
name: "Unread Room 1",
|
||||
invite: [bot.credentials?.userId],
|
||||
});
|
||||
await bot.awaitRoomMembership(room1Id);
|
||||
|
||||
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "2 notifications" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||
// Let's create another room as well
|
||||
const room2Id = await app.client.createRoom({
|
||||
name: "Unread Room 2",
|
||||
invite: [bot.credentials?.userId],
|
||||
});
|
||||
await bot.awaitRoomMembership(room2Id);
|
||||
|
||||
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Let's configure unread room 1 so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(room1Id);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mention" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
// Let's open a room other than room 1 or room 2
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
|
||||
const clientBot = await bot.prepareClient();
|
||||
await clientBot.evaluate(
|
||||
async (client, { roomId, userId }) => {
|
||||
await client.sendMessage(roomId, {
|
||||
// @ts-ignore ignore usage of MsgType.text
|
||||
"msgtype": "m.text",
|
||||
"body": "User",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
||||
"m.mentions": {
|
||||
user_ids: [userId],
|
||||
},
|
||||
});
|
||||
},
|
||||
{ roomId, userId: user.userId },
|
||||
);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
// Let's make the bot send a new message in both room 1 and room 2
|
||||
await bot.sendMessage(room1Id, "Hello!");
|
||||
await bot.sendMessage(room2Id, "Hello!");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||
});
|
||||
// Let's activate the unread filter now
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const otherRoomId = await app.client.createRoom({ name: "other room" });
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "activity" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
await app.settings.openUserSettings("Notifications");
|
||||
await page.getByText("Show all activity in the room list (dots or number of unread messages)").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Switch to the other room to avoid the notification to be cleared
|
||||
await app.viewRoomById(otherRoomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||
});
|
||||
|
||||
test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mark as unread" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||
await room.hover();
|
||||
await room.getByRole("button", { name: "More Options" }).click();
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
|
||||
// Remove hover on the room list item
|
||||
await roomListView.hover();
|
||||
|
||||
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
|
||||
});
|
||||
|
||||
test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "silent" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("Off").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||
});
|
||||
// Unread filter should only show room 2!!
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 2" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 1" })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,33 +73,4 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await revokeAccessTokenPromise;
|
||||
await revokeRefreshTokenPromise;
|
||||
});
|
||||
|
||||
test(
|
||||
"it should log out the user & wipe data when logging out via MAS",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ mas, page, mailpitClient }, testInfo) => {
|
||||
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const userId = `alice_${testInfo.testId}`;
|
||||
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await expect(page.getByText("Welcome")).toBeVisible();
|
||||
await page.goto("about:blank");
|
||||
|
||||
// @ts-expect-error
|
||||
const result = await mas.manage("kill-sessions", userId);
|
||||
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
|
||||
|
||||
await page.goto("http://localhost:8080");
|
||||
await expect(
|
||||
page.getByText("For security, this session has been signed out. Please sign in again."),
|
||||
).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
|
||||
|
||||
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
||||
expect(localStorageKeys).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -43,7 +43,6 @@ test.describe("Pills", () => {
|
||||
|
||||
// go back to the message room and try to click on the pill text, as a user would
|
||||
await app.viewRoomByName(messageRoom);
|
||||
await expect(page).toHaveURL(new RegExp(`/#/room/${messageRoomId}`));
|
||||
const pillText = page.locator(".mx_EventTile_body .mx_Pill .mx_Pill_text");
|
||||
await expect(pillText).toHaveCSS("pointer-events", "none");
|
||||
await pillText.click({ force: true }); // force is to ensure we bypass pointer-events
|
||||
|
||||
@@ -136,30 +136,13 @@ test.describe("RightPanel", () => {
|
||||
});
|
||||
test.describe("room reporting", () => {
|
||||
test.skip(isDendrite, "Dendrite does not implement room reporting");
|
||||
test("should handle reporting a room", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
test("should handle reporting a room", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
await page.getByRole("menuitem", { name: "Report room" }).click();
|
||||
const dialog = await page.getByRole("dialog", { name: "Report Room" });
|
||||
await dialog.getByLabel("reason").fill("This room should be reported");
|
||||
await expect(dialog).toMatchScreenshot("room-report-dialog.png");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
|
||||
// Dialog should have gone
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
|
||||
});
|
||||
test("should handle reporting a room and leaving the room", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
await page.getByRole("menuitem", { name: "Report room" }).click();
|
||||
const dialog = await page.getByRole("dialog", { name: "Report room" });
|
||||
await dialog.getByRole("switch", { name: "Leave room" }).click();
|
||||
await dialog.getByLabel("reason").fill("This room should be reported");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
await page.getByRole("dialog", { name: "Leave room" }).getByRole("button", { name: "Leave" }).click();
|
||||
|
||||
// Dialog should have gone
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
|
||||
await expect(page.getByText("Your report was sent.")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Invites", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
displayName: "Bob",
|
||||
},
|
||||
});
|
||||
|
||||
test("should render an invite view", { tag: "@screenshot" }, async ({ page, homeserver, user, bot, app }) => {
|
||||
const roomId = await bot.createRoom({ is_direct: true });
|
||||
await bot.inviteUser(roomId, user.userId);
|
||||
await app.viewRoomByName("Bob");
|
||||
await expect(page.locator(".mx_RoomView")).toMatchScreenshot("Invites_room_view.png", {
|
||||
// Hide the mxid, which is not stable.
|
||||
css: `
|
||||
.mx_RoomPreviewBar_inviter_mxid {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to decline an invite", async ({ page, homeserver, user, bot, app }) => {
|
||||
const roomId = await bot.createRoom({ is_direct: true });
|
||||
await bot.inviteUser(roomId, user.userId);
|
||||
await app.viewRoomByName("Bob");
|
||||
await page.getByRole("button", { name: "Decline", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Bob", exact: true }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should be able to decline an invite, report the room and ignore the user",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, homeserver, user, bot, app }) => {
|
||||
const roomId = await bot.createRoom({ is_direct: true });
|
||||
await bot.inviteUser(roomId, user.userId);
|
||||
await app.viewRoomByName("Bob");
|
||||
await page.getByRole("button", { name: "Decline and block" }).click();
|
||||
await page.getByLabel("Ignore user").click();
|
||||
await page.getByLabel("Report room").click();
|
||||
await page.getByLabel("Reason").fill("Do not want the room");
|
||||
const roomReported = page.waitForRequest(
|
||||
(req) =>
|
||||
req.url().endsWith(`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/report`) &&
|
||||
req.method() === "POST",
|
||||
);
|
||||
await expect(page.getByRole("dialog", { name: "Decline invitation" })).toMatchScreenshot(
|
||||
"Invites_reject_dialog.png",
|
||||
);
|
||||
await page.getByRole("button", { name: "Decline invite" }).click();
|
||||
|
||||
// Check room was reported.
|
||||
await roomReported;
|
||||
|
||||
// Check user is ignored.
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
const ignoredUsersList = page.getByRole("list", { name: "Ignored users" });
|
||||
await ignoredUsersList.scrollIntoViewIfNeeded();
|
||||
await expect(ignoredUsersList.getByRole("listitem", { name: bot.credentials.userId })).toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -28,10 +28,7 @@ test.describe("Preferences user settings tab", () => {
|
||||
const tab = await app.settings.openUserSettings("Preferences");
|
||||
// Assert that the top heading is rendered
|
||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", {
|
||||
// masked due to daylight saving time
|
||||
mask: [tab.locator("#mx_dropdownUserTimezone_value")],
|
||||
});
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
|
||||
});
|
||||
|
||||
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
|
||||
|
||||
@@ -255,8 +255,8 @@ test.describe("Sliding Sync", () => {
|
||||
// Select the room to reject
|
||||
await page.getByRole("treeitem", { name: "Room to Reject" }).click();
|
||||
|
||||
// Decline the invite
|
||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Decline", exact: true }).click();
|
||||
// Reject the invite
|
||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Reject", exact: true }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
|
||||
|
||||
@@ -28,8 +28,6 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
||||
const OLD_NAME = "Alan";
|
||||
const NEW_NAME = "Alan (away)";
|
||||
|
||||
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
|
||||
|
||||
const getEventTilesWithBodies = (page: Page): Locator => {
|
||||
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
||||
};
|
||||
@@ -918,27 +916,7 @@ test.describe("Timeline", () => {
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should be able to hide a video", async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||
msgtype: "m.video" as MsgType,
|
||||
body: "bbb.webm",
|
||||
url: upload.content_uri,
|
||||
});
|
||||
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MVideoBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the video is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show video" })).toBeVisible();
|
||||
await expect(page.locator("video")).not.toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1341,44 +1319,4 @@ test.describe("Timeline", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("spoilers", { tag: "@screenshot" }, () => {
|
||||
test("clicking a spoiler containing the pill de-spoilers on 1st click, then follows link on 2nd", async ({
|
||||
page,
|
||||
user,
|
||||
app,
|
||||
room,
|
||||
}) => {
|
||||
// View room
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
||||
// Send a spoilered pill
|
||||
await app.client.sendMessage(room.roomId, {
|
||||
msgtype: "m.text",
|
||||
body: user.userId,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<span data-mx-spoiler>https://matrix.to/#/${user.userId}</span>`,
|
||||
});
|
||||
|
||||
const screenshotOptions = {
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const eventTile = page.locator(".mx_RoomView_body .mx_EventTile_last");
|
||||
await expect(eventTile).toMatchScreenshot("spoiler.png", screenshotOptions);
|
||||
|
||||
const rightPanelButton = page.getByText("Share profile");
|
||||
const pill = page.locator(".mx_UserPill");
|
||||
await pill.click({ force: true }); // force to click the spoiler wrapper instead
|
||||
await expect(eventTile).toMatchScreenshot("spoiler-uncovered.png", screenshotOptions);
|
||||
await expect(rightPanelButton).not.toBeVisible(); // assert the right panel is not yet open
|
||||
|
||||
await pill.click();
|
||||
await expect(rightPanelButton).toBeVisible(); // assert the right panel is open
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,7 +114,7 @@ export class ElementAppPage {
|
||||
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
|
||||
*/
|
||||
public getComposerField(isRightPanel?: boolean): Locator {
|
||||
return this.getComposer(isRightPanel).locator("div[contenteditable]");
|
||||
return this.getComposer(isRightPanel).locator("[contenteditable]");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 957 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "develop@sha256:66955f34a593cfc3b6e77b8d5510c60c6094f5bade8a17d2feaefbb8662ccf09";
|
||||
const TAG = "develop@sha256:d19854a3dbbb4d5d24d84767d17e1a623181ae5f2bdda3505819c05a8d3c8611";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -116,7 +116,6 @@
|
||||
@import "./views/auth/_Welcome.pcss";
|
||||
@import "./views/avatars/_BaseAvatar.pcss";
|
||||
@import "./views/avatars/_DecoratedRoomAvatar.pcss";
|
||||
@import "./views/avatars/_RoomAvatarView.pcss";
|
||||
@import "./views/avatars/_WidgetAvatar.pcss";
|
||||
@import "./views/avatars/_WithPresenceIndicator.pcss";
|
||||
@import "./views/beta/_BetaCard.pcss";
|
||||
@@ -228,7 +227,6 @@
|
||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||
@import "./views/messages/_EventTileBubble.pcss";
|
||||
@import "./views/messages/_HiddenBody.pcss";
|
||||
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
|
||||
@import "./views/messages/_JumpToDatePicker.pcss";
|
||||
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||
@import "./views/messages/_MEmoteBody.pcss";
|
||||
@@ -341,6 +339,8 @@
|
||||
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
||||
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
|
||||
@import "./views/settings/_AvatarSetting.pcss";
|
||||
@import "./views/settings/_CrossSigningPanel.pcss";
|
||||
@import "./views/settings/_CryptographyPanel.pcss";
|
||||
@import "./views/settings/_FontScalingPanel.pcss";
|
||||
@import "./views/settings/_ImageSizePanel.pcss";
|
||||
@import "./views/settings/_IntegrationManager.pcss";
|
||||
@@ -353,6 +353,7 @@
|
||||
@import "./views/settings/_PhoneNumbers.pcss";
|
||||
@import "./views/settings/_PowerLevelSelector.pcss";
|
||||
@import "./views/settings/_RoomProfileSettings.pcss";
|
||||
@import "./views/settings/_SecureBackupPanel.pcss";
|
||||
@import "./views/settings/_SetIntegrationManager.pcss";
|
||||
@import "./views/settings/_SettingsFieldset.pcss";
|
||||
@import "./views/settings/_SettingsHeader.pcss";
|
||||
|
||||
@@ -16,7 +16,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
border: 1px solid $quinary-content;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 3px rgb(23, 25, 28, 0.05);
|
||||
box-shadow: 0px 1px 3px rgba(23, 25, 28, 0.05);
|
||||
|
||||
background-color: $system;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
filter: drop-shadow(0px 3px 5px rgb(0, 0, 0, 0.2));
|
||||
filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2));
|
||||
background-color: currentColor;
|
||||
|
||||
display: flex;
|
||||
|
||||
@@ -24,7 +24,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
height: $ZoomButtons_button-size;
|
||||
width: $ZoomButtons_button-size;
|
||||
background: $background;
|
||||
box-shadow: 0px 4px 12px rgb(0, 0, 0, 0.25);
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
.mx_ZoomButtons_icon {
|
||||
$ZoomButtons_icon-size: 12px;
|
||||
|
||||
@@ -48,7 +48,7 @@ $height: 220px;
|
||||
display: flex;
|
||||
font-size: $font-12px;
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
background: linear-gradient(rgb(0, 0, 0, 0.9), rgb(0, 0, 0, 0));
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
.mx_WidgetPip_backButton {
|
||||
@@ -69,5 +69,5 @@ $height: 220px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
background: linear-gradient(rgb(0, 0, 0, 0), rgb(0, 0, 0, 0.9));
|
||||
background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.9));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_ContextualMenu {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0px 4px 24px rgb(0, 0, 0, 0.1);
|
||||
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
border: var(--cpd-border-width-1) solid var(--cpd-color-border-interactive-secondary);
|
||||
color: $primary-content;
|
||||
|
||||
@@ -41,7 +41,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
padding-bottom: 10px;
|
||||
|
||||
border: 1px solid $quinary-content;
|
||||
box-shadow: 0 1px 3px rgb(23, 25, 28, 0.05);
|
||||
box-shadow: 0 1px 3px rgba(23, 25, 28, 0.05);
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_top {
|
||||
|
||||
@@ -293,7 +293,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
> hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: rgb(141, 151, 165, 0.2);
|
||||
background-color: rgba(141, 151, 165, 0.2);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -352,9 +352,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
mask-image: linear-gradient(
|
||||
to top,
|
||||
transparent,
|
||||
rgb(255, 255, 255, 30%) 4px,
|
||||
rgb(255, 255, 255, 55%) 8px,
|
||||
rgb(255, 255, 255, 75%) 12px,
|
||||
rgba(255, 255, 255, 30%) 4px,
|
||||
rgba(255, 255, 255, 55%) 8px,
|
||||
rgba(255, 255, 255, 75%) 12px,
|
||||
black 16px
|
||||
);
|
||||
}
|
||||
@@ -370,9 +370,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
linear-gradient(
|
||||
to top,
|
||||
transparent,
|
||||
rgb(255, 255, 255, 30%) 4px,
|
||||
rgb(255, 255, 255, 55%) 8px,
|
||||
rgb(255, 255, 255, 75%) 12px,
|
||||
rgba(255, 255, 255, 30%) 4px,
|
||||
rgba(255, 255, 255, 55%) 8px,
|
||||
rgba(255, 255, 255, 75%) 12px,
|
||||
black 16px
|
||||
);
|
||||
mask-position:
|
||||
|
||||
@@ -19,12 +19,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
background-image:
|
||||
radial-gradient(
|
||||
53.85% 66.75% at 87.55% 0%,
|
||||
hsl(250deg, 76%, 71%, 0.261) 0%,
|
||||
hsl(250deg, 100%, 88%, 0) 100%
|
||||
hsla(250deg, 76%, 71%, 0.261) 0%,
|
||||
hsla(250deg, 100%, 88%, 0) 100%
|
||||
),
|
||||
radial-gradient(41.93% 41.93% at 0% 0%, hsl(222deg, 29%, 20%, 0.28) 0%, hsl(250deg, 100%, 88%, 0) 100%),
|
||||
radial-gradient(100% 100% at 0% 0%, hsl(250deg, 100%, 88%, 0.174) 0%, hsl(0deg, 100%, 86%, 0) 100%),
|
||||
radial-gradient(106.35% 96.26% at 100% 0%, hsl(250deg, 100%, 88%, 0.4) 0%, hsl(167deg, 76%, 82%, 0) 100%);
|
||||
radial-gradient(41.93% 41.93% at 0% 0%, hsla(222deg, 29%, 20%, 0.28) 0%, hsla(250deg, 100%, 88%, 0) 100%),
|
||||
radial-gradient(100% 100% at 0% 0%, hsla(250deg, 100%, 88%, 0.174) 0%, hsla(0deg, 100%, 86%, 0) 100%),
|
||||
radial-gradient(106.35% 96.26% at 100% 0%, hsla(250deg, 100%, 88%, 0.4) 0%, hsla(167deg, 76%, 82%, 0) 100%);
|
||||
/* blur to reduce color banding issues due to alpha-blending multiple gradients */
|
||||
filter: blur(8px);
|
||||
inset: -9px;
|
||||
@@ -34,8 +34,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* gradient to apply different amounts of dithering to different parts of the gradient */
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
/* 10% dithering at the top */ rgb(0, 0, 0, 0.9) 20%,
|
||||
/* 80% dithering at the bottom */ rgb(0, 0, 0, 0.2) 100%
|
||||
/* 10% dithering at the top */ rgba(0, 0, 0, 0.9) 20%,
|
||||
/* 80% dithering at the bottom */ rgba(0, 0, 0, 0.2) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
grid-row: 2 / 4;
|
||||
grid-column: 1;
|
||||
background-color: $system;
|
||||
box-shadow: 0px 4px 20px rgb(0, 0, 0, 0.5);
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
grid-column: 1;
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
color: $primary-content;
|
||||
box-shadow: 0px 4px 24px rgb(0, 0, 0, 0.1);
|
||||
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.1);
|
||||
border: var(--cpd-border-width-1) solid var(--cpd-color-border-interactive-secondary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -37,7 +37,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
justify-content: space-between;
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid rgb(141, 151, 165, 0.2);
|
||||
border-top: 1px solid rgba(141, 151, 165, 0.2);
|
||||
|
||||
> * {
|
||||
flex-basis: content;
|
||||
|
||||
@@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
opacity: 0.72;
|
||||
padding: 20px 0;
|
||||
background: linear-gradient(rgb(0, 0, 0, 0), rgb(0, 0, 0, 0.8));
|
||||
background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
|
||||
.mx_AuthFooter a:link,
|
||||
|
||||
@@ -19,7 +19,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
display: flex;
|
||||
margin: 100px auto auto;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px 0 rgb(0, 0, 0, 0.33);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.33);
|
||||
background-color: $authpage-modal-bg-color;
|
||||
|
||||
@media only screen and (max-height: 768px) {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_RoomAvatarView {
|
||||
--room-avatar-size: 32px;
|
||||
|
||||
position: relative;
|
||||
|
||||
/* Keep the container to the same size than the avatar */
|
||||
inline-size: var(--room-avatar-size);
|
||||
block-size: var(--room-avatar-size);
|
||||
|
||||
.mx_RoomAvatarView_RoomAvatar {
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.mx_RoomAvatarView_RoomAvatar_icon {
|
||||
mask-image: url("$(res)/img/element-icons/roomlist/room-avatar-view-icon-mask.svg");
|
||||
}
|
||||
|
||||
.mx_RoomAvatarView_RoomAvatar_presence {
|
||||
mask-image: url("$(res)/img/element-icons/roomlist/room-avatar-view-presence-mask.svg");
|
||||
}
|
||||
|
||||
.mx_RoomAvatarView_icon {
|
||||
position: absolute;
|
||||
|
||||
/* Place half the icon inside the avatar */
|
||||
/* Avatar size - (icon size (16px) / 2) */
|
||||
left: calc((var(--room-avatar-size) - 8px));
|
||||
bottom: var(--cpd-space-0-5x);
|
||||
}
|
||||
|
||||
.mx_RoomAvatarView_PresenceDecoration {
|
||||
position: absolute;
|
||||
|
||||
/* Place half the icon inside the avatar */
|
||||
/* Avatar size - (icon size (8px) / 2) */
|
||||
left: calc((var(--room-avatar-size) - 4px));
|
||||
bottom: var(--cpd-space-0-5x);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
max-height: 80%;
|
||||
|
||||
.mx_CompoundDialog_footer {
|
||||
box-shadow: 0px -4px 4px rgb(0, 0, 0, 0.05); /* hardcoded colour for both themes */
|
||||
box-shadow: 0px -4px 4px rgba(0, 0, 0, 0.05); /* hardcoded colour for both themes */
|
||||
z-index: 1; /* needed to make footer & shadow appear above dialog content */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_EditHistoryMessage_deletion {
|
||||
color: rgb(255, 76, 85);
|
||||
background-color: rgb(255, 76, 85, 0.1);
|
||||
background-color: rgba(255, 76, 85, 0.1);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.mx_EditHistoryMessage_insertion {
|
||||
color: rgb(26, 169, 123);
|
||||
background-color: rgb(26, 169, 123, 0.1);
|
||||
background-color: rgba(26, 169, 123, 0.1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_ReportRoomDialog,
|
||||
.mx_DeclineAndBlockInviteDialog {
|
||||
.mx_ReportRoomDialog {
|
||||
textarea {
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||
@@ -14,28 +13,4 @@ Please see LICENSE files in the repository root for full details.
|
||||
border-radius: 0.5rem;
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
/*
|
||||
Workaround to fix labels appearing with the wrong color.
|
||||
|
||||
.mx_Dialog (in res/css/_common.pcss) redefines the body color
|
||||
as $light-fg-color rather than the standard primary color.
|
||||
|
||||
This forces the colour to match the Compound style, but
|
||||
in the future the Dialogs should not force a color.
|
||||
*/
|
||||
form label {
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DeclineAndBlockInviteDialog {
|
||||
div[aria-disabled="true"] > label {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.mx_SettingsFlag_label {
|
||||
color: var(--cpd-color-text-primary);
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_ServerPicker {
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: $spacing-16;
|
||||
border-bottom: 1px solid rgb(141, 151, 165, 0.2);
|
||||
border-bottom: 1px solid rgba(141, 151, 165, 0.2);
|
||||
display: grid;
|
||||
grid-template-columns: auto min-content;
|
||||
grid-template-rows: auto auto auto;
|
||||
|
||||
@@ -20,7 +20,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(0, 0, 0, 0.2) transparent;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_header {
|
||||
@@ -201,7 +201,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_item_selected {
|
||||
color: rgb(0, 0, 0, 0.5);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid $accent;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
pointer-events: none;
|
||||
|
||||
span {
|
||||
box-shadow: 0px 4px 15px rgb(0, 0, 0, 0.15);
|
||||
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: $spacing-8;
|
||||
background-color: $background;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
.mx_HiddenMediaPlaceholder {
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
|
||||
/* To center the text in the middle of the frame */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
||||
> div {
|
||||
color: $accent;
|
||||
/* Icon alignment */
|
||||
display: flex;
|
||||
> svg {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:hover .mx_HiddenMediaPlaceholder {
|
||||
background-color: $background;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
overflow: hidden;
|
||||
|
||||
/* Hardcoded colours because it's the same on all themes */
|
||||
background-color: rgb(0, 0, 0, 0.6);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -79,3 +79,39 @@ Please see LICENSE files in the repository root for full details.
|
||||
color: $imagebody-giflabel-color;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_HiddenImagePlaceholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
/* To center the text in the middle of the frame */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: $header-panel-bg-color;
|
||||
|
||||
.mx_HiddenImagePlaceholder_button {
|
||||
color: $accent;
|
||||
|
||||
span.mx_HiddenImagePlaceholder_eye {
|
||||
margin-right: 8px;
|
||||
|
||||
background-color: $accent;
|
||||
mask-image: url("$(res)/img/element-icons/eye.svg");
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
span:not(.mx_HiddenImagePlaceholder_eye) {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile:hover .mx_HiddenImagePlaceholder {
|
||||
background-color: $background;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_AccessibleButton_kind_secondary {
|
||||
color: $secondary-content;
|
||||
background-color: rgb(141, 151, 165, 0.2);
|
||||
background-color: rgba(141, 151, 165, 0.2);
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
padding-bottom: 10px;
|
||||
|
||||
border: var(--cpd-border-width-1) solid var(--cpd-color-border-interactive-secondary);
|
||||
box-shadow: 0px 4px 24px rgb(0, 0, 0, 0.1);
|
||||
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_top {
|
||||
|
||||
@@ -30,7 +30,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
height: 775px;
|
||||
right: -253.77px;
|
||||
top: 0;
|
||||
background: radial-gradient(49.95% 49.95% at 50% 50%, rgb(13, 189, 139, 0.12) 0%, rgb(18, 115, 235, 0) 100%);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
top: var(--cpd-space-2x); /* equal to padding-top of parent */
|
||||
left: 0;
|
||||
border-radius: 12px;
|
||||
background-color: rgb(141, 151, 165, 0.1);
|
||||
background-color: rgba(141, 151, 165, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 24px 0 rgb(27, 29, 34, 0.1);
|
||||
box-shadow: 0 4px 24px 0 rgba(27, 29, 34, 0.1);
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
color: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
|
||||
.mx_SpaceMenu_button {
|
||||
svg {
|
||||
transition: transform 0.1s linear;
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
*/
|
||||
.mx_RoomListItemView {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_container {
|
||||
padding-left: var(--cpd-space-2x);
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
height: 100%;
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-3x);
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
/* The border is only under the room name and the future hover menu */
|
||||
@@ -35,7 +35,7 @@
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
|
||||
.mx_RoomListItemView_roomName {
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -46,28 +46,8 @@
|
||||
|
||||
.mx_RoomListItemView_menu_open {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-1-5x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_selected {
|
||||
background-color: var(--cpd-color-bg-action-secondary-pressed);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_notification_decoration {
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-2x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_empty {
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-3x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_bold .mx_RoomListItemView_roomName {
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_MessageTimestamp {
|
||||
border-radius: var(--MBody-border-radius);
|
||||
/* Hardcoded colours because it's the same on all themes */
|
||||
background-color: rgb(0, 0, 0, 0.6);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: #ffffff;
|
||||
padding: 0px 4px 0px 4px;
|
||||
}
|
||||
|
||||
@@ -816,13 +816,11 @@ $left-gutter: 64px;
|
||||
.mx_EventTile_spoiler_content {
|
||||
filter: blur(5px) saturate(0.1) sepia(1);
|
||||
transition-duration: 0.5s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.visible > .mx_EventTile_spoiler_content {
|
||||
filter: none;
|
||||
user-select: auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
border-bottom: 1px solid var(--cpd-color-gray-400);
|
||||
|
||||
/* From figma */
|
||||
box-shadow: 0 var(--cpd-space-2x) var(--cpd-space-6x) calc(var(--cpd-space-2x) * -1) rgb(27, 29, 34, 0.1);
|
||||
box-shadow: 0 var(--cpd-space-2x) var(--cpd-space-6x) calc(var(--cpd-space-2x) * -1) rgba(27, 29, 34, 0.1);
|
||||
|
||||
.mx_PinnedMessageBanner_main {
|
||||
background: transparent;
|
||||
|
||||
36
res/css/views/settings/_CrossSigningPanel.pcss
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_CrossSigningPanel_statusList {
|
||||
border-spacing: 0;
|
||||
|
||||
th {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-inline-end: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CrossSigningPanel_buttonRow {
|
||||
margin: 1em 0;
|
||||
|
||||
:nth-child(n + 1) {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CrossSigningPanel_advanced {
|
||||
width: fit-content;
|
||||
}
|
||||
32
res/css/views/settings/_CryptographyPanel.pcss
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_CryptographyPanel_sessionInfo {
|
||||
padding: 0em;
|
||||
border-spacing: 0px;
|
||||
}
|
||||
.mx_CryptographyPanel_sessionInfo > tr {
|
||||
vertical-align: baseline;
|
||||
padding: 0em;
|
||||
|
||||
th {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0 1em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CryptographyPanel_importExportButtons {
|
||||
display: inline-flex;
|
||||
flex-flow: wrap;
|
||||
row-gap: $spacing-8;
|
||||
column-gap: $spacing-8;
|
||||
}
|
||||
44
res/css/views/settings/_SecureBackupPanel.pcss
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_SecureBackupPanel_deviceName {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mx_SecureBackupPanel_buttonRow {
|
||||
margin: 1em 0;
|
||||
display: inline-flex;
|
||||
flex-flow: wrap;
|
||||
row-gap: 10px;
|
||||
|
||||
:nth-child(n + 1) {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SecureBackupPanel_statusList {
|
||||
border-spacing: 0;
|
||||
|
||||
th {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-inline-end: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SecureBackupPanel_advanced {
|
||||
width: fit-content;
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
padding: var(--cpd-space-10x);
|
||||
border-radius: var(--cpd-space-4x);
|
||||
/* From figma */
|
||||
box-shadow: 0 1.2px 2.4px 0 rgb(27, 29, 34, 0.15);
|
||||
box-shadow: 0 1.2px 2.4px 0 rgba(27, 29, 34, 0.15);
|
||||
border: 1px solid var(--cpd-color-gray-400);
|
||||
|
||||
.mx_EncryptionCard_header {
|
||||
|
||||
@@ -11,12 +11,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
column-gap: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab_ignoredUsers {
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab_ignoredUser {
|
||||
margin-bottom: $spacing-4;
|
||||
}
|
||||
|
||||
@@ -46,8 +46,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_VerificationShowSas_emojiSas_label {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: $font-12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mx_VerificationShowSas_emojiSas_break {
|
||||
|
||||
@@ -95,7 +95,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgb(0, 0, 0, 0.6);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
border-radius: 8px;
|
||||
|
||||
background-color: $system;
|
||||
box-shadow: 0px 4px 20px rgb(0, 0, 0, 0.2);
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.mx_LegacyCallViewButtons {
|
||||
bottom: 13px;
|
||||
|
||||