Compare commits
87 Commits
hs/add-hid
...
v1.11.97-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
286231aa37 | ||
|
|
3f20df5e08 | ||
|
|
d5e070b300 | ||
|
|
d8ecb6362a | ||
|
|
bcc4ecf0cb | ||
|
|
24d9a174d7 | ||
|
|
7970b968c2 | ||
|
|
59e591c462 | ||
|
|
804cb62698 | ||
|
|
8bb4d44532 | ||
|
|
209ab59978 | ||
|
|
6ae11dab52 | ||
|
|
fac982811c | ||
|
|
d7730f417b | ||
|
|
829b588dbf | ||
|
|
9a7cc7eb34 | ||
|
|
e537da4251 | ||
|
|
094a7071e2 | ||
|
|
a5673f603f | ||
|
|
0c210b9b3a | ||
|
|
05df321f34 | ||
|
|
8116dc5f60 | ||
|
|
d090499329 | ||
|
|
6784d071a6 | ||
|
|
3f47487472 | ||
|
|
89e22e00fb | ||
|
|
bbd798ef36 | ||
|
|
f3f05874fa | ||
|
|
d9091bcba9 | ||
|
|
68692c5af5 | ||
|
|
03dc093e89 | ||
|
|
c68157ec46 | ||
|
|
4fc8b8915b | ||
|
|
690d623dcf | ||
|
|
102a1ddb9e | ||
|
|
99ea51c6f2 | ||
|
|
3f1e56b715 | ||
|
|
f3653abe92 | ||
|
|
a6e8d512d0 | ||
|
|
13c4ab2cf4 | ||
|
|
74da64db63 | ||
|
|
e5d37a324d | ||
|
|
d0c1610bd2 | ||
|
|
64e2a843c3 | ||
|
|
fba59381a0 | ||
|
|
e1970df704 | ||
|
|
b54122884c | ||
|
|
0d28df0f67 | ||
|
|
3a39486468 | ||
|
|
0dc295e3b8 | ||
|
|
5a6c9a4c9a | ||
|
|
599112e122 | ||
|
|
170dcd1c0e | ||
|
|
435d0f96b8 | ||
|
|
c1a44414ec | ||
|
|
a32704ae5b | ||
|
|
5b1be70ee8 | ||
|
|
a6ae04bcde | ||
|
|
b65d18433d | ||
|
|
3587161a2c | ||
|
|
35aed69604 | ||
|
|
d2c334dd25 | ||
|
|
98470b8045 | ||
|
|
4d97af0baf | ||
|
|
f59af3786e | ||
|
|
4fa540962a | ||
|
|
e4f9c650ee | ||
|
|
f3654e45d6 | ||
|
|
2a8b26d90a | ||
|
|
6ed811d4c9 | ||
|
|
c85e6d196d | ||
|
|
98c691670e | ||
|
|
7e3866dd9a | ||
|
|
c6b1a92f2e | ||
|
|
7b809171fc | ||
|
|
0bef212679 | ||
|
|
56d115c2ff | ||
|
|
cdd2622151 | ||
|
|
e662c1959b | ||
|
|
b5f8f2b9f5 | ||
|
|
425adb147a | ||
|
|
839329b52a | ||
|
|
7de54a385e | ||
|
|
55b0b1107e | ||
|
|
550f529a30 | ||
|
|
a6ad6e9ae2 | ||
|
|
d88776e2dc |
@@ -11,7 +11,7 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Download release tarball
|
- name: Download release tarball
|
||||||
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1
|
||||||
with:
|
with:
|
||||||
tag: ${{ inputs.tag }}
|
tag: ${{ inputs.tag }}
|
||||||
fileName: element-*.tar.gz*
|
fileName: element-*.tar.gz*
|
||||||
|
|||||||
11
.github/workflows/build_develop.yml
vendored
@@ -26,12 +26,6 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||||
steps:
|
steps:
|
||||||
# Workaround for https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj
|
|
||||||
- uses: unfor19/install-aws-cli-action@v1
|
|
||||||
with:
|
|
||||||
version: 2.22.35
|
|
||||||
verbose: false
|
|
||||||
arch: amd64
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@@ -115,10 +109,11 @@ jobs:
|
|||||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
||||||
# as the expires after 24h and requires auth to download.
|
# as the expires after 24h and requires auth to download.
|
||||||
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
||||||
|
# Checksum algorithm specified as per https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
||||||
- name: Deploy to R2
|
- name: Deploy to R2
|
||||||
run: |
|
run: |
|
||||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
|
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto
|
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/docker.yaml
vendored
@@ -37,14 +37,14 @@ jobs:
|
|||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
|
|||||||
1
.github/workflows/static_analysis.yaml
vendored
@@ -51,6 +51,7 @@ jobs:
|
|||||||
error|invalid_json
|
error|invalid_json
|
||||||
error|misconfigured
|
error|misconfigured
|
||||||
welcome_to_element
|
welcome_to_element
|
||||||
|
devtools|settings|elementCallUrl
|
||||||
|
|
||||||
rethemendex_lint:
|
rethemendex_lint:
|
||||||
name: "Rethemendex Check"
|
name: "Rethemendex Check"
|
||||||
|
|||||||
2
.github/workflows/tests.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Skip SonarCloud in merge queue
|
- name: Skip SonarCloud in merge queue
|
||||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||||
uses: guibranco/github-status-action-v2@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc
|
uses: guibranco/github-status-action-v2@fe98467f9071758c7fc214af9dbac7f301bd23d4
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|||||||
40
CHANGELOG.md
@@ -1,3 +1,43 @@
|
|||||||
|
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)
|
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|||||||
12
Dockerfile
@@ -19,7 +19,10 @@ RUN /src/scripts/docker-package.sh
|
|||||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginx:alpine-slim
|
FROM nginxinc/nginx-unprivileged:alpine-slim
|
||||||
|
|
||||||
|
# Need root user to install packages & manipulate the usr directory
|
||||||
|
USER root
|
||||||
|
|
||||||
# Install jq and moreutils for sponge, both used by our entrypoints
|
# Install jq and moreutils for sponge, both used by our entrypoints
|
||||||
RUN apk add jq moreutils
|
RUN apk add jq moreutils
|
||||||
@@ -31,13 +34,6 @@ COPY --from=builder /src/webapp /app
|
|||||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
||||||
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
|
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
|
||||||
|
|
||||||
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
|
|
||||||
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
|
|
||||||
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
|
|
||||||
RUN chmod -R g+w /var/cache/nginx /etc/nginx
|
|
||||||
|
|
||||||
RUN rm -rf /usr/share/nginx/html \
|
RUN rm -rf /usr/share/nginx/html \
|
||||||
&& ln -s /app /usr/share/nginx/html
|
&& ln -s /app /usr/share/nginx/html
|
||||||
|
|
||||||
|
|||||||
@@ -384,8 +384,6 @@ The VoIP and Jitsi options are:
|
|||||||
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
||||||
at any time without notice.
|
at any time without notice.
|
||||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||||
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
|
|
||||||
and may be removed at any time without notice. Defaults to `https://call.element.io`.
|
|
||||||
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
||||||
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
||||||
- `participant_limit`: The maximum number of users who can join a call; if
|
- `participant_limit`: The maximum number of users who can join a call; if
|
||||||
|
|||||||
2
knip.ts
@@ -40,6 +40,8 @@ export default {
|
|||||||
// Used by webpack
|
// Used by webpack
|
||||||
"process",
|
"process",
|
||||||
"util",
|
"util",
|
||||||
|
// Embedded into webapp
|
||||||
|
"@element-hq/element-call-embedded",
|
||||||
],
|
],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// Used in scripts & workflows
|
// Used in scripts & workflows
|
||||||
|
|||||||
25
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.11.95",
|
"version": "1.11.97-rc.0",
|
||||||
"description": "Element: the future of secure communication",
|
"description": "Element: the future of secure communication",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -65,16 +65,17 @@
|
|||||||
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
||||||
"coverage": "yarn test --coverage",
|
"coverage": "yarn test --coverage",
|
||||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
|
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
||||||
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@playwright/test": "1.50.1",
|
"@playwright/test": "1.51.1",
|
||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.18",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"oidc-client-ts": "3.1.0",
|
"oidc-client-ts": "3.2.0",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"caniuse-lite": "1.0.30001701",
|
"caniuse-lite": "1.0.30001704",
|
||||||
"testcontainers": "10.20.0",
|
"testcontainers": "10.21.0",
|
||||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||||
},
|
},
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
"@types/png-chunks-extract": "^1.0.2",
|
"@types/png-chunks-extract": "^1.0.2",
|
||||||
"@types/react-virtualized": "^9.21.30",
|
"@types/react-virtualized": "^9.21.30",
|
||||||
"@vector-im/compound-design-tokens": "^4.0.0",
|
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||||
"@vector-im/compound-web": "^7.7.2",
|
"@vector-im/compound-web": "^7.9.0",
|
||||||
"@vector-im/matrix-wysiwyg": "2.38.2",
|
"@vector-im/matrix-wysiwyg": "2.38.2",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"css-tree": "^3.0.0",
|
"css-tree": "^3.0.0",
|
||||||
"diff-dom": "^5.0.0",
|
"diff-dom": "^5.0.0",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
"emojibase-regex": "15.3.2",
|
"emojibase-regex": "15.3.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@@ -115,12 +117,12 @@
|
|||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.3.1",
|
||||||
"html-entities": "^2.0.0",
|
"html-entities": "^2.0.0",
|
||||||
|
"html-react-parser": "^5.2.2",
|
||||||
"is-ip": "^3.1.0",
|
"is-ip": "^3.1.0",
|
||||||
"js-xxhash": "^4.0.0",
|
"js-xxhash": "^4.0.0",
|
||||||
"jsrsasign": "^11.0.0",
|
"jsrsasign": "^11.0.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.16.0",
|
"katex": "^0.16.0",
|
||||||
"linkify-element": "4.2.0",
|
|
||||||
"linkify-react": "4.2.0",
|
"linkify-react": "4.2.0",
|
||||||
"linkify-string": "4.2.0",
|
"linkify-string": "4.2.0",
|
||||||
"linkifyjs": "4.2.0",
|
"linkifyjs": "4.2.0",
|
||||||
@@ -128,7 +130,7 @@
|
|||||||
"maplibre-gl": "^5.0.0",
|
"maplibre-gl": "^5.0.0",
|
||||||
"matrix-encrypt-attachment": "^1.0.3",
|
"matrix-encrypt-attachment": "^1.0.3",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "37.3.0-rc.0",
|
||||||
"matrix-widget-api": "^1.10.0",
|
"matrix-widget-api": "^1.10.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
@@ -144,6 +146,7 @@
|
|||||||
"react-blurhash": "^0.3.0",
|
"react-blurhash": "^0.3.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-focus-lock": "^2.5.1",
|
"react-focus-lock": "^2.5.1",
|
||||||
|
"react-string-replace": "^1.1.1",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
@@ -177,6 +180,7 @@
|
|||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||||
|
"@element-hq/element-call-embedded": "0.9.0",
|
||||||
"@element-hq/element-web-playwright-common": "^1.1.5",
|
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||||
"@peculiar/webcrypto": "^1.4.3",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
@@ -263,6 +267,7 @@
|
|||||||
"minimist": "^1.2.6",
|
"minimist": "^1.2.6",
|
||||||
"modernizr": "^3.12.0",
|
"modernizr": "^3.12.0",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
"playwright-core": "^1.51.0",
|
"playwright-core": "^1.51.0",
|
||||||
"postcss": "8.4.46",
|
"postcss": "8.4.46",
|
||||||
"postcss-easings": "^4.0.0",
|
"postcss-easings": "^4.0.0",
|
||||||
@@ -274,7 +279,7 @@
|
|||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
"postcss-scss": "^4.0.4",
|
"postcss-scss": "^4.0.4",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "3.5.2",
|
"prettier": "3.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
|
|||||||
13
patches/@matrix-org+react-sdk-module-api+2.5.0.patch
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}>;
|
||||||
76
patches/@types+react+18.3.18.patch
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
|
||||||
|
index 6ea73ef..cb51757 100644
|
||||||
|
--- a/node_modules/@types/react/index.d.ts
|
||||||
|
+++ b/node_modules/@types/react/index.d.ts
|
||||||
|
@@ -151,7 +151,7 @@ declare namespace React {
|
||||||
|
/**
|
||||||
|
* The current value of the ref.
|
||||||
|
*/
|
||||||
|
- readonly current: T | null;
|
||||||
|
+ current: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES {
|
||||||
|
@@ -186,7 +186,7 @@ declare namespace React {
|
||||||
|
* @see {@link RefObject}
|
||||||
|
*/
|
||||||
|
|
||||||
|
- type Ref<T> = RefCallback<T> | RefObject<T> | null;
|
||||||
|
+ type Ref<T> = RefCallback<T> | RefObject<T | null> | null;
|
||||||
|
/**
|
||||||
|
* A legacy implementation of refs where you can pass a string to a ref prop.
|
||||||
|
*
|
||||||
|
@@ -300,7 +300,7 @@ declare namespace React {
|
||||||
|
*
|
||||||
|
* @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
|
||||||
|
*/
|
||||||
|
- ref?: LegacyRef<T> | undefined;
|
||||||
|
+ ref?: LegacyRef<T | null> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@@ -1234,7 +1234,7 @@ declare namespace React {
|
||||||
|
*
|
||||||
|
* @see {@link ForwardRefRenderFunction}
|
||||||
|
*/
|
||||||
|
- type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
|
||||||
|
+ type ForwardedRef<T> = ((instance: T | null) => void) | RefObject<T | null> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the function passed to {@link forwardRef}. This is considered different
|
||||||
|
@@ -1565,7 +1565,7 @@ declare namespace React {
|
||||||
|
[propertyName: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
- function createRef<T>(): RefObject<T>;
|
||||||
|
+ function createRef<T>(): RefObject<T | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the component returned from {@link forwardRef}.
|
||||||
|
@@ -1989,7 +1989,7 @@ declare namespace React {
|
||||||
|
* @version 16.8.0
|
||||||
|
* @see {@link https://react.dev/reference/react/useRef}
|
||||||
|
*/
|
||||||
|
- function useRef<T>(initialValue: T): MutableRefObject<T>;
|
||||||
|
+ function useRef<T>(initialValue: T): RefObject<T>;
|
||||||
|
// convenience overload for refs given as a ref prop as they typically start with a null value
|
||||||
|
/**
|
||||||
|
* `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
|
||||||
|
@@ -2004,7 +2004,7 @@ declare namespace React {
|
||||||
|
* @version 16.8.0
|
||||||
|
* @see {@link https://react.dev/reference/react/useRef}
|
||||||
|
*/
|
||||||
|
- function useRef<T>(initialValue: T | null): RefObject<T>;
|
||||||
|
+ function useRef<T>(initialValue: T | null): RefObject<T | null>;
|
||||||
|
// convenience overload for potentially undefined initialValue / call with 0 arguments
|
||||||
|
// has a default to stop it from defaulting to {} instead
|
||||||
|
/**
|
||||||
|
@@ -2017,7 +2017,7 @@ declare namespace React {
|
||||||
|
* @version 16.8.0
|
||||||
|
* @see {@link https://react.dev/reference/react/useRef}
|
||||||
|
*/
|
||||||
|
- function useRef<T = undefined>(initialValue?: undefined): MutableRefObject<T | undefined>;
|
||||||
|
+ function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
|
||||||
|
/**
|
||||||
|
* The signature is identical to `useEffect`, but it fires synchronously after all DOM mutations.
|
||||||
|
* Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside
|
||||||
@@ -162,6 +162,7 @@ test.describe("Cryptography", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
||||||
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
const secretStorageKey = await enableKeyBackup(app);
|
const secretStorageKey = await enableKeyBackup(app);
|
||||||
|
|
||||||
// Fetch the current cross-signing keys
|
// Fetch the current cross-signing keys
|
||||||
|
|||||||
@@ -27,16 +27,22 @@ test.use({
|
|||||||
test.describe("Dehydration", () => {
|
test.describe("Dehydration", () => {
|
||||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||||
|
|
||||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
|
||||||
// Create a backup (which will create SSSS, and dehydrated device)
|
// Verify the device by resetting the key (which will create SSSS, and dehydrated device)
|
||||||
|
|
||||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
|
|
||||||
await completeCreateSecretStorageDialog(page);
|
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 expectDehydratedDeviceEnabled(app);
|
await expectDehydratedDeviceEnabled(app);
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ test.describe("Cryptography", function () {
|
|||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
"Sender's verified identity has changed",
|
"Sender's verified identity was reset",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
|||||||
/* should show an error for a message from a previously verified device */
|
/* should show an error for a message from a previously verified device */
|
||||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||||
const lastTile = page.locator(".mx_EventTile_last");
|
const lastTile = page.locator(".mx_EventTile_last");
|
||||||
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ test.describe("Key storage out of sync toast", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
// We need to wait for there to be two toasts as the wait below won't work in isolation:
|
||||||
|
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
|
||||||
|
// it would always be checking the same toast, even if another one is now the first.
|
||||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,9 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
|||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||||
|
|
||||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||||
|
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
||||||
|
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
||||||
|
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
||||||
if (await useSecurityKey.isVisible()) {
|
if (await useSecurityKey.isVisible()) {
|
||||||
await useSecurityKey.click();
|
await useSecurityKey.click();
|
||||||
}
|
}
|
||||||
@@ -289,17 +292,28 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the security settings and enable secure key backup.
|
* Open the encryption settings and enable key storage and recovery
|
||||||
*
|
* Assumes that the current device has been verified
|
||||||
* 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
|
* Returns the recovery key
|
||||||
*/
|
*/
|
||||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
|
||||||
|
|
||||||
return await completeCreateSecretStorageDialog(app.page);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,14 +18,6 @@ test.describe("Room list filters and sort", () => {
|
|||||||
labsFlags: ["feature_new_room_list"],
|
labsFlags: ["feature_new_room_list"],
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the room list
|
|
||||||
* @param page
|
|
||||||
*/
|
|
||||||
function getRoomList(page: Page) {
|
|
||||||
return page.getByTestId("room-list");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrimaryFilters(page: Page) {
|
function getPrimaryFilters(page: Page) {
|
||||||
return page.getByRole("listbox", { name: "Room list filters" });
|
return page.getByRole("listbox", { name: "Room list filters" });
|
||||||
}
|
}
|
||||||
@@ -33,56 +25,141 @@ test.describe("Room list filters and sort", () => {
|
|||||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||||
// The notification toast is displayed above the search section
|
// The notification toast is displayed above the search section
|
||||||
await app.closeNotificationToast();
|
await app.closeNotificationToast();
|
||||||
|
|
||||||
await app.client.createRoom({ name: "empty room" });
|
|
||||||
|
|
||||||
const unReadDmId = await bot.createRoom({
|
|
||||||
name: "unread dm",
|
|
||||||
invite: [user.userId],
|
|
||||||
is_direct: true,
|
|
||||||
});
|
|
||||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
|
||||||
|
|
||||||
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.");
|
|
||||||
|
|
||||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
|
||||||
await app.client.evaluate(async (client, favouriteId) => {
|
|
||||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
|
||||||
}, favouriteId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
test.describe("Room list", () => {
|
||||||
const roomList = getRoomList(page);
|
/**
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
* Get the room list
|
||||||
|
* @param page
|
||||||
const allFilters = await primaryFilters.locator("option").all();
|
*/
|
||||||
for (const filter of allFilters) {
|
function getRoomList(page: Page) {
|
||||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
return page.getByTestId("room-list");
|
||||||
}
|
}
|
||||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
let unReadDmId: string | undefined;
|
||||||
// only one room should be visible
|
let unReadRoomId: string | undefined;
|
||||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
|
||||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
|
||||||
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
|
||||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
await app.client.createRoom({ name: "empty room" });
|
||||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
unReadDmId = await bot.createRoom({
|
||||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
name: "unread dm",
|
||||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
invite: [user.userId],
|
||||||
|
is_direct: true,
|
||||||
|
});
|
||||||
|
await app.client.joinRoom(unReadDmId);
|
||||||
|
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
await bot.joinRoom(unReadRoomId);
|
||||||
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||||
expect(await roomList.locator("role=gridcell").count()).toBe(3);
|
|
||||||
|
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||||
|
await app.client.evaluate(async (client, favouriteId) => {
|
||||||
|
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||||
|
}, favouriteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
const roomList = getRoomList(page);
|
||||||
|
const primaryFilters = getPrimaryFilters(page);
|
||||||
|
|
||||||
|
const allFilters = await primaryFilters.locator("option").all();
|
||||||
|
for (const filter of allFilters) {
|
||||||
|
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
||||||
|
}
|
||||||
|
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||||
|
// only one room should be visible
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||||
|
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||||
|
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", () => {
|
||||||
|
/**
|
||||||
|
* Get the empty state
|
||||||
|
* @param page
|
||||||
|
*/
|
||||||
|
function getEmptyRoomList(page: Page) {
|
||||||
|
return page.getByTestId("empty-room-list");
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should render the default placeholder when there is no filter",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
const emptyRoomList = getEmptyRoomList(page);
|
||||||
|
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
|
||||||
|
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("should render the placeholder for unread filter", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
const primaryFilters = getPrimaryFilters(page);
|
||||||
|
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||||
|
|
||||||
|
const emptyRoomList = getEmptyRoomList(page);
|
||||||
|
await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png");
|
||||||
|
|
||||||
|
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
|
||||||
|
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
["People", "Rooms", "Favourite"].forEach((filter) => {
|
||||||
|
test(
|
||||||
|
`should render the placeholder for ${filter} filter`,
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
const primaryFilters = getPrimaryFilters(page);
|
||||||
|
await primaryFilters.getByRole("option", { name: filter }).click();
|
||||||
|
|
||||||
|
const emptyRoomList = getEmptyRoomList(page);
|
||||||
|
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ test.describe("Room list", () => {
|
|||||||
test.use({
|
test.use({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
labsFlags: ["feature_new_room_list"],
|
labsFlags: ["feature_new_room_list"],
|
||||||
|
botCreateOpts: {
|
||||||
|
displayName: "BotBob",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,55 +29,199 @@ test.describe("Room list", () => {
|
|||||||
test.beforeEach(async ({ page, app, user }) => {
|
test.beforeEach(async ({ page, app, user }) => {
|
||||||
// The notification toast is displayed above the search section
|
// The notification toast is displayed above the search section
|
||||||
await app.closeNotificationToast();
|
await app.closeNotificationToast();
|
||||||
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 }) => {
|
test.describe("Room list", () => {
|
||||||
const roomListView = getRoomList(page);
|
test.beforeEach(async ({ page, app, user }) => {
|
||||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
for (let i = 0; i < 30; i++) {
|
||||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
await app.client.createRoom({ name: `room${i}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await roomListView.hover();
|
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
// Scroll to the end of the room list
|
const roomListView = getRoomList(page);
|
||||||
await page.mouse.wheel(0, 1000);
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.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 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("should open the room when it is clicked", async ({ page, app, user }) => {
|
test.describe("Notification decoration", () => {
|
||||||
const roomListView = getRoomList(page);
|
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
const roomListView = getRoomList(page);
|
||||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
await bot.createRoom({
|
||||||
const roomListView = getRoomList(page);
|
name: "invited room",
|
||||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
invite: [user.userId],
|
||||||
await roomItem.hover();
|
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");
|
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
const roomListView = getRoomList(page);
|
||||||
await roomItemMenu.click();
|
|
||||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
|
||||||
|
|
||||||
// It should make the room favourited
|
const roomId = await app.client.createRoom({ name: "2 notifications" });
|
||||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
// Check that the room is favourited
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
await roomItem.hover();
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
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
|
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||||
await roomItem.hover();
|
await expect(room).toBeVisible();
|
||||||
await roomItemMenu.click();
|
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||||
await expect(roomItem).not.toBeVisible();
|
});
|
||||||
|
|
||||||
|
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "mention" });
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
|
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.");
|
||||||
|
|
||||||
|
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||||
|
await expect(room).toBeVisible();
|
||||||
|
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { selectHomeserver } from "../utils";
|
|||||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||||
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
import { createBot } from "../crypto/utils.ts";
|
||||||
|
|
||||||
// This test requires fixed credentials for the device signing keys below to work
|
// This test requires fixed credentials for the device signing keys below to work
|
||||||
const username = "user1234";
|
const username = "user1234";
|
||||||
@@ -258,6 +259,34 @@ test.describe("Login", () => {
|
|||||||
|
|
||||||
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Continues to show verification prompt after cancelling device verification", async ({
|
||||||
|
page,
|
||||||
|
homeserver,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
// Create a different device which is cross-signed, meaning we need to verify this device
|
||||||
|
await createBot(page, homeserver, credentials, true);
|
||||||
|
|
||||||
|
// Wait to avoid homeserver rate limit on logins
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
// Load the page and see that we are asked to verify
|
||||||
|
await page.goto("/#/welcome");
|
||||||
|
await login(page, homeserver, credentials);
|
||||||
|
let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
|
await expect(h1).toBeVisible();
|
||||||
|
|
||||||
|
// Click "Verify with another device"
|
||||||
|
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||||
|
|
||||||
|
// Cancel the new dialog
|
||||||
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
|
|
||||||
|
// Check that we are still being asked to verify
|
||||||
|
h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
|
await expect(h1).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,4 +73,33 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await revokeAccessTokenPromise;
|
await revokeAccessTokenPromise;
|
||||||
await revokeRefreshTokenPromise;
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -10,6 +10,7 @@ import { type Locator, type Page } from "@playwright/test";
|
|||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
|
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
const ROOM_NAME = "Test room";
|
const ROOM_NAME = "Test room";
|
||||||
const ROOM_NAME_LONG =
|
const ROOM_NAME_LONG =
|
||||||
@@ -133,6 +134,17 @@ test.describe("RightPanel", () => {
|
|||||||
await page.getByLabel("Room info").nth(1).click();
|
await page.getByLabel("Room info").nth(1).click();
|
||||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||||
});
|
});
|
||||||
|
test.describe("room reporting", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite does not implement room reporting");
|
||||||
|
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 dialog.getByRole("button", { name: "Send report" }).click();
|
||||||
|
await expect(page.getByText("Your report was sent.")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("in spaces", () => {
|
test.describe("in spaces", () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2023 Suguru Hirahara
|
Copyright 2023 Suguru Hirahara
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -50,8 +50,8 @@ test.describe("Appearance user settings tab", () => {
|
|||||||
// Click "Show advanced" link button
|
// Click "Show advanced" link button
|
||||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||||
|
|
||||||
await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click();
|
await tab.getByLabel("Use bundled emoji font").click();
|
||||||
await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click();
|
await tab.getByLabel("Use a system font").click();
|
||||||
|
|
||||||
// Assert that the font-family value was removed
|
// Assert that the font-family value was removed
|
||||||
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ test.describe("Preferences user settings tab", () => {
|
|||||||
const tab = await app.settings.openUserSettings("Preferences");
|
const tab = await app.settings.openUserSettings("Preferences");
|
||||||
// Assert that the top heading is rendered
|
// Assert that the top heading is rendered
|
||||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
|
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")],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
|
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
|
||||||
|
|||||||
@@ -7,47 +7,15 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Page, type Request } from "@playwright/test";
|
import { type Page, type Request } from "@playwright/test";
|
||||||
import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
|
|
||||||
|
|
||||||
import { test as base, expect } from "../../element-web-test";
|
import { test as base, expect } from "../../element-web-test";
|
||||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import type { Bot } from "../../pages/bot";
|
import type { Bot } from "../../pages/bot";
|
||||||
|
|
||||||
const test = base.extend<{
|
const test = base.extend<{
|
||||||
slidingSyncProxy: StartedTestContainer;
|
|
||||||
testRoom: { roomId: string; name: string };
|
testRoom: { roomId: string; name: string };
|
||||||
joinedBot: Bot;
|
joinedBot: Bot;
|
||||||
}>({
|
}>({
|
||||||
slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => {
|
|
||||||
const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3")
|
|
||||||
.withNetwork(network)
|
|
||||||
.withExposedPorts(8008)
|
|
||||||
.withLogConsumer(logger.getConsumer("sliding-sync-proxy"))
|
|
||||||
.withWaitStrategy(Wait.forHttp("/client/server.json", 8008))
|
|
||||||
.withEnvironment({
|
|
||||||
SYNCV3_SECRET: "bwahahaha",
|
|
||||||
SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`,
|
|
||||||
SYNCV3_SERVER: `http://homeserver:8008`,
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
|
|
||||||
const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`;
|
|
||||||
await page.addInitScript((proxyAddress) => {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"mx_local_settings",
|
|
||||||
JSON.stringify({
|
|
||||||
feature_sliding_sync_proxy_url: proxyAddress,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true");
|
|
||||||
}, proxyAddress);
|
|
||||||
await use(container);
|
|
||||||
await container.stop();
|
|
||||||
},
|
|
||||||
// Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script
|
|
||||||
credentials: async ({ slidingSyncProxy, credentials }, use) => {
|
|
||||||
await use(credentials);
|
|
||||||
},
|
|
||||||
testRoom: async ({ user, app }, use) => {
|
testRoom: async ({ user, app }, use) => {
|
||||||
const name = "Test Room";
|
const name = "Test Room";
|
||||||
const roomId = await app.client.createRoom({ name });
|
const roomId = await app.client.createRoom({ name });
|
||||||
@@ -82,6 +50,14 @@ test.describe("Sliding Sync", () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
config: {
|
||||||
|
features: {
|
||||||
|
feature_simplified_sliding_sync: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Load the user fixture for all tests
|
// Load the user fixture for all tests
|
||||||
test.beforeEach(({ user }) => {});
|
test.beforeEach(({ user }) => {});
|
||||||
|
|
||||||
@@ -188,15 +164,7 @@ test.describe("Sliding Sync", () => {
|
|||||||
).not.toBeAttached();
|
).not.toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => {
|
test("should show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => {
|
||||||
// TODO: for now. Later we should.
|
|
||||||
|
|
||||||
// disable notifs in this room (TODO: CS API call?)
|
|
||||||
const locator = page.getByRole("treeitem", { name: "Test Room" });
|
|
||||||
await locator.hover();
|
|
||||||
await locator.getByRole("button", { name: "Notification options" }).click();
|
|
||||||
await page.getByRole("menuitemradio", { name: "Mute room" }).click();
|
|
||||||
|
|
||||||
// create a new room so we know when the message has been received as it'll re-shuffle the room list
|
// create a new room so we know when the message has been received as it'll re-shuffle the room list
|
||||||
await app.client.createRoom({ name: "Dummy" });
|
await app.client.createRoom({ name: "Dummy" });
|
||||||
|
|
||||||
@@ -207,9 +175,7 @@ test.describe("Sliding Sync", () => {
|
|||||||
// wait for this message to arrive, tell by the room list resorting
|
// wait for this message to arrive, tell by the room list resorting
|
||||||
await checkOrder(["Test Room", "Dummy"], page);
|
await checkOrder(["Test Room", "Dummy"], page);
|
||||||
|
|
||||||
await expect(
|
await expect(page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge")).toBeAttached();
|
||||||
page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"),
|
|
||||||
).not.toBeAttached();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should update user settings promptly", async ({ page, app }) => {
|
test("should update user settings promptly", async ({ page, app }) => {
|
||||||
@@ -221,6 +187,37 @@ test.describe("Sliding Sync", () => {
|
|||||||
await expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached();
|
await expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should send subscribe_rooms on room switch if room not already subscribed", async ({ page, app }) => {
|
||||||
|
// create rooms and check room names are correct
|
||||||
|
const roomIds: string[] = [];
|
||||||
|
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
|
||||||
|
const id = await app.client.createRoom({ name: fruit });
|
||||||
|
roomIds.push(id);
|
||||||
|
await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
|
||||||
|
}
|
||||||
|
const [roomAId, roomPId] = roomIds;
|
||||||
|
|
||||||
|
const matchRoomSubRequest = (subRoomId: string) => (request: Request) => {
|
||||||
|
if (!request.url().includes("/sync")) return false;
|
||||||
|
const body = request.postDataJSON();
|
||||||
|
return body.room_subscriptions?.[subRoomId];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select the Test Room and wait for playwright to get the request
|
||||||
|
const [request] = await Promise.all([
|
||||||
|
page.waitForRequest(matchRoomSubRequest(roomAId)),
|
||||||
|
page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
|
||||||
|
]);
|
||||||
|
const roomSubscriptions = request.postDataJSON().room_subscriptions;
|
||||||
|
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
|
||||||
|
|
||||||
|
// Switch to another room and wait for playwright to get the request
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForRequest(matchRoomSubRequest(roomPId)),
|
||||||
|
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("should show and be able to accept/reject/rescind invites", async ({
|
test("should show and be able to accept/reject/rescind invites", async ({
|
||||||
page,
|
page,
|
||||||
app,
|
app,
|
||||||
@@ -361,52 +358,4 @@ test.describe("Sliding Sync", () => {
|
|||||||
// ensure the reply-to does not disappear
|
// ensure the reply-to does not disappear
|
||||||
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
|
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should send unsubscribe_rooms for every room switch", async ({ page, app }) => {
|
|
||||||
// create rooms and check room names are correct
|
|
||||||
const roomIds: string[] = [];
|
|
||||||
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
|
|
||||||
const id = await app.client.createRoom({ name: fruit });
|
|
||||||
roomIds.push(id);
|
|
||||||
await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
|
|
||||||
}
|
|
||||||
const [roomAId, roomPId, roomOId] = roomIds;
|
|
||||||
|
|
||||||
const matchRoomSubRequest = (subRoomId: string) => (request: Request) => {
|
|
||||||
if (!request.url().includes("/sync")) return false;
|
|
||||||
const body = request.postDataJSON();
|
|
||||||
return body.txn_id && body.room_subscriptions?.[subRoomId];
|
|
||||||
};
|
|
||||||
const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => {
|
|
||||||
if (!request.url().includes("/sync")) return false;
|
|
||||||
const body = request.postDataJSON();
|
|
||||||
return (
|
|
||||||
body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Select the Test Room and wait for playwright to get the request
|
|
||||||
const [request] = await Promise.all([
|
|
||||||
page.waitForRequest(matchRoomSubRequest(roomAId)),
|
|
||||||
page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
|
|
||||||
]);
|
|
||||||
const roomSubscriptions = request.postDataJSON().room_subscriptions;
|
|
||||||
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
|
|
||||||
|
|
||||||
// Switch to another room and wait for playwright to get the request
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForRequest(matchRoomSubRequest(roomPId)),
|
|
||||||
page.waitForRequest(matchRoomUnsubRequest(roomAId)),
|
|
||||||
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// And switch to even another room and wait for playwright to get the request
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForRequest(matchRoomSubRequest(roomOId)),
|
|
||||||
page.waitForRequest(matchRoomUnsubRequest(roomPId)),
|
|
||||||
page.getByRole("treeitem", { name: "Orange", exact: true }).click(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// TODO: Add tests for encrypted rooms
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -35,17 +35,18 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR
|
|||||||
name: spaceName,
|
name: spaceName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...roomIds.map(spaceChildInitialState),
|
...roomIds.map((r) => spaceChildInitialState(r)),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] {
|
function spaceChildInitialState(roomId: string, order?: string): ICreateRoomOpts["initial_state"]["0"] {
|
||||||
return {
|
return {
|
||||||
type: "m.space.child",
|
type: "m.space.child",
|
||||||
state_key: roomId,
|
state_key: roomId,
|
||||||
content: {
|
content: {
|
||||||
via: [roomId.split(":")[1]],
|
via: [roomId.split(":")[1]],
|
||||||
|
order,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -121,9 +122,10 @@ test.describe("Spaces", () => {
|
|||||||
await page.getByRole("button", { name: "Skip for now" }).click();
|
await page.getByRole("button", { name: "Skip for now" }).click();
|
||||||
|
|
||||||
// Assert rooms exist in the room list
|
// Assert rooms exist in the room list
|
||||||
await expect(page.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
|
const roomList = page.getByRole("tree", { name: "Rooms" });
|
||||||
await expect(page.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
|
await expect(roomList.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
|
await expect(roomList.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
|
||||||
|
await expect(roomList.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
|
||||||
|
|
||||||
// Assert rooms exist in the space explorer
|
// Assert rooms exist in the space explorer
|
||||||
await expect(
|
await expect(
|
||||||
@@ -155,7 +157,7 @@ test.describe("Spaces", () => {
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Just me" }).click();
|
await page.getByRole("button", { name: "Just me" }).click();
|
||||||
|
|
||||||
await page.getByText("Sample Room").click({ force: true }); // force click as checkbox size is zero
|
await page.getByRole("checkbox", { name: "Sample Room" }).click();
|
||||||
|
|
||||||
// Temporal implementation as multiple elements with the role "button" and name "Add" are found
|
// Temporal implementation as multiple elements with the role "button" and name "Add" are found
|
||||||
await page.locator(".mx_AddExistingToSpace_footer").getByRole("button", { name: "Add" }).click();
|
await page.locator(".mx_AddExistingToSpace_footer").getByRole("button", { name: "Add" }).click();
|
||||||
@@ -165,6 +167,50 @@ test.describe("Spaces", () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should allow user to add an existing room to a space after creation",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
await app.client.createRoom({
|
||||||
|
name: "Sample Room",
|
||||||
|
});
|
||||||
|
await app.client.createRoom({
|
||||||
|
name: "A Room that will not be selected",
|
||||||
|
});
|
||||||
|
|
||||||
|
const menu = await openSpaceCreateMenu(page);
|
||||||
|
await menu.getByRole("button", { name: "Private" }).click();
|
||||||
|
|
||||||
|
await menu
|
||||||
|
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||||
|
.setInputFiles("playwright/sample-files/riot.png");
|
||||||
|
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
|
||||||
|
await menu
|
||||||
|
.getByRole("textbox", { name: "Description" })
|
||||||
|
.fill("This is a personal space to mourn Riot.im...");
|
||||||
|
await menu.getByRole("textbox", { name: "Name" }).fill("This is my Riot");
|
||||||
|
await menu.getByRole("textbox", { name: "Name" }).press("Enter");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Just me" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Skip for now" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "Add existing room" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("checkbox", { name: "Sample Room" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("dialog", { name: "Avatar Add existing rooms" })).toMatchScreenshot(
|
||||||
|
"add-existing-rooms-dialog.png",
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Add" }).click();
|
||||||
|
await expect(
|
||||||
|
page.locator(".mx_SpaceHierarchy_list").getByRole("treeitem", { name: "Sample Room" }),
|
||||||
|
).toBeVisible();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should allow user to invite another to a space", { tag: "@no-webkit" }, async ({ page, app, user, bot }) => {
|
test("should allow user to invite another to a space", { tag: "@no-webkit" }, async ({ page, app, user, bot }) => {
|
||||||
await app.client.createSpace({
|
await app.client.createSpace({
|
||||||
visibility: "public" as any,
|
visibility: "public" as any,
|
||||||
@@ -291,4 +337,36 @@ test.describe("Spaces", () => {
|
|||||||
// Assert we get shown the new room intro, and thus not the soft crash screen
|
// Assert we get shown the new room intro, and thus not the soft crash screen
|
||||||
await expect(page.locator(".mx_NewRoomIntro")).toBeVisible();
|
await expect(page.locator(".mx_NewRoomIntro")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should render spaces view", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
||||||
|
axe.disableRules([
|
||||||
|
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||||
|
"nested-interactive",
|
||||||
|
// XXX: We have some known contrast issues here
|
||||||
|
"color-contrast",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const childSpaceId1 = await app.client.createSpace({
|
||||||
|
name: "Child Space 1",
|
||||||
|
initial_state: [],
|
||||||
|
});
|
||||||
|
const childSpaceId2 = await app.client.createSpace({
|
||||||
|
name: "Child Space 2",
|
||||||
|
initial_state: [],
|
||||||
|
});
|
||||||
|
const childSpaceId3 = await app.client.createSpace({
|
||||||
|
name: "Child Space 3",
|
||||||
|
initial_state: [],
|
||||||
|
});
|
||||||
|
await app.client.createSpace({
|
||||||
|
name: "Root Space",
|
||||||
|
initial_state: [
|
||||||
|
spaceChildInitialState(childSpaceId1, "a"),
|
||||||
|
spaceChildInitialState(childSpaceId2, "b"),
|
||||||
|
spaceChildInitialState(childSpaceId3, "c"),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await app.viewSpaceByName("Root Space");
|
||||||
|
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -28,6 +28,8 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
|||||||
const OLD_NAME = "Alan";
|
const OLD_NAME = "Alan";
|
||||||
const NEW_NAME = "Alan (away)";
|
const NEW_NAME = "Alan (away)";
|
||||||
|
|
||||||
|
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
|
||||||
|
|
||||||
const getEventTilesWithBodies = (page: Page): Locator => {
|
const getEventTilesWithBodies = (page: Page): Locator => {
|
||||||
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
||||||
};
|
};
|
||||||
@@ -905,6 +907,39 @@ test.describe("Timeline", () => {
|
|||||||
mask: [page.locator(".mx_MessageTimestamp")],
|
mask: [page.locator(".mx_MessageTimestamp")],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await sendImage(app.client, room.roomId, NEW_AVATAR);
|
||||||
|
await app.timeline.scrollToBottom();
|
||||||
|
const imgTile = page.locator(".mx_MImageBody").first();
|
||||||
|
await expect(imgTile).toBeVisible();
|
||||||
|
await imgTile.hover();
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||||
@@ -1306,4 +1341,44 @@ 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
|
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
|
||||||
*/
|
*/
|
||||||
public getComposerField(isRightPanel?: boolean): Locator {
|
public getComposerField(isRightPanel?: boolean): Locator {
|
||||||
return this.getComposer(isRightPanel).locator("[contenteditable]");
|
return this.getComposer(isRightPanel).locator("div[contenteditable]");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
BIN
playwright/sample-files/5secvid.webm
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 2.3 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 |
|
After Width: | Height: | Size: 957 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 46 KiB |
BIN
playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
After 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";
|
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||||
|
|
||||||
const TAG = "develop@sha256:2ea87d45fc7ff3327c671b3b4447e6b2032d4f5ca07d62d8aef0d900e105c2f4";
|
const TAG = "develop@sha256:66955f34a593cfc3b6e77b8d5510c60c6094f5bade8a17d2feaefbb8662ccf09";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||||
|
|||||||
@@ -128,7 +128,6 @@
|
|||||||
@import "./views/dialogs/_AddExistingToSpaceDialog.pcss";
|
@import "./views/dialogs/_AddExistingToSpaceDialog.pcss";
|
||||||
@import "./views/dialogs/_AnalyticsLearnMoreDialog.pcss";
|
@import "./views/dialogs/_AnalyticsLearnMoreDialog.pcss";
|
||||||
@import "./views/dialogs/_BugReportDialog.pcss";
|
@import "./views/dialogs/_BugReportDialog.pcss";
|
||||||
@import "./views/dialogs/_BulkRedactDialog.pcss";
|
|
||||||
@import "./views/dialogs/_ChangelogDialog.pcss";
|
@import "./views/dialogs/_ChangelogDialog.pcss";
|
||||||
@import "./views/dialogs/_CompoundDialog.pcss";
|
@import "./views/dialogs/_CompoundDialog.pcss";
|
||||||
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss";
|
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss";
|
||||||
@@ -153,6 +152,7 @@
|
|||||||
@import "./views/dialogs/_ModalWidgetDialog.pcss";
|
@import "./views/dialogs/_ModalWidgetDialog.pcss";
|
||||||
@import "./views/dialogs/_PollCreateDialog.pcss";
|
@import "./views/dialogs/_PollCreateDialog.pcss";
|
||||||
@import "./views/dialogs/_RegistrationEmailPromptDialog.pcss";
|
@import "./views/dialogs/_RegistrationEmailPromptDialog.pcss";
|
||||||
|
@import "./views/dialogs/_ReportRoomDialog.pcss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialog.pcss";
|
@import "./views/dialogs/_RoomSettingsDialog.pcss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialogBridges.pcss";
|
@import "./views/dialogs/_RoomSettingsDialogBridges.pcss";
|
||||||
@import "./views/dialogs/_RoomUpgradeDialog.pcss";
|
@import "./views/dialogs/_RoomUpgradeDialog.pcss";
|
||||||
@@ -212,7 +212,6 @@
|
|||||||
@import "./views/elements/_ServerPicker.pcss";
|
@import "./views/elements/_ServerPicker.pcss";
|
||||||
@import "./views/elements/_SettingsFlag.pcss";
|
@import "./views/elements/_SettingsFlag.pcss";
|
||||||
@import "./views/elements/_Spinner.pcss";
|
@import "./views/elements/_Spinner.pcss";
|
||||||
@import "./views/elements/_StyledCheckbox.pcss";
|
|
||||||
@import "./views/elements/_StyledRadioButton.pcss";
|
@import "./views/elements/_StyledRadioButton.pcss";
|
||||||
@import "./views/elements/_SyntaxHighlight.pcss";
|
@import "./views/elements/_SyntaxHighlight.pcss";
|
||||||
@import "./views/elements/_TagComposer.pcss";
|
@import "./views/elements/_TagComposer.pcss";
|
||||||
@@ -228,6 +227,7 @@
|
|||||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||||
@import "./views/messages/_EventTileBubble.pcss";
|
@import "./views/messages/_EventTileBubble.pcss";
|
||||||
@import "./views/messages/_HiddenBody.pcss";
|
@import "./views/messages/_HiddenBody.pcss";
|
||||||
|
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
|
||||||
@import "./views/messages/_JumpToDatePicker.pcss";
|
@import "./views/messages/_JumpToDatePicker.pcss";
|
||||||
@import "./views/messages/_LegacyCallEvent.pcss";
|
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||||
@import "./views/messages/_MEmoteBody.pcss";
|
@import "./views/messages/_MEmoteBody.pcss";
|
||||||
@@ -270,6 +270,7 @@
|
|||||||
@import "./views/right_panel/_VerificationPanel.pcss";
|
@import "./views/right_panel/_VerificationPanel.pcss";
|
||||||
@import "./views/right_panel/_WidgetCard.pcss";
|
@import "./views/right_panel/_WidgetCard.pcss";
|
||||||
@import "./views/room_settings/_AliasSettings.pcss";
|
@import "./views/room_settings/_AliasSettings.pcss";
|
||||||
|
@import "./views/rooms/RoomListPanel/_EmptyRoomList.pcss";
|
||||||
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
|
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
|
||||||
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
|
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
|
||||||
@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss";
|
@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -16,9 +16,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
.mx_SelectableDeviceTile_checkbox {
|
.mx_SelectableDeviceTile_checkbox {
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
|
|
||||||
.mx_Checkbox_background + div {
|
> div {
|
||||||
flex: 1 0;
|
margin-top: auto;
|
||||||
/* override more specific selector */
|
margin-bottom: auto;
|
||||||
margin-left: $spacing-16 !important;
|
margin-right: var(--cpd-space-1x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -70,38 +70,26 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--cpd-color-text-secondary);
|
color: var(--cpd-color-text-secondary);
|
||||||
margin: 20px 0 12px;
|
margin: 20px 0 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_QuickSettingsButton_pinToSidebarHeading {
|
|
||||||
padding-left: 24px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
display: flex;
|
||||||
|
|
||||||
.mx_Checkbox {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_QuickSettingsButton_favouritesCheckbox,
|
|
||||||
.mx_QuickSettingsButton_peopleCheckbox {
|
|
||||||
.mx_Checkbox_background + div {
|
|
||||||
padding-left: 22px;
|
|
||||||
position: relative;
|
|
||||||
margin-left: 6px;
|
|
||||||
font-size: $font-15px;
|
|
||||||
line-height: $font-24px;
|
|
||||||
color: var(--cpd-color-text-primary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_QuickSettingsButton_moreOptionsButton {
|
.mx_QuickSettingsButton_moreOptionsButton {
|
||||||
padding-left: 22px;
|
margin-left: var(--cpd-space-7x);
|
||||||
margin-left: 22px;
|
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
color: var(--cpd-color-text-primary);
|
color: var(--cpd-color-text-primary);
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_QuickSettingsButton_option {
|
||||||
|
margin-bottom: var(--cpd-space-3x);
|
||||||
|
label {
|
||||||
|
/* Correctly line up icons and text. */
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_QuickSettingsButton_ContextMenuWrapper_new_room_list {
|
.mx_QuickSettingsButton_ContextMenuWrapper_new_room_list {
|
||||||
@@ -111,15 +99,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_QuickSettingsButton_icon {
|
.mx_QuickSettingsButton_icon {
|
||||||
// TODO remove when all icons have fill=currentColor
|
margin-right: var(--cpd-space-1x);
|
||||||
* {
|
|
||||||
fill: $secondary-content;
|
|
||||||
}
|
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
width: 16px;
|
width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
position: absolute;
|
margin-top: auto;
|
||||||
left: 0;
|
margin-bottom: auto;
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -247,15 +247,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
.mx_AccessibleButton_kind_primary_outline {
|
.mx_AccessibleButton_kind_primary_outline {
|
||||||
padding: 3px 16px; /* to account for the 1px border */
|
padding: 3px 16px; /* to account for the 1px border */
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Checkbox {
|
|
||||||
display: inline-flex;
|
|
||||||
|
|
||||||
label {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -32,6 +32,11 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
.mx_AddExistingToSpace_section {
|
.mx_AddExistingToSpace_section {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
// provides space for scrollbar so that checkbox and scrollbar do not collide
|
// provides space for scrollbar so that checkbox and scrollbar do not collide
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
@@ -214,6 +219,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
||||||
|
form {
|
||||||
|
/* Align checkboxes. */
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ {
|
.mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
@@ -227,8 +238,4 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Checkbox {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2021 Robin Townsend <robin@robin.town>
|
|
||||||
|
|
||||||
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_BulkRedactDialog {
|
|
||||||
.mx_Checkbox,
|
|
||||||
.mx_BulkRedactDialog_checkboxMicrocopy {
|
|
||||||
line-height: $font-20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BulkRedactDialog_checkboxMicrocopy {
|
|
||||||
margin-left: 26px;
|
|
||||||
color: $secondary-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -43,11 +43,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
.mx_Field_valid.mx_Field:focus-within {
|
.mx_Field_valid.mx_Field:focus-within {
|
||||||
border-color: $input-border-color;
|
border-color: $input-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Checkbox input[type="checkbox"]:checked + label > .mx_Checkbox_background {
|
|
||||||
background: $info-plinth-fg-color;
|
|
||||||
border-color: $info-plinth-fg-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ExportDialog_progress {
|
.mx_ExportDialog_progress {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -74,10 +74,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
color: $tertiary-content;
|
color: $tertiary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Checkbox {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
res/css/views/dialogs/_ReportRoomDialog.pcss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
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_ReportRoomDialog {
|
||||||
|
textarea {
|
||||||
|
font: var(--cpd-font-body-md-regular);
|
||||||
|
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||||
|
background: var(--cpd-color-bg-canvas-default);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -19,13 +19,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
|
|
||||||
.mx_WidgetCapabilitiesPromptDialog_byline {
|
|
||||||
color: $muted-fg-color;
|
|
||||||
margin-left: 26px;
|
|
||||||
font-size: $font-12px;
|
|
||||||
line-height: $font-12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_buttons {
|
.mx_Dialog_buttons {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -7,26 +7,5 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_LabelledCheckbox {
|
.mx_LabelledCheckbox {
|
||||||
display: flex;
|
margin-top: var(--cpd-space-2x);
|
||||||
gap: 8px;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.mx_Checkbox {
|
|
||||||
margin-top: 3px; /* visually align with label text */
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LabelledCheckbox_labels {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.mx_LabelledCheckbox_label {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LabelledCheckbox_byline {
|
|
||||||
display: block;
|
|
||||||
padding-top: $spacing-4;
|
|
||||||
color: $muted-fg-color;
|
|
||||||
font-size: $font-11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2020 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_Checkbox {
|
|
||||||
$size: $font-16px;
|
|
||||||
$border-radius: 0.27rem;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
& + label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + label > .mx_Checkbox_background {
|
|
||||||
display: inline-flex;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
height: $size;
|
|
||||||
width: $size;
|
|
||||||
size: 0.5rem;
|
|
||||||
border: 1px solid var(--cpd-color-border-interactive-primary);
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: $border-radius;
|
|
||||||
|
|
||||||
.mx_Checkbox_checkmark {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 100%;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + label > *:not(.mx_Checkbox_background) {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled + label {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
& + label .mx_Checkbox_background {
|
|
||||||
@mixin unreal-focus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Checkbox.mx_Checkbox_kind_solid input[type="checkbox"] {
|
|
||||||
& + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
|
|
||||||
background: var(--cpd-color-icon-on-solid-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked + label > .mx_Checkbox_background {
|
|
||||||
background: var(--cpd-color-bg-accent-rest);
|
|
||||||
border-color: var(--cpd-color-bg-accent-rest);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked:disabled + label > .mx_Checkbox_background {
|
|
||||||
background: var(--cpd-color-bg-action-primary-disabled);
|
|
||||||
border-color: var(--cpd-color-bg-action-primary-disabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Checkbox.mx_Checkbox_kind_outline input[type="checkbox"] {
|
|
||||||
& + label > .mx_Checkbox_background .mx_Checkbox_checkmark {
|
|
||||||
background: var(--cpd-color-bg-accent-rest);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked + label > .mx_Checkbox_background {
|
|
||||||
background: transparent;
|
|
||||||
border-color: var(--cpd-color-bg-accent-rest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
res/css/views/messages/_HiddenMediaPlaceholder.pcss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -79,39 +79,3 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
color: $imagebody-giflabel-color;
|
color: $imagebody-giflabel-color;
|
||||||
pointer-events: none;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -101,6 +101,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
margin: $spacing-12 0 $spacing-4;
|
margin: $spacing-12 0 $spacing-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_leave {
|
.mx_RoomSummaryCard_bottomOptions {
|
||||||
margin: 0 0 var(--cpd-space-8x);
|
margin: 0 0 var(--cpd-space-8x);
|
||||||
}
|
}
|
||||||
|
|||||||
33
res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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_EmptyRoomList_GenericPlaceholder {
|
||||||
|
align-self: center;
|
||||||
|
/** It should take 2/3 of the width **/
|
||||||
|
width: 66%;
|
||||||
|
/** It should be positioned at 1/3 of the height **/
|
||||||
|
padding-top: 33%;
|
||||||
|
|
||||||
|
.mx_EmptyRoomList_GenericPlaceholder_title {
|
||||||
|
font: var(--cpd-font-body-lg-semibold);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EmptyRoomList_GenericPlaceholder_description {
|
||||||
|
font: var(--cpd-font-body-sm-regular);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EmptyRoomList_DefaultPlaceholder {
|
||||||
|
margin-top: var(--cpd-space-4x);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,4 @@
|
|||||||
|
|
||||||
.mx_RoomList {
|
.mx_RoomList {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.mx_RoomList_List {
|
|
||||||
/* Avoid when on hover, the background color to be on top of the right border */
|
|
||||||
padding-right: 1px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,18 +16,22 @@
|
|||||||
*/
|
*/
|
||||||
.mx_RoomListItemView {
|
.mx_RoomListItemView {
|
||||||
all: unset;
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||||
|
|
||||||
|
.mx_RoomListItemView_content {
|
||||||
|
padding-right: var(--cpd-space-1-5x);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomListItemView_container {
|
.mx_RoomListItemView_container {
|
||||||
padding-left: var(--cpd-space-3x);
|
padding-left: var(--cpd-space-2x);
|
||||||
font: var(--cpd-font-body-md-regular);
|
font: var(--cpd-font-body-md-regular);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.mx_RoomListItemView_content {
|
.mx_RoomListItemView_content {
|
||||||
padding-right: var(--cpd-space-3x);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
/* The border is only under the room name and the future hover menu */
|
/* The border is only under the room name and the future hover menu */
|
||||||
@@ -46,4 +50,24 @@
|
|||||||
|
|
||||||
.mx_RoomListItemView_menu_open {
|
.mx_RoomListItemView_menu_open {
|
||||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -816,11 +816,13 @@ $left-gutter: 64px;
|
|||||||
.mx_EventTile_spoiler_content {
|
.mx_EventTile_spoiler_content {
|
||||||
filter: blur(5px) saturate(0.1) sepia(1);
|
filter: blur(5px) saturate(0.1) sepia(1);
|
||||||
transition-duration: 0.5s;
|
transition-duration: 0.5s;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.visible > .mx_EventTile_spoiler_content {
|
&.visible > .mx_EventTile_spoiler_content {
|
||||||
filter: none;
|
filter: none;
|
||||||
user-select: auto;
|
user-select: auto;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -393,8 +393,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_StyledRadioButton,
|
.mx_StyledRadioButton {
|
||||||
.mx_Checkbox {
|
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -14,17 +14,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SidebarUserSettingsTab_checkbox {
|
.mx_SidebarUserSettingsTab_icon {
|
||||||
margin-bottom: $spacing-8;
|
margin-right: var(--cpd-space-2x);
|
||||||
/* override checkbox styles */
|
margin-top: auto;
|
||||||
label {
|
margin-bottom: auto;
|
||||||
align-items: flex-start !important;
|
}
|
||||||
}
|
|
||||||
|
.mx_SidebarUserSettingsTab_checkbox label {
|
||||||
svg {
|
display: flex;
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
margin-right: $spacing-8;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,7 @@ getPRInfo() {
|
|||||||
|
|
||||||
# Some CIs don't give us enough info, so we just get the PR number and ask the
|
# Some CIs don't give us enough info, so we just get the PR number and ask the
|
||||||
# GH API for more info - "fork:branch". Some give us this directly.
|
# GH API for more info - "fork:branch". Some give us this directly.
|
||||||
if [ -n "$BUILDKITE_BRANCH" ]; then
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
# BuildKite
|
|
||||||
head=$BUILDKITE_BRANCH
|
|
||||||
elif [ -n "$PR_NUMBER" ]; then
|
|
||||||
# GitHub
|
# GitHub
|
||||||
getPRInfo $PR_NUMBER
|
getPRInfo $PR_NUMBER
|
||||||
elif [ -n "$REVIEW_ID" ]; then
|
elif [ -n "$REVIEW_ID" ]; then
|
||||||
@@ -79,11 +76,14 @@ if [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then
|
|||||||
clone $deforg $defrepo ${withoutPrefix%%/pr-*}
|
clone $deforg $defrepo ${withoutPrefix%%/pr-*}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Try the target branch of the push or PR.
|
# Try the target branch of the push or PR, or the branch that was pushed to
|
||||||
if [ -n "$GITHUB_BASE_REF" ]; then
|
# (ie. the 'master' branch should use matching 'master' dependencies)
|
||||||
clone $deforg $defrepo $GITHUB_BASE_REF
|
base_or_branch=$GITHUB_BASE_REF
|
||||||
elif [ -n "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" ]; then
|
if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
|
||||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
base_or_branch=${GITHUB_REF}
|
||||||
|
fi
|
||||||
|
if [ -n "$base_or_branch" ]; then
|
||||||
|
clone $deforg $defrepo $base_or_branch
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
|
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type JSXElementConstructor } from "react";
|
import { type JSX, type JSXElementConstructor } from "react";
|
||||||
|
|
||||||
export type { NonEmptyArray, XOR, Writeable } from "matrix-js-sdk/src/matrix";
|
export type { NonEmptyArray, XOR, Writeable } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type SSOAction,
|
type SSOAction,
|
||||||
encodeUnpaddedBase64,
|
encodeUnpaddedBase64,
|
||||||
type OidcRegistrationClientMetadata,
|
type OidcRegistrationClientMetadata,
|
||||||
|
MatrixEventEvent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
@@ -228,6 +229,16 @@ export default abstract class BasePlatform {
|
|||||||
window.focus();
|
window.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeHandler = (): void => notification.close();
|
||||||
|
|
||||||
|
// Clear a notification from a redacted event.
|
||||||
|
if (ev) {
|
||||||
|
ev.once(MatrixEventEvent.BeforeRedaction, closeHandler);
|
||||||
|
notification.onclose = () => {
|
||||||
|
ev.off(MatrixEventEvent.BeforeRedaction, closeHandler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type LegacyRef, type ReactNode } from "react";
|
import React, { type JSX, type LegacyRef, type ReactNode } from "react";
|
||||||
import sanitizeHtml, { type IOptions } from "sanitize-html";
|
import sanitizeHtml, { type IOptions } from "sanitize-html";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import katex from "katex";
|
import katex from "katex";
|
||||||
@@ -25,7 +25,7 @@ import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
|||||||
import { sanitizeHtmlParams, transformTags } from "./Linkify";
|
import { sanitizeHtmlParams, transformTags } from "./Linkify";
|
||||||
import { graphemeSegmenter } from "./utils/strings";
|
import { graphemeSegmenter } from "./utils/strings";
|
||||||
|
|
||||||
export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify";
|
export { Linkify, linkifyAndSanitizeHtml } from "./Linkify";
|
||||||
|
|
||||||
// Anything outside the basic multilingual plane will be a surrogate pair
|
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||||
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||||
@@ -365,53 +365,6 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bodyToDiv(
|
|
||||||
content: IContent,
|
|
||||||
highlights: Optional<string[]>,
|
|
||||||
opts: EventRenderOpts = {},
|
|
||||||
ref?: React.Ref<HTMLDivElement>,
|
|
||||||
): ReactNode {
|
|
||||||
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
|
|
||||||
|
|
||||||
return formattedBody ? (
|
|
||||||
<div
|
|
||||||
key="body"
|
|
||||||
ref={ref}
|
|
||||||
className={className}
|
|
||||||
dangerouslySetInnerHTML={{ __html: formattedBody }}
|
|
||||||
dir="auto"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div key="body" ref={ref} className={className} dir="auto">
|
|
||||||
{emojiBodyElements || strippedBody}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bodyToSpan(
|
|
||||||
content: IContent,
|
|
||||||
highlights: Optional<string[]>,
|
|
||||||
opts: EventRenderOpts = {},
|
|
||||||
ref?: React.Ref<HTMLSpanElement>,
|
|
||||||
includeDir = true,
|
|
||||||
): ReactNode {
|
|
||||||
const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts);
|
|
||||||
|
|
||||||
return formattedBody ? (
|
|
||||||
<span
|
|
||||||
key="body"
|
|
||||||
ref={ref}
|
|
||||||
className={className}
|
|
||||||
dangerouslySetInnerHTML={{ __html: formattedBody }}
|
|
||||||
dir={includeDir ? "auto" : undefined}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span key="body" ref={ref} className={className} dir={includeDir ? "auto" : undefined}>
|
|
||||||
{emojiBodyElements || strippedBody}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BodyToNodeReturn {
|
interface BodyToNodeReturn {
|
||||||
strippedBody: string;
|
strippedBody: string;
|
||||||
formattedBody?: string;
|
formattedBody?: string;
|
||||||
@@ -419,7 +372,11 @@ interface BodyToNodeReturn {
|
|||||||
className: string;
|
className: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bodyToNode(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): BodyToNodeReturn {
|
export function bodyToNode(
|
||||||
|
content: IContent,
|
||||||
|
highlights: Optional<string[]>,
|
||||||
|
opts: EventRenderOpts = {},
|
||||||
|
): BodyToNodeReturn {
|
||||||
const eventInfo = analyseEvent(content, highlights, opts);
|
const eventInfo = analyseEvent(content, highlights, opts);
|
||||||
|
|
||||||
let emojiBody = false;
|
let emojiBody = false;
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ export interface IConfigOptions {
|
|||||||
obey_asserted_identity?: boolean; // MSC3086
|
obey_asserted_identity?: boolean; // MSC3086
|
||||||
};
|
};
|
||||||
element_call: {
|
element_call: {
|
||||||
url?: string;
|
|
||||||
guest_spa_url?: string;
|
guest_spa_url?: string;
|
||||||
use_exclusively?: boolean;
|
use_exclusively?: boolean;
|
||||||
participant_limit?: number;
|
participant_limit?: number;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
type MatrixCall,
|
type MatrixCall,
|
||||||
} from "matrix-js-sdk/src/webrtc/call";
|
} from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
|
||||||
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
|
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
@@ -596,7 +595,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
|||||||
|
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case CallState.Ringing: {
|
case CallState.Ringing: {
|
||||||
const incomingCallPushRule = new PushProcessor(MatrixClientPeg.safeGet()).getPushRuleById(
|
const incomingCallPushRule = MatrixClientPeg.safeGet().pushProcessor.getPushRuleById(
|
||||||
RuleId.IncomingCall,
|
RuleId.IncomingCall,
|
||||||
);
|
);
|
||||||
const pushRuleEnabled = incomingCallPushRule?.enabled;
|
const pushRuleEnabled = incomingCallPushRule?.enabled;
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ interface ILoadSessionOpts {
|
|||||||
ignoreGuest?: boolean;
|
ignoreGuest?: boolean;
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
fragmentQueryParams?: QueryDict;
|
fragmentQueryParams?: QueryDict;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,7 +197,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
|
|
||||||
if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
|
if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
|
||||||
logger.log("Using guest access credentials");
|
logger.log("Using guest access credentials");
|
||||||
return doSetLoggedIn(
|
await doSetLoggedIn(
|
||||||
{
|
{
|
||||||
userId: fragmentQueryParams.guest_user_id as string,
|
userId: fragmentQueryParams.guest_user_id as string,
|
||||||
accessToken: fragmentQueryParams.guest_access_token as string,
|
accessToken: fragmentQueryParams.guest_access_token as string,
|
||||||
@@ -206,7 +207,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
).then(() => true);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
const success = await restoreSessionFromStorage({
|
const success = await restoreSessionFromStorage({
|
||||||
ignoreGuest: Boolean(opts.ignoreGuest),
|
ignoreGuest: Boolean(opts.ignoreGuest),
|
||||||
@@ -225,6 +227,11 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
// fall back to welcome screen
|
// fall back to welcome screen
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// We may be aborted e.g. because our token expired, so don't show an error here
|
||||||
|
if (opts.abortSignal?.aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (e instanceof AbortLoginAndRebuildStorage) {
|
if (e instanceof AbortLoginAndRebuildStorage) {
|
||||||
// If we're aborting login because of a storage inconsistency, we don't
|
// If we're aborting login because of a storage inconsistency, we don't
|
||||||
// need to show the general failure dialog. Instead, just go back to welcome.
|
// need to show the general failure dialog. Instead, just go back to welcome.
|
||||||
@@ -236,7 +243,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleLoadSessionFailure(e);
|
return handleLoadSessionFailure(e, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +413,39 @@ export function attemptTokenLogin(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the pickle key inside the credentials or create it if it does not exist for this device.
|
||||||
|
*
|
||||||
|
* @param credentials Holds the device to load/store the pickle key
|
||||||
|
*
|
||||||
|
* @returns {Promise} promise which resolves to the loaded or generated pickle key or undefined if
|
||||||
|
* none was loaded nor generated
|
||||||
|
*/
|
||||||
|
async function loadOrCreatePickleKey(credentials: IMatrixClientCreds): Promise<string | undefined> {
|
||||||
|
// Try to load the pickle key
|
||||||
|
const userId = credentials.userId;
|
||||||
|
const deviceId = credentials.deviceId;
|
||||||
|
let pickleKey = (await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "")) ?? undefined;
|
||||||
|
if (!pickleKey) {
|
||||||
|
// Create it if it did not exist
|
||||||
|
pickleKey =
|
||||||
|
userId && deviceId
|
||||||
|
? ((await PlatformPeg.get()?.createPickleKey(userId, deviceId)) ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
if (pickleKey) {
|
||||||
|
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
|
||||||
|
} else {
|
||||||
|
logger.log("Pickle key not created");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log(
|
||||||
|
`Pickle key already exists for ${credentials.userId}|${credentials.deviceId} do not create a new one`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pickleKey;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after a successful token login or OIDC authorization.
|
* Called after a successful token login or OIDC authorization.
|
||||||
* Clear storage then save new credentials in storage
|
* Clear storage then save new credentials in storage
|
||||||
@@ -413,6 +453,8 @@ export function attemptTokenLogin(
|
|||||||
*/
|
*/
|
||||||
async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise<void> {
|
async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise<void> {
|
||||||
await clearStorage();
|
await clearStorage();
|
||||||
|
// SSO does not go through setLoggedIn so we need to load/create the pickle key here too
|
||||||
|
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
|
||||||
await persistCredentials(credentials);
|
await persistCredentials(credentials);
|
||||||
|
|
||||||
// remember that we just logged in
|
// remember that we just logged in
|
||||||
@@ -621,7 +663,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessionOpts): Promise<boolean> {
|
||||||
logger.error("Unable to load session", e);
|
logger.error("Unable to load session", e);
|
||||||
|
|
||||||
const modal = Modal.createDialog(SessionRestoreErrorDialog, {
|
const modal = Modal.createDialog(SessionRestoreErrorDialog, {
|
||||||
@@ -636,7 +678,7 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// try, try again
|
// try, try again
|
||||||
return loadSession();
|
return loadSession(loadSessionOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -655,18 +697,8 @@ async function handleLoadSessionFailure(e: unknown): Promise<boolean> {
|
|||||||
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||||
credentials.freshLogin = true;
|
credentials.freshLogin = true;
|
||||||
stopMatrixClient();
|
stopMatrixClient();
|
||||||
const pickleKey =
|
credentials.pickleKey = await loadOrCreatePickleKey(credentials);
|
||||||
credentials.userId && credentials.deviceId
|
return doSetLoggedIn(credentials, true, true);
|
||||||
? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (pickleKey) {
|
|
||||||
logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`);
|
|
||||||
} else {
|
|
||||||
logger.log("Pickle key not created");
|
|
||||||
}
|
|
||||||
|
|
||||||
return doSetLoggedIn({ ...credentials, pickleKey: pickleKey ?? undefined }, true, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1124,12 +1156,13 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
|
|||||||
baseUrl: hsUrl,
|
baseUrl: hsUrl,
|
||||||
accessToken,
|
accessToken,
|
||||||
});
|
});
|
||||||
const { user_id: userId } = await tempClient.whoami();
|
const { user_id: userId, device_id: deviceId } = await tempClient.whoami();
|
||||||
await doSetLoggedIn(
|
await doSetLoggedIn(
|
||||||
{
|
{
|
||||||
homeserverUrl: hsUrl,
|
homeserverUrl: hsUrl,
|
||||||
accessToken,
|
accessToken,
|
||||||
userId,
|
userId,
|
||||||
|
deviceId,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ import sanitizeHtml, { type IOptions } from "sanitize-html";
|
|||||||
import { merge } from "lodash";
|
import { merge } from "lodash";
|
||||||
import _Linkify from "linkify-react";
|
import _Linkify from "linkify-react";
|
||||||
|
|
||||||
import {
|
import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix";
|
||||||
_linkifyElement,
|
|
||||||
_linkifyString,
|
|
||||||
ELEMENT_URL_PATTERN,
|
|
||||||
options as linkifyMatrixOptions,
|
|
||||||
} from "./linkify-matrix";
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
@@ -223,17 +218,6 @@ export function linkifyString(str: string, options = linkifyMatrixOptions): stri
|
|||||||
return _linkifyString(str, options);
|
return _linkifyString(str, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'.
|
|
||||||
*
|
|
||||||
* @param {object} element DOM element to linkify
|
|
||||||
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrixOptions
|
|
||||||
* @returns {object}
|
|
||||||
*/
|
|
||||||
export function linkifyElement(element: HTMLElement, options = linkifyMatrixOptions): HTMLElement {
|
|
||||||
return _linkifyElement(element, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linkify the given string and sanitize the HTML afterwards.
|
* Linkify the given string and sanitize the HTML afterwards.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -299,6 +299,12 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
|||||||
opts.threadSupport = true;
|
opts.threadSupport = true;
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||||
|
throw new UserFriendlyError("sliding_sync_legacy_no_longer_supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has enabled the labs feature for sliding sync, set it up
|
||||||
|
// otherwise check if the feature is supported
|
||||||
|
if (SettingsStore.getValue("feature_simplified_sliding_sync")) {
|
||||||
opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient);
|
opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient);
|
||||||
} else {
|
} else {
|
||||||
SlidingSyncManager.instance.checkSupport(this.matrixClient);
|
SlidingSyncManager.instance.checkSupport(this.matrixClient);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type Key, type MutableRefObject, type ReactElement, type RefCallback } from "react";
|
import React, { type Key, type RefObject, type ReactElement, type RefCallback } from "react";
|
||||||
|
|
||||||
interface IChildProps {
|
interface IChildProps {
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
@@ -20,7 +20,7 @@ interface IProps {
|
|||||||
// a list of state objects to apply to each child node in turn
|
// a list of state objects to apply to each child node in turn
|
||||||
startStyles: React.CSSProperties[];
|
startStyles: React.CSSProperties[];
|
||||||
|
|
||||||
innerRef?: MutableRefObject<any>;
|
innerRef?: RefObject<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number]): c is ReactElement {
|
function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number]): c is ReactElement {
|
||||||
|
|||||||
@@ -238,25 +238,25 @@ export function determineUnreadState(
|
|||||||
room?: Room,
|
room?: Room,
|
||||||
threadId?: string,
|
threadId?: string,
|
||||||
includeThreads?: boolean,
|
includeThreads?: boolean,
|
||||||
): { level: NotificationLevel; symbol: string | null; count: number } {
|
): { level: NotificationLevel; symbol: string | null; count: number; invited: boolean } {
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getUnsentMessages(room, threadId).length > 0) {
|
if (getUnsentMessages(room, threadId).length > 0) {
|
||||||
return { symbol: "!", count: 1, level: NotificationLevel.Unsent };
|
return { symbol: "!", count: 1, level: NotificationLevel.Unsent, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
||||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
|
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
|
||||||
return { symbol: "!", count: 1, level: NotificationLevel.Highlight };
|
return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
||||||
return { symbol: null, count: 0, level: NotificationLevel.None };
|
return { symbol: null, count: 0, level: NotificationLevel.None, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const redNotifs = getUnreadNotificationCount(
|
const redNotifs = getUnreadNotificationCount(
|
||||||
@@ -269,12 +269,12 @@ export function determineUnreadState(
|
|||||||
|
|
||||||
const trueCount = greyNotifs || redNotifs;
|
const trueCount = greyNotifs || redNotifs;
|
||||||
if (redNotifs > 0) {
|
if (redNotifs > 0) {
|
||||||
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight };
|
return { symbol: null, count: trueCount, level: NotificationLevel.Highlight, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const markedUnreadState = getMarkedUnreadState(room);
|
const markedUnreadState = getMarkedUnreadState(room);
|
||||||
if (greyNotifs > 0 || markedUnreadState) {
|
if (greyNotifs > 0 || markedUnreadState) {
|
||||||
return { symbol: null, count: trueCount, level: NotificationLevel.Notification };
|
return { symbol: null, count: trueCount, level: NotificationLevel.Notification, invited: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have any notified messages, but we might have unread messages. Let's find out.
|
// We don't have any notified messages, but we might have unread messages. Let's find out.
|
||||||
@@ -293,5 +293,6 @@ export function determineUnreadState(
|
|||||||
symbol: null,
|
symbol: null,
|
||||||
count: trueCount,
|
count: trueCount,
|
||||||
level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None,
|
level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None,
|
||||||
|
invited: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
|
|||||||
preferred_domain: "meet.element.io",
|
preferred_domain: "meet.element.io",
|
||||||
},
|
},
|
||||||
element_call: {
|
element_call: {
|
||||||
url: "https://call.element.io",
|
|
||||||
use_exclusively: false,
|
use_exclusively: false,
|
||||||
participant_limit: 8,
|
participant_limit: 8,
|
||||||
brand: "Element Call",
|
brand: "Element Call",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
ContentHelpers,
|
ContentHelpers,
|
||||||
Direction,
|
Direction,
|
||||||
|
|||||||
@@ -36,45 +36,51 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
* list ops)
|
* list ops)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient, ClientEvent, EventType, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
type MSC3575Filter,
|
type MSC3575Filter,
|
||||||
type MSC3575List,
|
type MSC3575List,
|
||||||
|
type MSC3575SlidingSyncResponse,
|
||||||
MSC3575_STATE_KEY_LAZY,
|
MSC3575_STATE_KEY_LAZY,
|
||||||
MSC3575_STATE_KEY_ME,
|
MSC3575_STATE_KEY_ME,
|
||||||
MSC3575_WILDCARD,
|
MSC3575_WILDCARD,
|
||||||
SlidingSync,
|
SlidingSync,
|
||||||
|
SlidingSyncEvent,
|
||||||
|
SlidingSyncState,
|
||||||
} from "matrix-js-sdk/src/sliding-sync";
|
} from "matrix-js-sdk/src/sliding-sync";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import SlidingSyncController from "./settings/controllers/SlidingSyncController";
|
|
||||||
|
|
||||||
// how long to long poll for
|
// how long to long poll for
|
||||||
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
|
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
|
||||||
|
|
||||||
|
// The state events we will get for every single room/space/old room/etc
|
||||||
|
// This list is only augmented when a direct room subscription is made. (e.g you view a room)
|
||||||
|
const REQUIRED_STATE_LIST = [
|
||||||
|
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
||||||
|
[EventType.RoomAvatar, ""], // any room avatar
|
||||||
|
[EventType.RoomCanonicalAlias, ""], // for room name calculations
|
||||||
|
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||||
|
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
||||||
|
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
||||||
|
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
||||||
|
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
||||||
|
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
||||||
|
];
|
||||||
|
|
||||||
// the things to fetch when a user clicks on a room
|
// the things to fetch when a user clicks on a room
|
||||||
const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
|
const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
|
||||||
timeline_limit: 50,
|
timeline_limit: 50,
|
||||||
// missing required_state which will change depending on the kind of room
|
// missing required_state which will change depending on the kind of room
|
||||||
include_old_rooms: {
|
include_old_rooms: {
|
||||||
timeline_limit: 0,
|
timeline_limit: 0,
|
||||||
required_state: [
|
required_state: REQUIRED_STATE_LIST,
|
||||||
// state needed to handle space navigation and tombstone chains
|
|
||||||
[EventType.RoomCreate, ""],
|
|
||||||
[EventType.RoomTombstone, ""],
|
|
||||||
[EventType.SpaceChild, MSC3575_WILDCARD],
|
|
||||||
[EventType.SpaceParent, MSC3575_WILDCARD],
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// lazy load room members so rooms like Matrix HQ don't take forever to load
|
// lazy load room members so rooms like Matrix HQ don't take forever to load
|
||||||
const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted";
|
const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted";
|
||||||
const UNENCRYPTED_SUBSCRIPTION = {
|
const UNENCRYPTED_SUBSCRIPTION = {
|
||||||
required_state: [
|
required_state: [
|
||||||
[MSC3575_WILDCARD, MSC3575_WILDCARD], // all events
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership
|
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest.
|
[EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest.
|
||||||
],
|
],
|
||||||
@@ -90,6 +96,72 @@ const ENCRYPTED_SUBSCRIPTION = {
|
|||||||
...DEFAULT_ROOM_SUBSCRIPTION_INFO,
|
...DEFAULT_ROOM_SUBSCRIPTION_INFO,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// the complete set of lists made in SSS. The manager will spider all of these lists depending
|
||||||
|
// on the count for each one.
|
||||||
|
const sssLists: Record<string, MSC3575List> = {
|
||||||
|
spaces: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 0, // we don't care about the most recent message for spaces
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
room_types: ["m.space"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invites: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
is_invite: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
favourites: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
tags: ["m.favourite"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dms: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
is_dm: true,
|
||||||
|
is_invite: false,
|
||||||
|
// If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead
|
||||||
|
not_tags: ["m.favourite", "m.lowpriority"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
untagged: {
|
||||||
|
// SSS will dupe suppress invites/dms from here, so we don't need "not dms, not invites"
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type PartialSlidingSyncRequest = {
|
export type PartialSlidingSyncRequest = {
|
||||||
filters?: MSC3575Filter;
|
filters?: MSC3575Filter;
|
||||||
sort?: string[];
|
sort?: string[];
|
||||||
@@ -103,6 +175,8 @@ export type PartialSlidingSyncRequest = {
|
|||||||
* sync options and code.
|
* sync options and code.
|
||||||
*/
|
*/
|
||||||
export class SlidingSyncManager {
|
export class SlidingSyncManager {
|
||||||
|
public static serverSupportsSlidingSync: boolean;
|
||||||
|
|
||||||
public static readonly ListSpaces = "space_list";
|
public static readonly ListSpaces = "space_list";
|
||||||
public static readonly ListSearch = "search_list";
|
public static readonly ListSearch = "search_list";
|
||||||
private static readonly internalInstance = new SlidingSyncManager();
|
private static readonly internalInstance = new SlidingSyncManager();
|
||||||
@@ -116,48 +190,17 @@ export class SlidingSyncManager {
|
|||||||
return SlidingSyncManager.internalInstance;
|
return SlidingSyncManager.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
private configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
// create the set of lists we will use.
|
||||||
|
const lists = new Map();
|
||||||
|
for (const listName in sssLists) {
|
||||||
|
lists.set(listName, sssLists[listName]);
|
||||||
|
}
|
||||||
// by default use the encrypted subscription as that gets everything, which is a safer
|
// by default use the encrypted subscription as that gets everything, which is a safer
|
||||||
// default than potentially missing member events.
|
// default than potentially missing member events.
|
||||||
this.slidingSync = new SlidingSync(
|
this.slidingSync = new SlidingSync(proxyUrl, lists, ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
|
||||||
proxyUrl,
|
|
||||||
new Map(),
|
|
||||||
ENCRYPTED_SUBSCRIPTION,
|
|
||||||
client,
|
|
||||||
SLIDING_SYNC_TIMEOUT_MS,
|
|
||||||
);
|
|
||||||
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
|
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
|
||||||
// set the space list
|
|
||||||
this.slidingSync.setList(SlidingSyncManager.ListSpaces, {
|
|
||||||
ranges: [[0, 20]],
|
|
||||||
sort: ["by_name"],
|
|
||||||
slow_get_all_rooms: true,
|
|
||||||
timeline_limit: 0,
|
|
||||||
required_state: [
|
|
||||||
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
|
||||||
[EventType.RoomAvatar, ""], // any room avatar
|
|
||||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
|
||||||
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
|
||||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
|
||||||
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
|
||||||
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
|
||||||
],
|
|
||||||
include_old_rooms: {
|
|
||||||
timeline_limit: 0,
|
|
||||||
required_state: [
|
|
||||||
[EventType.RoomCreate, ""],
|
|
||||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
|
||||||
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
|
||||||
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
|
||||||
],
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
room_types: ["m.space"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.configureDefer.resolve();
|
this.configureDefer.resolve();
|
||||||
return this.slidingSync;
|
return this.slidingSync;
|
||||||
}
|
}
|
||||||
@@ -220,99 +263,113 @@ export class SlidingSyncManager {
|
|||||||
return this.slidingSync!.getListParams(listKey)!;
|
return this.slidingSync!.getListParams(listKey)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRoomVisible(roomId: string, visible: boolean): Promise<string> {
|
/**
|
||||||
|
* Announces that the user has chosen to view the given room and that room will now
|
||||||
|
* be displayed, so it should have more state loaded.
|
||||||
|
* @param roomId The room to set visible
|
||||||
|
*/
|
||||||
|
public async setRoomVisible(roomId: string): Promise<void> {
|
||||||
await this.configureDefer.promise;
|
await this.configureDefer.promise;
|
||||||
const subscriptions = this.slidingSync!.getRoomSubscriptions();
|
const subscriptions = this.slidingSync!.getRoomSubscriptions();
|
||||||
if (visible) {
|
if (subscriptions.has(roomId)) return;
|
||||||
subscriptions.add(roomId);
|
|
||||||
} else {
|
subscriptions.add(roomId);
|
||||||
subscriptions.delete(roomId);
|
|
||||||
}
|
|
||||||
const room = this.client?.getRoom(roomId);
|
const room = this.client?.getRoom(roomId);
|
||||||
let shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId));
|
// default to safety: request all state if we can't work it out. This can happen if you
|
||||||
if (!room) {
|
// refresh the app whilst viewing a room: we call setRoomVisible before we know anything
|
||||||
// default to safety: request all state if we can't work it out. This can happen if you
|
// about the room.
|
||||||
// refresh the app whilst viewing a room: we call setRoomVisible before we know anything
|
let shouldLazyLoad = false;
|
||||||
// about the room.
|
if (room) {
|
||||||
shouldLazyLoad = false;
|
// do not lazy load encrypted rooms as we need the entire member list.
|
||||||
|
shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId));
|
||||||
}
|
}
|
||||||
logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad);
|
logger.log("SlidingSync setRoomVisible:", roomId, "shouldLazyLoad:", shouldLazyLoad);
|
||||||
if (shouldLazyLoad) {
|
if (shouldLazyLoad) {
|
||||||
// lazy load this room
|
// lazy load this room
|
||||||
this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME);
|
this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME);
|
||||||
}
|
}
|
||||||
const p = this.slidingSync!.modifyRoomSubscriptions(subscriptions);
|
this.slidingSync!.modifyRoomSubscriptions(subscriptions);
|
||||||
if (room) {
|
if (room) {
|
||||||
return roomId; // we have data already for this room, show immediately e.g it's in a list
|
return; // we have data already for this room, show immediately e.g it's in a list
|
||||||
}
|
}
|
||||||
try {
|
// wait until we know about this room. This may take a little while.
|
||||||
// wait until the next sync before returning as RoomView may need to know the current state
|
return new Promise((resolve) => {
|
||||||
await p;
|
logger.log(`SlidingSync setRoomVisible room ${roomId} not found, waiting for ClientEvent.Room`);
|
||||||
} catch {
|
const waitForRoom = (r: Room): void => {
|
||||||
logger.warn("SlidingSync setRoomVisible:", roomId, visible, "failed to confirm transaction");
|
if (r.roomId === roomId) {
|
||||||
}
|
this.client?.off(ClientEvent.Room, waitForRoom);
|
||||||
return roomId;
|
logger.log(`SlidingSync room ${roomId} found, resolving setRoomVisible`);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.client?.on(ClientEvent.Room, waitForRoom);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all rooms on the user's account. Used for pre-populating the local search cache.
|
* Retrieve all rooms on the user's account. Retrieval is gradual over time.
|
||||||
* Retrieval is gradual over time.
|
* This function MUST be called BEFORE the first sync request goes out.
|
||||||
* @param batchSize The number of rooms to return in each request.
|
* @param batchSize The number of rooms to return in each request.
|
||||||
* @param gapBetweenRequestsMs The number of milliseconds to wait between requests.
|
* @param gapBetweenRequestsMs The number of milliseconds to wait between requests.
|
||||||
*/
|
*/
|
||||||
public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise<void> {
|
private async startSpidering(
|
||||||
await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load
|
slidingSync: SlidingSync,
|
||||||
let startIndex = batchSize;
|
batchSize: number,
|
||||||
let hasMore = true;
|
gapBetweenRequestsMs: number,
|
||||||
let firstTime = true;
|
): Promise<void> {
|
||||||
while (hasMore) {
|
// The manager has created several lists (see `sssLists` in this file), all of which will be spidered simultaneously.
|
||||||
const endIndex = startIndex + batchSize - 1;
|
// There are multiple lists to ensure that we can populate invites/favourites/DMs sections immediately, rather than
|
||||||
try {
|
// potentially waiting minutes if they are all very old rooms (and hence are returned last by the server). In this
|
||||||
const ranges = [
|
// way, the lists are effectively priority requests. We don't actually care which room goes into which list at this
|
||||||
[0, batchSize - 1],
|
// point, as the RoomListStore will calculate this based on the returned data.
|
||||||
[startIndex, endIndex],
|
|
||||||
];
|
// copy the initial set of list names and ranges, we'll keep this map updated.
|
||||||
if (firstTime) {
|
const listToUpperBound = new Map(
|
||||||
await this.slidingSync!.setList(SlidingSyncManager.ListSearch, {
|
Object.keys(sssLists).map((listName) => {
|
||||||
// e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
|
return [listName, sssLists[listName].ranges[0][1]];
|
||||||
// any changes to the list whilst spidering are caught.
|
}),
|
||||||
ranges: ranges,
|
);
|
||||||
sort: [
|
console.log("startSpidering:", listToUpperBound);
|
||||||
"by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough
|
|
||||||
],
|
// listen for a response from the server. ANY 200 OK will do here, as we assume that it is ACKing
|
||||||
timeline_limit: 0, // we only care about the room details, not messages in the room
|
// the request change we have sent out. TODO: this may not be true if you concurrently subscribe to a room :/
|
||||||
required_state: [
|
// but in that case, for spidering at least, it isn't the end of the world as request N+1 includes all indexes
|
||||||
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
// from request N.
|
||||||
[EventType.RoomAvatar, ""], // any room avatar
|
const lifecycle = async (
|
||||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
state: SlidingSyncState,
|
||||||
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
_: MSC3575SlidingSyncResponse | null,
|
||||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
err?: Error,
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
): Promise<void> => {
|
||||||
],
|
if (state !== SlidingSyncState.Complete) {
|
||||||
// we don't include_old_rooms here in an effort to reduce the impact of spidering all rooms
|
return;
|
||||||
// on the user's account. This means some data in the search dialog results may be inaccurate
|
|
||||||
// e.g membership of space, but this will be corrected when the user clicks on the room
|
|
||||||
// as the direct room subscription does include old room iterations.
|
|
||||||
filters: {
|
|
||||||
// we get spaces via a different list, so filter them out
|
|
||||||
not_room_types: ["m.space"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, ranges);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// do nothing, as we reject only when we get interrupted but that's fine as the next
|
|
||||||
// request will include our data
|
|
||||||
} finally {
|
|
||||||
// gradually request more over time, even on errors.
|
|
||||||
await sleep(gapBetweenRequestsMs);
|
|
||||||
}
|
}
|
||||||
const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!;
|
await sleep(gapBetweenRequestsMs); // don't tightloop; even on errors
|
||||||
hasMore = endIndex + 1 < listData.joinedCount;
|
if (err) {
|
||||||
startIndex += batchSize;
|
return;
|
||||||
firstTime = false;
|
}
|
||||||
}
|
|
||||||
|
// for all lists with total counts > range => increase the range
|
||||||
|
let hasSetRanges = false;
|
||||||
|
listToUpperBound.forEach((currentUpperBound, listName) => {
|
||||||
|
const totalCount = slidingSync.getListData(listName)?.joinedCount || 0;
|
||||||
|
if (currentUpperBound < totalCount) {
|
||||||
|
// increment the upper bound
|
||||||
|
const newUpperBound = currentUpperBound + batchSize;
|
||||||
|
console.log(`startSpidering: ${listName} ${currentUpperBound} => ${newUpperBound}`);
|
||||||
|
listToUpperBound.set(listName, newUpperBound);
|
||||||
|
// make the next request. This will only send the request when this callback has finished, so if
|
||||||
|
// we set all the list ranges at once we will only send 1 new request.
|
||||||
|
slidingSync.setListRanges(listName, [[0, newUpperBound]]);
|
||||||
|
hasSetRanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!hasSetRanges) {
|
||||||
|
// finish spidering
|
||||||
|
slidingSync.off(SlidingSyncEvent.Lifecycle, lifecycle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
slidingSync.on(SlidingSyncEvent.Lifecycle, lifecycle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -325,42 +382,10 @@ export class SlidingSyncManager {
|
|||||||
* @returns A working Sliding Sync or undefined
|
* @returns A working Sliding Sync or undefined
|
||||||
*/
|
*/
|
||||||
public async setup(client: MatrixClient): Promise<SlidingSync | undefined> {
|
public async setup(client: MatrixClient): Promise<SlidingSync | undefined> {
|
||||||
const baseUrl = client.baseUrl;
|
const slidingSync = this.configure(client, client.baseUrl);
|
||||||
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
logger.info("Simplified Sliding Sync activated at", client.baseUrl);
|
||||||
const wellKnownProxyUrl = await this.getProxyFromWellKnown(client);
|
this.startSpidering(slidingSync, 50, 50); // 50 rooms at a time, 50ms apart
|
||||||
|
return slidingSync;
|
||||||
const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl;
|
|
||||||
|
|
||||||
this.configure(client, slidingSyncEndpoint);
|
|
||||||
logger.info("Sliding sync activated at", slidingSyncEndpoint);
|
|
||||||
this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart
|
|
||||||
|
|
||||||
return this.slidingSync;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the sliding sync proxy URL from the client well known
|
|
||||||
* @param client The MatrixClient to use
|
|
||||||
* @return The proxy url
|
|
||||||
*/
|
|
||||||
public async getProxyFromWellKnown(client: MatrixClient): Promise<string | undefined> {
|
|
||||||
let proxyUrl: string | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clientDomain = await client.getDomain();
|
|
||||||
if (clientDomain === null) {
|
|
||||||
throw new RangeError("Homeserver domain is null");
|
|
||||||
}
|
|
||||||
const clientWellKnown = await AutoDiscovery.findClientConfig(clientDomain);
|
|
||||||
proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url;
|
|
||||||
} catch {
|
|
||||||
// Either client.getDomain() is null so we've shorted out, or is invalid so `AutoDiscovery.findClientConfig` has thrown
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proxyUrl != undefined) {
|
|
||||||
logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl);
|
|
||||||
}
|
|
||||||
return proxyUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -371,9 +396,9 @@ export class SlidingSyncManager {
|
|||||||
public async nativeSlidingSyncSupport(client: MatrixClient): Promise<boolean> {
|
public async nativeSlidingSyncSupport(client: MatrixClient): Promise<boolean> {
|
||||||
// Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561
|
// Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561
|
||||||
// `client` can be undefined/null in tests for some reason.
|
// `client` can be undefined/null in tests for some reason.
|
||||||
const support = await client?.doesServerSupportUnstableFeature("org.matrix.msc3575");
|
const support = await client?.doesServerSupportUnstableFeature("org.matrix.simplified_msc3575");
|
||||||
if (support) {
|
if (support) {
|
||||||
logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable");
|
logger.log("nativeSlidingSyncSupport: org.matrix.simplified_msc3575 sliding sync advertised as unstable");
|
||||||
}
|
}
|
||||||
return support;
|
return support;
|
||||||
}
|
}
|
||||||
@@ -387,20 +412,9 @@ export class SlidingSyncManager {
|
|||||||
*/
|
*/
|
||||||
public async checkSupport(client: MatrixClient): Promise<void> {
|
public async checkSupport(client: MatrixClient): Promise<void> {
|
||||||
if (await this.nativeSlidingSyncSupport(client)) {
|
if (await this.nativeSlidingSyncSupport(client)) {
|
||||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
SlidingSyncManager.serverSupportsSlidingSync = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
SlidingSyncManager.serverSupportsSlidingSync = false;
|
||||||
const proxyUrl = await this.getProxyFromWellKnown(client);
|
|
||||||
if (proxyUrl != undefined) {
|
|
||||||
const response = await fetch(new URL("/client/server.json", proxyUrl), {
|
|
||||||
method: Method.Get,
|
|
||||||
signal: timeoutSignal(10 * 1000), // 10s
|
|
||||||
});
|
|
||||||
if (response.status === 200) {
|
|
||||||
logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl);
|
|
||||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||||||
|
|
||||||
import shouldHideEvent from "./shouldHideEvent";
|
import shouldHideEvent from "./shouldHideEvent";
|
||||||
import { haveRendererForEvent } from "./events/EventTileFactory";
|
import { haveRendererForEvent } from "./events/EventTileFactory";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import { RoomNotifState, getRoomNotifsState } from "./RoomNotifs";
|
import { RoomNotifState, getRoomNotifsState } from "./RoomNotifs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,12 +43,6 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean {
|
export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean {
|
||||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
|
||||||
// TODO: https://github.com/vector-im/element-web/issues/23207
|
|
||||||
// Sliding Sync doesn't support unread indicator dots (yet...)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toCheck: Array<Room | Thread> = [room];
|
const toCheck: Array<Room | Thread> = [room];
|
||||||
if (includeThreads) {
|
if (includeThreads) {
|
||||||
toCheck.push(...room.getThreads());
|
toCheck.push(...room.getThreads());
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
|
|||||||
* nodeRef = inputRef when inputRef argument is provided.
|
* nodeRef = inputRef when inputRef argument is provided.
|
||||||
*/
|
*/
|
||||||
export const useRovingTabIndex = <T extends HTMLElement>(
|
export const useRovingTabIndex = <T extends HTMLElement>(
|
||||||
inputRef?: RefObject<T>,
|
inputRef?: RefObject<T | null>,
|
||||||
): [FocusHandler, boolean, RefCallback<T>, RefObject<T | null>] => {
|
): [FocusHandler, boolean, RefCallback<T>, RefObject<T | null>] => {
|
||||||
const context = useContext(RovingTabIndexContext);
|
const context = useContext(RovingTabIndexContext);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
@@ -16,13 +16,12 @@ import { KeyBindingAction } from "../KeyboardShortcuts";
|
|||||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||||
|
|
||||||
interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
||||||
label?: string;
|
|
||||||
onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent
|
onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent
|
||||||
onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton
|
onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton
|
||||||
}
|
}
|
||||||
|
|
||||||
// Semantic component for representing a styled role=menuitemcheckbox
|
// Semantic component for representing a styled role=menuitemcheckbox
|
||||||
export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onChange, onClose, ...props }) => {
|
export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, onChange, onClose, ...props }) => {
|
||||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||||
|
|
||||||
const onKeyDown = (e: React.KeyboardEvent): void => {
|
const onKeyDown = (e: React.KeyboardEvent): void => {
|
||||||
@@ -63,7 +62,6 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onCh
|
|||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
{...props}
|
{...props}
|
||||||
role="menuitemcheckbox"
|
role="menuitemcheckbox"
|
||||||
aria-label={label}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type RefObject } from "react";
|
import React, { type JSX, type RefObject } from "react";
|
||||||
|
|
||||||
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
|
import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton";
|
||||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||||
|
|
||||||
type Props<T extends keyof HTMLElementTagNameMap> = Omit<ButtonProps<T>, "tabIndex"> & {
|
type Props<T extends keyof HTMLElementTagNameMap> = Omit<ButtonProps<T>, "tabIndex"> & {
|
||||||
inputRef?: RefObject<HTMLElementTagNameMap[T]>;
|
inputRef?: RefObject<HTMLElementTagNameMap[T] | null>;
|
||||||
focusOnMouseOver?: boolean;
|
focusOnMouseOver?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ReactElement, type RefCallback } from "react";
|
import { type ReactElement, type RefCallback, type RefObject } from "react";
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||||
import { type FocusHandler, type Ref } from "./types";
|
import { type FocusHandler } from "./types";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
inputRef?: Ref;
|
inputRef?: RefObject<HTMLElement | null>;
|
||||||
children(renderProps: {
|
children(renderProps: {
|
||||||
onFocus: FocusHandler;
|
onFocus: FocusHandler;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|||||||
@@ -6,8 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type RefObject } from "react";
|
|
||||||
|
|
||||||
export type Ref = RefObject<HTMLElement>;
|
|
||||||
|
|
||||||
export type FocusHandler = () => void;
|
export type FocusHandler = () => void;
|
||||||
|
|||||||