Compare commits
233 Commits
t3chguy/co
...
t3chguy/cs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20be92ab7e | ||
|
|
1afa202ef5 | ||
|
|
7e3a6d9c42 | ||
|
|
9d3cba1621 | ||
|
|
e6d572c413 | ||
|
|
ced4456c16 | ||
|
|
654a9eeb3a | ||
|
|
dc0763f58a | ||
|
|
19d8798dc0 | ||
|
|
d9f6355693 | ||
|
|
4613c44f47 | ||
|
|
773662e018 | ||
|
|
0961e6f6e4 | ||
|
|
5c2d89a0d7 | ||
|
|
40120cc226 | ||
|
|
8fef5c5ec6 | ||
|
|
9864294575 | ||
|
|
d1a0875d7f | ||
|
|
523fba271f | ||
|
|
99690f6314 | ||
|
|
58b75e0fe3 | ||
|
|
ed59a53734 | ||
|
|
b243c94093 | ||
|
|
7cd9ec4fbd | ||
|
|
9fa23d3c19 | ||
|
|
05c64bd6bc | ||
|
|
040c348700 | ||
|
|
c4d8c3e85f | ||
|
|
82390d6a23 | ||
|
|
2c2b607d3c | ||
|
|
60e574f37d | ||
|
|
d2ba05a3d8 | ||
|
|
5c28ed6738 | ||
|
|
1808a16ed3 | ||
|
|
bff234749f | ||
|
|
0733bebc38 | ||
|
|
9d9782f62b | ||
|
|
0cfaeaa3a7 | ||
|
|
4a3cf3e69d | ||
|
|
c7134e8532 | ||
|
|
1d3421417f | ||
|
|
ef63661cb0 | ||
|
|
e29da89826 | ||
|
|
d2727754e3 | ||
|
|
179cf0f8e1 | ||
|
|
de74816dd8 | ||
|
|
7b024f956d | ||
|
|
362e34513d | ||
|
|
5b900ab6e2 | ||
|
|
23fbe9cef6 | ||
|
|
cd71c109d3 | ||
|
|
a28eabf73b | ||
|
|
dbe8ad0529 | ||
|
|
b446506aee | ||
|
|
9254c4247e | ||
|
|
3d80e607ce | ||
|
|
0a1ac23681 | ||
|
|
976d1bc9ec | ||
|
|
4bd8eeb17a | ||
|
|
cff9119324 | ||
|
|
a13e9c1285 | ||
|
|
9272f0180c | ||
|
|
9d233c49f4 | ||
|
|
98af06b949 | ||
|
|
e066f3836d | ||
|
|
ea5117944c | ||
|
|
3f1831577e | ||
|
|
4fcbaaf6e1 | ||
|
|
bdeae0711a | ||
|
|
1b25e62698 | ||
|
|
9adcea3079 | ||
|
|
014a9edf0f | ||
|
|
67b0311852 | ||
|
|
df084ebe11 | ||
|
|
6ed3dc32c5 | ||
|
|
dbdf2f6353 | ||
|
|
7b8082a818 | ||
|
|
a155948231 | ||
|
|
b8f4e87185 | ||
|
|
3e928cf6a6 | ||
|
|
a2ca6f858f | ||
|
|
efe59ff35f | ||
|
|
4fda167c11 | ||
|
|
d30e6f25d3 | ||
|
|
5324834b47 | ||
|
|
63f269e52c | ||
|
|
2fb0bf6152 | ||
|
|
3b0bc0bb4a | ||
|
|
e97de7d2ea | ||
|
|
b3e8bc8fea | ||
|
|
f0738a295a | ||
|
|
7887cbb81b | ||
|
|
be8a18964e | ||
|
|
d610c3d1ae | ||
|
|
9faee160e9 | ||
|
|
242f2deb64 | ||
|
|
5607291f1e | ||
|
|
39fb67c201 | ||
|
|
71895a3891 | ||
|
|
f4e74c8dd2 | ||
|
|
d62f4d4d49 | ||
|
|
a2f5c49438 | ||
|
|
f84e2815d0 | ||
|
|
bfc2c884bc | ||
|
|
dc67863cbc | ||
|
|
e7be9d16b9 | ||
|
|
d1f45da51a | ||
|
|
52d082aed6 | ||
|
|
1a7023779c | ||
|
|
2ab9d345b4 | ||
|
|
c7fa97cc73 | ||
|
|
6a57f69cd9 | ||
|
|
e1fb8da2e4 | ||
|
|
7320e3702c | ||
|
|
386db8f385 | ||
|
|
5a9656350e | ||
|
|
046fb335c0 | ||
|
|
bb5bf5a462 | ||
|
|
916c5a0232 | ||
|
|
81b3ec9df2 | ||
|
|
a352a3838e | ||
|
|
61168f0531 | ||
|
|
3c6f3f7814 | ||
|
|
3e2ee7c829 | ||
|
|
ac399e8afd | ||
|
|
4987d6c573 | ||
|
|
c883ceeb4b | ||
|
|
421a69aede | ||
|
|
16fbb27983 | ||
|
|
319034ab7a | ||
|
|
f7e6cb6129 | ||
|
|
9dc9b169ab | ||
|
|
df5b56a2ca | ||
|
|
5370f25870 | ||
|
|
04cf53e7aa | ||
|
|
57fd3c46bb | ||
|
|
6228edcd67 | ||
|
|
4a934b105b | ||
|
|
99178bce86 | ||
|
|
2e87f7cb90 | ||
|
|
1acef76d2d | ||
|
|
10d459d209 | ||
|
|
45ab536737 | ||
|
|
afa186cdf4 | ||
|
|
44cbd260dc | ||
|
|
7ca4c8bd7f | ||
|
|
f00b643774 | ||
|
|
f46869e114 | ||
|
|
0c293bbbd0 | ||
|
|
0577e245da | ||
|
|
5620962e04 | ||
|
|
60c2482819 | ||
|
|
ebec5435e1 | ||
|
|
3112a35907 | ||
|
|
fe1505de59 | ||
|
|
1c684489da | ||
|
|
5869c519ed | ||
|
|
771696e0f0 | ||
|
|
54cbbb6ff0 | ||
|
|
69abef1f1c | ||
|
|
fd152c9c7e | ||
|
|
16e71ffd58 | ||
|
|
3a020c942a | ||
|
|
803c40c03c | ||
|
|
eeb2c0f690 | ||
|
|
32cce82790 | ||
|
|
5f07fbbc1b | ||
|
|
8753a9136f | ||
|
|
b33b1f3d01 | ||
|
|
08a1525937 | ||
|
|
135db543e1 | ||
|
|
b788e772d4 | ||
|
|
e8cf24abce | ||
|
|
4a62e87205 | ||
|
|
e7eeb98c9c | ||
|
|
c4f22cc3b3 | ||
|
|
c45d8c599c | ||
|
|
75fa94084d | ||
|
|
661b026cd9 | ||
|
|
30bd70dacf | ||
|
|
20dbcd17fa | ||
|
|
716da7784a | ||
|
|
ad9150df2d | ||
|
|
5929269e31 | ||
|
|
0d7d2fb0a4 | ||
|
|
29a54405d8 | ||
|
|
dae90a059f | ||
|
|
2f727430e1 | ||
|
|
4392aa1ed0 | ||
|
|
a721c5f4ea | ||
|
|
79f1176b92 | ||
|
|
92bb15fbba | ||
|
|
7aa7793640 | ||
|
|
f282be05ca | ||
|
|
744922cbcc | ||
|
|
7183d91930 | ||
|
|
cdedcc0b5a | ||
|
|
b679693702 | ||
|
|
fbb43d5e61 | ||
|
|
a79f6e7aa5 | ||
|
|
81c375007e | ||
|
|
aee24be1b4 | ||
|
|
1285b73be6 | ||
|
|
c203f02731 | ||
|
|
64130a018b | ||
|
|
e2fc1574bf | ||
|
|
de0492b786 | ||
|
|
0a46edaaff | ||
|
|
dd89cee328 | ||
|
|
29ff8a6199 | ||
|
|
184e6e3f29 | ||
|
|
7a01cdae0a | ||
|
|
06656a6472 | ||
|
|
5de9d5d24f | ||
|
|
0eff1caab2 | ||
|
|
b7acbe65c1 | ||
|
|
5736635a65 | ||
|
|
fcd23b48e0 | ||
|
|
250d6571fe | ||
|
|
f3a880f1c3 | ||
|
|
3d683ec5c6 | ||
|
|
81f1841aea | ||
|
|
e62125e61f | ||
|
|
c675453d72 | ||
|
|
ac0a91be9e | ||
|
|
333bec33ee | ||
|
|
f400d8db0a | ||
|
|
bb9c9982ef | ||
|
|
e2fd873f5e | ||
|
|
ac255445d1 | ||
|
|
842edc6577 | ||
|
|
faadcf902e | ||
|
|
191f951303 |
@@ -10,6 +10,7 @@ module.exports = {
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
@@ -167,6 +168,10 @@ module.exports = {
|
||||
group: ["@vector-im/compound-design-tokens/icons/*"],
|
||||
message: "Please use @vector-im/compound-design-tokens/assets/web/icons/* instead",
|
||||
},
|
||||
{
|
||||
group: ["**/packages/shared-components/**", "../packages/shared-components/**"],
|
||||
message: "Please use @element-hq/web-shared-components",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
3
.github/labels.yml
vendored
@@ -279,3 +279,6 @@
|
||||
- name: "Z-Flaky-Test-Disabled"
|
||||
description: "The flaking test has been disabled"
|
||||
color: "ededed"
|
||||
- name: "Z-Skip-Coverage"
|
||||
description: "Skip SonarQube coverage for this PR"
|
||||
color: "ededed"
|
||||
|
||||
4
.github/workflows/build.yml
vendored
@@ -42,9 +42,9 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
# Disable cache on Windows as it is slower than not caching
|
||||
# https://github.com/actions/setup-node/issues/975
|
||||
|
||||
2
.github/workflows/build_debian.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
VERSION: ${{ github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Download package
|
||||
run: |
|
||||
|
||||
4
.github/workflows/build_develop.yml
vendored
@@ -26,9 +26,9 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
2
.github/workflows/deploy.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
env:
|
||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Load GPG key
|
||||
run: |
|
||||
|
||||
6
.github/workflows/docker.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
env:
|
||||
TEST_TAG: vectorim/element-web:test
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
images: |
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
repository: vectorim/element-web
|
||||
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
repository: element-hq/element-web-pro
|
||||
|
||||
12
.github/workflows/docs.yml
vendored
@@ -17,23 +17,23 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Fetch element-desktop
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: element-hq/element-desktop
|
||||
path: element-desktop
|
||||
|
||||
- name: Fetch element-web
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
path: element-web
|
||||
|
||||
- name: Fetch matrix-js-sdk
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
path: matrix-js-sdk
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: element-web/yarn.lock
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
working-directory: element-web
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn ts-node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
|
||||
yarn node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
|
||||
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
|
||||
with:
|
||||
mdbook-version: "0.4.10"
|
||||
mdbook-version: "0.5.1"
|
||||
|
||||
- name: Install mdbook extensions
|
||||
run: cargo install mdbook-combiner mdbook-mermaid
|
||||
|
||||
12
.github/workflows/end-to-end-tests.yaml
vendored
@@ -50,11 +50,11 @@ jobs:
|
||||
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- runAllTests: false
|
||||
project: Pinecone
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
name: webapp
|
||||
path: webapp
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: yarn.lock
|
||||
@@ -194,13 +194,13 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
if: inputs.skip != true
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
if: inputs.skip != true
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Update synapse image
|
||||
run: |
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/playwright-image-updates
|
||||
|
||||
6
.github/workflows/release_prepare.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
REPOS: matrix-js-sdk element-web element-desktop
|
||||
steps:
|
||||
- name: Checkout Element Desktop
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
if: inputs.element-desktop
|
||||
with:
|
||||
repository: element-hq/element-desktop
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
- name: Checkout Element Web
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
if: inputs.element-web
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
- name: Checkout Matrix JS SDK
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
if: inputs.matrix-js-sdk
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: 🔧 Set up node environment
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: ".node-version"
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
22
.github/workflows/static_analysis.yaml
vendored
@@ -22,9 +22,9 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
@@ -73,9 +73,9 @@ jobs:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -97,9 +97,9 @@ jobs:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -115,9 +115,9 @@ jobs:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -133,9 +133,9 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
8
.github/workflows/tests.yml
vendored
@@ -39,12 +39,12 @@ jobs:
|
||||
runner: [1, 2]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "yarn"
|
||||
@@ -118,12 +118,12 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "yarn"
|
||||
|
||||
2
.github/workflows/triage-stale.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
with:
|
||||
operations-per-run: 100
|
||||
|
||||
|
||||
6
.github/workflows/update-jitsi.yml
vendored
@@ -9,9 +9,9 @@ jobs:
|
||||
update:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
run: "yarn update:jitsi"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/jitsi-update
|
||||
|
||||
27
.github/workflows/update-topics.yaml
vendored
@@ -26,13 +26,13 @@ jobs:
|
||||
env:
|
||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||
PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org"
|
||||
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
|
||||
PUBLIC_DISCUSSION_ROOM_ID: "!xUW4PpAe1CmThA3r2wI8IrgwwsK006-zqWdJCljpd10"
|
||||
ANNOUNCEMENT_ROOM_ID: "!ars5ndgI6IIYZXECiJ-u8YljHNzShJn3nHdB-3rYI2M"
|
||||
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
|
||||
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
|
||||
with:
|
||||
script: |
|
||||
const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
|
||||
const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_DISCUSSION_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
|
||||
|
||||
const repo = context.repo;
|
||||
const { data } = await github.rest.repos.getLatestRelease({
|
||||
@@ -71,18 +71,23 @@ jobs:
|
||||
const data = await res.json();
|
||||
console.log(roomId, "got event", data);
|
||||
|
||||
if (!regex.test(data.topic)) {
|
||||
core.setFailed("Topic format is incorrect for room " + roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const topic = data.topic.replace(regex, releaseTopic);
|
||||
if (topic === data.topic) {
|
||||
console.log(roomId, "nothing to do");
|
||||
return;
|
||||
}
|
||||
if (data["org.matrix.msc3765.topic"]) {
|
||||
data["org.matrix.msc3765.topic"].forEach(d => {
|
||||
data["org.matrix.msc3765.topic"]?.["m.text"].forEach(d => {
|
||||
d.body = d.body.replace(regex, releaseTopic);
|
||||
});
|
||||
}
|
||||
if (data["m.topic"]) {
|
||||
data["m.topic"].forEach(d => {
|
||||
data["m.topic"]?.["m.text"].forEach(d => {
|
||||
d.body = d.body.replace(regex, releaseTopic);
|
||||
});
|
||||
}
|
||||
@@ -97,12 +102,18 @@ jobs:
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
console.log(roomId, "topic updated:", topic);
|
||||
const resJson = res.json();
|
||||
if (resJson.errcode) {
|
||||
core.setFailed(`Error updating ${roomId}: ${resJson.error}`);
|
||||
} else {
|
||||
console.log(roomId, await res.text());
|
||||
console.log(roomId, "topic updated:", topic);
|
||||
}
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
core.setFailed(`Error updating ${roomId}: ${errText}`);
|
||||
}
|
||||
}
|
||||
|
||||
await updateReleaseInTopic(LOBBY_ROOM_ID);
|
||||
await updateReleaseInTopic(PUBLIC_ROOM_ID);
|
||||
await updateReleaseInTopic(PUBLIC_DISCUSSION_ROOM_ID);
|
||||
await updateReleaseInTopic(ANNOUNCEMENT_ROOM_ID);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
/dist
|
||||
/lib
|
||||
/node_modules
|
||||
/packages/
|
||||
/webapp
|
||||
/*.log
|
||||
yarn.lock
|
||||
|
||||
68
CHANGELOG.md
@@ -1,3 +1,71 @@
|
||||
Changes in [1.12.6](https://github.com/element-hq/element-web/releases/tag/v1.12.6) (2025-12-03)
|
||||
================================================================================================
|
||||
This release fixes a bug where 1:1 calling was incorrectly not available if no Element Call focus was set.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Add option to pick call options for voice calls. ([#31413](https://github.com/element-hq/element-web/pull/31413)).
|
||||
|
||||
Changes in [1.12.5](https://github.com/element-hq/element-web/releases/tag/v1.12.5) (2025-12-02)
|
||||
================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Update Emojibase to v17 ([#31307](https://github.com/element-hq/element-web/pull/31307)). Contributed by @t3chguy.
|
||||
* Adds tooltip for compose menu ([#31122](https://github.com/element-hq/element-web/pull/31122)). Contributed by @byteplow.
|
||||
* Add option to hide pinned message banner in room view ([#31296](https://github.com/element-hq/element-web/pull/31296)). Contributed by @florianduros.
|
||||
* update twemoji to not monochromise emoji with BLACK in their name ([#31281](https://github.com/element-hq/element-web/pull/31281)). Contributed by @ara4n.
|
||||
* upgrade to twemoji 17.0.2 and fix #14695 ([#31267](https://github.com/element-hq/element-web/pull/31267)). Contributed by @ara4n.
|
||||
* Add options to hide right panel in room view ([#31252](https://github.com/element-hq/element-web/pull/31252)). Contributed by @florianduros.
|
||||
* Delayed event management: split endpoints, no auth ([#31183](https://github.com/element-hq/element-web/pull/31183)). Contributed by @AndrewFerr.
|
||||
* Support using Element Call for voice calls in DMs ([#30817](https://github.com/element-hq/element-web/pull/30817)). Contributed by @Half-Shot.
|
||||
* Improve screen reader accessibility of auth pages ([#31236](https://github.com/element-hq/element-web/pull/31236)). Contributed by @t3chguy.
|
||||
* Add posthog tracking for key backup toasts ([#31195](https://github.com/element-hq/element-web/pull/31195)). Contributed by @Half-Shot.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Return to using Fira Code as the default monospace font ([#31302](https://github.com/element-hq/element-web/pull/31302)). Contributed by @ara4n.
|
||||
* Fix case of home screen being displayed erroneously ([#31301](https://github.com/element-hq/element-web/pull/31301)). Contributed by @langleyd.
|
||||
* Fix message edition and reply when multiple rooms at displayed the same moment ([#31280](https://github.com/element-hq/element-web/pull/31280)). Contributed by @florianduros.
|
||||
* Key storage out of sync: reset key backup when needed ([#31279](https://github.com/element-hq/element-web/pull/31279)). Contributed by @uhoreg.
|
||||
* Fix invalid events crashing entire room rather than just their tile ([#31256](https://github.com/element-hq/element-web/pull/31256)). Contributed by @t3chguy.
|
||||
* Fix expand button of space panel getting cut off at the edges ([#31259](https://github.com/element-hq/element-web/pull/31259)). Contributed by @MidhunSureshR.
|
||||
* Fix pill buttons in dialogs ([#31246](https://github.com/element-hq/element-web/pull/31246)). Contributed by @dbkr.
|
||||
* Fix blank sections at the top and bottom of the member list when scrolling ([#31198](https://github.com/element-hq/element-web/pull/31198)). Contributed by @langleyd.
|
||||
* Fix emoji category selection with keyboard ([#31162](https://github.com/element-hq/element-web/pull/31162)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [1.12.4](https://github.com/element-hq/element-web/releases/tag/v1.12.4) (2025-11-18)
|
||||
================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Apply aria-hidden to emoji in SAS verification ([#31204](https://github.com/element-hq/element-web/pull/31204)). Contributed by @t3chguy.
|
||||
* Add options to hide header and composer of room view for the module api ([#31095](https://github.com/element-hq/element-web/pull/31095)). Contributed by @florianduros.
|
||||
* Experimental Module API Additions ([#30863](https://github.com/element-hq/element-web/pull/30863)). Contributed by @dbkr.
|
||||
* Change polls to use fieldset/legend markup ([#31160](https://github.com/element-hq/element-web/pull/31160)). Contributed by @langleyd.
|
||||
* Use compound Button styles for Jitsi button ([#31159](https://github.com/element-hq/element-web/pull/31159)). Contributed by @Half-Shot.
|
||||
* Add FocusLock to emoji picker ([#31146](https://github.com/element-hq/element-web/pull/31146)). Contributed by @langleyd.
|
||||
* Move room name, avatar, and topic to IOpts. ([#30981](https://github.com/element-hq/element-web/pull/30981)). Contributed by @kaylendog.
|
||||
* Add a devtool for looking at users and their devices ([#30983](https://github.com/element-hq/element-web/pull/30983)). Contributed by @uhoreg.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix room list handling of membership changes ([#31197](https://github.com/element-hq/element-web/pull/31197)). Contributed by @t3chguy.
|
||||
* Fix room list unable to be resized when displayed after a module ([#31186](https://github.com/element-hq/element-web/pull/31186)). Contributed by @florianduros.
|
||||
* Inhibit keyboard highlights in dialogs when effector is not in focus ([#31181](https://github.com/element-hq/element-web/pull/31181)). Contributed by @t3chguy.
|
||||
* Strip mentions from forwarded messages ([#30884](https://github.com/element-hq/element-web/pull/30884)). Contributed by @twassman.
|
||||
* Don't allow pin or edit of messages with a send status ([#31158](https://github.com/element-hq/element-web/pull/31158)). Contributed by @langleyd.
|
||||
* Hide room header buttons if the room hasn't been created yet. ([#31092](https://github.com/element-hq/element-web/pull/31092)). Contributed by @Half-Shot.
|
||||
* Fix screen readers not indicating the emoji picker search field is focused. ([#31128](https://github.com/element-hq/element-web/pull/31128)). Contributed by @langleyd.
|
||||
* Fix emoji picker highlight missing when not active element ([#31148](https://github.com/element-hq/element-web/pull/31148)). Contributed by @t3chguy.
|
||||
* Add relevant aria attribute for selected emoji in the emoji picker ([#31125](https://github.com/element-hq/element-web/pull/31125)). Contributed by @t3chguy.
|
||||
* Fix tooltips within context menu portals being unreliable ([#31129](https://github.com/element-hq/element-web/pull/31129)). Contributed by @t3chguy.
|
||||
* Avoid excessive re-render of room list and member list ([#31131](https://github.com/element-hq/element-web/pull/31131)). Contributed by @florianduros.
|
||||
* Make emoji picker height responsive. ([#31130](https://github.com/element-hq/element-web/pull/31130)). Contributed by @langleyd.
|
||||
* Emoji Picker: Focused emoji does not move with the arrow keys ([#30893](https://github.com/element-hq/element-web/pull/30893)). Contributed by @langleyd.
|
||||
* Fix audio player seek bar position ([#31127](https://github.com/element-hq/element-web/pull/31127)). Contributed by @florianduros.
|
||||
* Add aria label to emoji picker search ([#31126](https://github.com/element-hq/element-web/pull/31126)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [1.12.3](https://github.com/element-hq/element-web/releases/tag/v1.12.3) (2025-11-04)
|
||||
================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.19-labs@sha256:dce1c693ef318bca08c964ba3122ae6248e45a1b96d65c4563c8dc6fe80349a2
|
||||
# syntax=docker.io/docker/dockerfile:1.20-labs@sha256:dbcde2ebc4abc8bb5c3c499b9c9a6876842bf5da243951cd2697f921a7aeb6a9
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:de951ccb5f52277af681a421e3328760fc4d22fbf20c391d78ef85af58430df6 AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:5583cbe5d3347db372d9a9100eba272b548ca1f53246b9b769334bcd0eef2c26 AS builder
|
||||
|
||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||
ARG USE_CUSTOM_SDKS=false
|
||||
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
|
||||
# App
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:e2b324ae5571b5ea49ddeb03c966b240f43e5ecbdf73adcd528b49399fe11ad6
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:a6bec37058b9047ece799c01d98dc6d5aa0542b6583cc69f187652f91331a752
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
title = "Element Web & Desktop"
|
||||
authors = ["New Vector Ltd.", "The Matrix.org Foundation C.I.C."]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
|
||||
# The directory that documentation files are stored in
|
||||
src = "docs"
|
||||
|
||||
@@ -79,7 +79,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
11. If a variable is not receiving a value on declaration, its type must be defined.
|
||||
|
||||
```typescript
|
||||
let errorMessage: Optional<string>;
|
||||
let errorMessage: string;
|
||||
```
|
||||
|
||||
12. Objects can use shorthand declarations, including mixing of types.
|
||||
|
||||
@@ -43,7 +43,7 @@ const config: Config = {
|
||||
"counterpart": "<rootDir>/node_modules/counterpart",
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
|
||||
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)).+$",
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||
|
||||
2
knip.ts
@@ -42,6 +42,8 @@ export default {
|
||||
"util",
|
||||
// Embedded into webapp
|
||||
"@element-hq/element-call-embedded",
|
||||
// Transitive dep of jest
|
||||
"jsdom",
|
||||
|
||||
// Used by matrix-js-sdk, which means we have to include them as a
|
||||
// dependency so that // we can run `tsc` (since we import the typescript
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import * as YAML from "yaml";
|
||||
import * as fs from "fs";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
export type BuildConfig = {
|
||||
// Dev note: make everything here optional for user safety. Invalid
|
||||
|
||||
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as childProcess from "child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as childProcess from "node:child_process";
|
||||
import * as semver from "semver";
|
||||
|
||||
import { type BuildConfig } from "./BuildConfig";
|
||||
import { type BuildConfig } from "./BuildConfig.ts";
|
||||
|
||||
// This expects to be run from ./scripts/install.ts
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { readBuildConfig } from "../BuildConfig";
|
||||
import { installer } from "../installer";
|
||||
import { readBuildConfig } from "../BuildConfig.ts";
|
||||
import { installer } from "../installer.ts";
|
||||
|
||||
const buildConf = readBuildConfig();
|
||||
installer(buildConf);
|
||||
|
||||
63
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.12.3",
|
||||
"version": "1.12.6",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -38,16 +38,16 @@
|
||||
"clean": "rimraf lib webapp",
|
||||
"build": "yarn clean && yarn build:genfiles && yarn build:bundle",
|
||||
"build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
|
||||
"build:res": "ts-node scripts/copy-res.ts",
|
||||
"build:res": "node scripts/copy-res.ts",
|
||||
"build:genfiles": "yarn build:res && yarn build:module_system",
|
||||
"build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
|
||||
"build:bundle": "webpack --progress --mode production",
|
||||
"build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json",
|
||||
"build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts",
|
||||
"build:module_system": "node module_system/scripts/install.ts",
|
||||
"dist": "./scripts/package.sh",
|
||||
"start": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n modules,res \"yarn build:module_system\" \"yarn build:res\" && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"",
|
||||
"start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --server-type https\"",
|
||||
"start:res": "ts-node scripts/copy-res.ts -w",
|
||||
"start:res": "node scripts/copy-res.ts -w",
|
||||
"start:js": "webpack serve --output-path webapp --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js --mode development",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src test playwright module_system && prettier --check .",
|
||||
@@ -69,32 +69,31 @@
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.2.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"oidc-client-ts": "3.3.0",
|
||||
"**/pretty-format/react-is": "19.2.1",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"oidc-client-ts": "3.4.1",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001754",
|
||||
"caniuse-lite": "1.0.30001759",
|
||||
"testcontainers": "^11.0.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@element-hq/element-web-module-api": "1.5.0",
|
||||
"@element-hq/element-web-module-api": "1.9.0",
|
||||
"@element-hq/web-shared-components": "link:packages/shared-components",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/fira-code": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
"@matrix-org/analytics-events": "^0.30.0",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||
"@matrix-org/emojibase-bindings": "^1.5.0",
|
||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^10.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.1.2",
|
||||
"@vector-im/compound-design-tokens": "6.4.2",
|
||||
"@vector-im/compound-web": "^8.3.1",
|
||||
"@vector-im/matrix-wysiwyg": "2.40.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
@@ -109,7 +108,7 @@
|
||||
"diff-dom": "^5.0.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"domutils": "^3.2.2",
|
||||
"emojibase-regex": "15.3.2",
|
||||
"emojibase-regex": "^17.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "11.0.13",
|
||||
@@ -118,8 +117,8 @@
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"html-react-parser": "^5.2.2",
|
||||
"is-ip": "^3.1.0",
|
||||
"js-xxhash": "^4.0.0",
|
||||
"is-ip": "^5.0.0",
|
||||
"js-xxhash": "^5.0.0",
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
@@ -130,16 +129,15 @@
|
||||
"lodash": "^4.17.21",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.290.0",
|
||||
"posthog-js": "1.302.2",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
@@ -181,14 +179,14 @@
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.4.0",
|
||||
"@element-hq/element-call-embedded": "0.16.1",
|
||||
"@casualbot/jest-sonar-reporter": "2.5.0",
|
||||
"@element-hq/element-call-embedded": "0.16.3",
|
||||
"@element-hq/element-web-playwright-common": "^2.0.0",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@sentry/webpack-plugin": "^4.0.0",
|
||||
"@storybook/react-vite": "^9.1.10",
|
||||
"@storybook/react-vite": "^10.0.7",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@svgr/webpack": "^8.0.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
@@ -215,9 +213,9 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/sdp-transform": "^2.4.10",
|
||||
@@ -230,7 +228,7 @@
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"blob-polyfill": "^9.0.0",
|
||||
"chokidar": "^4.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"concurrently": "^9.0.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"core-js": "^3.38.1",
|
||||
@@ -283,25 +281,24 @@
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.7.4",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.0",
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"storybook": "^9.1.10",
|
||||
"storybook": "^10.0.7",
|
||||
"stylelint": "^16.23.0",
|
||||
"stylelint-config-standard": "^39.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^11.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-bundle-analyzer": "^5.0.0",
|
||||
"webpack-cli": "^6.0.0",
|
||||
"webpack-dev-server": "^5.0.0",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||
@@ -314,7 +311,7 @@
|
||||
"relativePaths": true
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
"node": ">=22.18"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, { useLayoutEffect } from "react";
|
||||
import { setLanguage } from "../src/utils/i18n";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { StoryContext } from "storybook/internal/csf";
|
||||
import { I18nApi, I18nContext } from "../src";
|
||||
|
||||
export const globalTypes = {
|
||||
theme: {
|
||||
@@ -70,9 +71,17 @@ const withTooltipProvider: Decorator = (Story) => {
|
||||
);
|
||||
};
|
||||
|
||||
const withI18nProvider: Decorator = (Story) => {
|
||||
return (
|
||||
<I18nContext.Provider value={new I18nApi()}>
|
||||
<Story />
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
tags: ["autodocs"],
|
||||
decorators: [withThemeProvider, withTooltipProvider],
|
||||
decorators: [withThemeProvider, withTooltipProvider, withI18nProvider],
|
||||
parameters: {
|
||||
options: {
|
||||
storySort: {
|
||||
|
||||
@@ -5,17 +5,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.
|
||||
*/
|
||||
|
||||
import { waitForPageReady } from "@storybook/test-runner";
|
||||
import { waitForPageReady, TestRunnerConfig } from "@storybook/test-runner";
|
||||
import { toMatchImageSnapshot } from "jest-image-snapshot";
|
||||
|
||||
const customSnapshotsDir = `${process.cwd()}/playwright/snapshots/`;
|
||||
const customReceivedDir = `${process.cwd()}/playwright/received/`;
|
||||
|
||||
/**
|
||||
* @type {import('@storybook/test-runner').TestRunnerConfig}
|
||||
*/
|
||||
const config = {
|
||||
setup(page) {
|
||||
const config: TestRunnerConfig = {
|
||||
setup() {
|
||||
expect.extend({ toMatchImageSnapshot });
|
||||
},
|
||||
async postVisit(page, context) {
|
||||
@@ -30,7 +30,7 @@ const config: Config = {
|
||||
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
|
||||
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|@storybook|storybook)).+$",
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@element-hq/web-shared-components",
|
||||
"version": "0.0.0-test.7",
|
||||
"version": "0.0.0-test.12",
|
||||
"description": "Shared components for Element",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -43,9 +43,14 @@
|
||||
"lint:types": "tsc --noEmit --jsx react",
|
||||
"test:storybook": "test-storybook --url http://localhost:6007/",
|
||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
|
||||
"test:storybook:update": "playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
|
||||
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
|
||||
},
|
||||
"resolutions": {
|
||||
"playwright": "1.57.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-hq/element-web-module-api": "^1.8.0",
|
||||
"@vector-im/compound-design-tokens": "^6.3.0",
|
||||
"classnames": "^2.5.1",
|
||||
"counterpart": "^0.18.6",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -56,27 +61,28 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-hq/element-web-playwright-common": "^2.0.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@storybook/addon-a11y": "^9.1.10",
|
||||
"@storybook/addon-designs": "^10.0.2",
|
||||
"@storybook/addon-docs": "^9.1.10",
|
||||
"@storybook/icons": "^1.6.0",
|
||||
"@storybook/react-vite": "^9.1.10",
|
||||
"@storybook/test-runner": "^0.23.0",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@storybook/addon-a11y": "^10.0.7",
|
||||
"@storybook/addon-designs": "^11.0.1",
|
||||
"@storybook/addon-docs": "^10.0.7",
|
||||
"@storybook/icons": "^2.0.0",
|
||||
"@storybook/react-vite": "^10.0.7",
|
||||
"@storybook/test-runner": "^0.24.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/counterpart": "^0.18.4",
|
||||
"@types/jest-image-snapshot": "^6.4.0",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/react": "^19.2.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "8",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-storybook": "^10.0.0",
|
||||
"eslint-plugin-storybook": "^10.0.7",
|
||||
"jest": "^30.2.0",
|
||||
"jest-image-snapshot": "^6.5.1",
|
||||
"patch-package": "^8.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"storybook": "^9.1.10",
|
||||
"storybook": "^10.0.7",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
@@ -87,7 +93,6 @@
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"peerDependencies": {
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -14,6 +14,8 @@ import { fireEvent } from "@testing-library/dom";
|
||||
import * as stories from "./AudioPlayerView.stories.tsx";
|
||||
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
|
||||
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
|
||||
import { I18nContext } from "../../utils/i18nContext.ts";
|
||||
import { I18nApi } from "../../index.ts";
|
||||
|
||||
const { Default, NoMediaName, NoSize, HasError } = composeStories(stories);
|
||||
|
||||
@@ -64,7 +66,9 @@ describe("AudioPlayerView", () => {
|
||||
error: false,
|
||||
});
|
||||
|
||||
render(<AudioPlayerView vm={vm} />);
|
||||
render(<AudioPlayerView vm={vm} />, {
|
||||
wrapper: ({ children }) => <I18nContext.Provider value={new I18nApi()}>{children}</I18nContext.Provider>,
|
||||
});
|
||||
await user.click(screen.getByRole("button", { name: "Play" }));
|
||||
expect(togglePlay).toHaveBeenCalled();
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Flex } from "../../utils/Flex";
|
||||
import styles from "./AudioPlayerView.module.css";
|
||||
import { PlayPauseButton } from "../PlayPauseButton";
|
||||
import { type PlaybackState } from "../playback";
|
||||
import { _t } from "../../utils/i18n";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
import { formatBytes } from "../../utils/FormattingUtils";
|
||||
import { Clock } from "../Clock";
|
||||
import { SeekBar } from "../SeekBar";
|
||||
@@ -90,6 +90,8 @@ interface AudioPlayerViewProps {
|
||||
* ```
|
||||
*/
|
||||
export function AudioPlayerView({ vm }: Readonly<AudioPlayerViewProps>): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
|
||||
const {
|
||||
playbackState,
|
||||
mediaName = _t("timeline|m.audio|unnamed_audio"),
|
||||
|
||||
@@ -23,7 +23,7 @@ exports[`AudioPlayerView renders the audio player in default state 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
@@ -114,7 +114,7 @@ exports[`AudioPlayerView renders the audio player in error state 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
@@ -210,7 +210,7 @@ exports[`AudioPlayerView renders the audio player without media name 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
@@ -301,7 +301,7 @@ exports[`AudioPlayerView renders the audio player without size 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -11,7 +11,7 @@ import Play from "@vector-im/compound-design-tokens/assets/web/icons/play-solid"
|
||||
import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
|
||||
|
||||
import styles from "./PlayPauseButton.module.css";
|
||||
import { _t } from "../../utils/i18n";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
|
||||
export interface PlayPauseButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
@@ -46,6 +46,8 @@ export function PlayPauseButton({
|
||||
togglePlay,
|
||||
...rest
|
||||
}: Readonly<PlayPauseButtonProps>): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
|
||||
const label = playing ? _t("action|pause") : _t("action|play");
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,7 +13,7 @@ exports[`PlayPauseButton renders the button in default state 1`] = `
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
@@ -45,7 +45,7 @@ exports[`PlayPauseButton renders the button in playing state 1`] = `
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -10,7 +10,7 @@ import { throttle } from "lodash";
|
||||
import classNames from "classnames";
|
||||
|
||||
import style from "./SeekBar.module.css";
|
||||
import { _t } from "../../utils/i18n";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
|
||||
export interface SeekBarProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
/**
|
||||
@@ -33,6 +33,8 @@ interface ISeekCSS extends CSSProperties {
|
||||
* ```
|
||||
*/
|
||||
export function SeekBar({ value = 0, className, ...rest }: Readonly<SeekBarProps>): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
|
||||
const [newValue, setNewValue] = useState(value);
|
||||
// Throttle the value setting to avoid excessive re-renders
|
||||
const setThrottledValue = useMemo(() => throttle(setNewValue, 10), []);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--cpd-color-gradient-critical-linear: linear-gradient(
|
||||
180deg,
|
||||
var(--cpd-color-alpha-red-500) 0%,
|
||||
var(--cpd-color-alpha-red-400) 20%,
|
||||
var(--cpd-color-alpha-red-300) 40%,
|
||||
var(--cpd-color-alpha-red-200) 60%,
|
||||
var(--cpd-color-alpha-red-100) 80%,
|
||||
var(--cpd-color-transparent) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.banner {
|
||||
container-type: inline-size;
|
||||
container-name: banner;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: var(--cpd-space-3x);
|
||||
padding: var(--cpd-space-4x);
|
||||
|
||||
border-top: 1px solid var(--cpd-color-gray-400);
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.banner[data-type="success"] {
|
||||
background: var(--cpd-color-gradient-subtle-linear);
|
||||
border-color: var(--cpd-color-green-900);
|
||||
}
|
||||
|
||||
.banner[data-type="critical"] {
|
||||
background: var(--cpd-color-gradient-critical-linear);
|
||||
border-color: var(--cpd-color-border-critical-primary);
|
||||
}
|
||||
|
||||
.banner[data-type="info"] {
|
||||
background: var(--cpd-color-gradient-info-linear);
|
||||
border-color: var(--cpd-color-blue-900);
|
||||
}
|
||||
|
||||
.banner[data-type="info"] :is(svg) {
|
||||
color: var(--cpd-color-blue-900);
|
||||
}
|
||||
|
||||
.banner[data-type="success"] :is(.content, svg) {
|
||||
color: var(--cpd-color-green-900);
|
||||
}
|
||||
|
||||
.banner[data-type="critical"] :is(.content, svg) {
|
||||
color: var(--cpd-color-red-900);
|
||||
}
|
||||
|
||||
.banner p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
/* lock icon dimensions */
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
|
||||
margin: 4px;
|
||||
|
||||
/* centre svg icons, as they are not full width */
|
||||
flex: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
|
||||
flex: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--cpd-space-1x);
|
||||
align-self: center;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fn } from "storybook/test";
|
||||
import { type Meta, type StoryObj } from "@storybook/react-vite";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import { Banner } from "./Banner";
|
||||
import { _t } from "../../utils/i18n";
|
||||
|
||||
const meta = {
|
||||
title: "room/Banner",
|
||||
component: Banner,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
children: <p>Hello! This is a status banner.</p>,
|
||||
onClose: fn(),
|
||||
},
|
||||
} satisfies Meta<typeof Banner>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
type: "info",
|
||||
},
|
||||
};
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
type: "success",
|
||||
},
|
||||
};
|
||||
export const Critical: Story = {
|
||||
args: {
|
||||
type: "critical",
|
||||
},
|
||||
};
|
||||
export const WithAction: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<p>
|
||||
{_t(
|
||||
"encryption|pinned_identity_changed",
|
||||
{ displayName: "Alice", userId: "@alice:example.org" },
|
||||
{
|
||||
a: (sub) => <a href="https://example.org">{sub}</a>,
|
||||
b: (sub) => <b>{sub}</b>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
),
|
||||
actions: <Button kind="primary">{_t("encryption|withdraw_verification_action")}</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAvatarImage: Story = {
|
||||
args: {
|
||||
avatar: <img alt="Example" src="https://picsum.photos/32/32" />,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutClose: Story = {
|
||||
args: {
|
||||
onClose: undefined,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
|
||||
import * as stories from "./Banner.stories.tsx";
|
||||
|
||||
const { Default, Info, Success, WithAction, WithAvatarImage, Critical } = composeStories(stories);
|
||||
|
||||
describe("AvatarWithDetails", () => {
|
||||
it("renders a default banner", () => {
|
||||
const { container } = render(<Default />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a info banner", () => {
|
||||
const { container } = render(<Info />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a success banner", () => {
|
||||
const { container } = render(<Success />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a critical banner", () => {
|
||||
const { container } = render(<Critical />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a banner with an action", () => {
|
||||
const { container } = render(<WithAction />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a banner with an avatar iamge", () => {
|
||||
const { container } = render(<WithAvatarImage />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
93
packages/shared-components/src/composer/Banner/Banner.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, {
|
||||
type MouseEventHandler,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
type PropsWithChildren,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle";
|
||||
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
||||
import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info";
|
||||
|
||||
import styles from "./Banner.module.css";
|
||||
import { _t } from "../../utils/i18n";
|
||||
|
||||
interface BannerProps {
|
||||
/**
|
||||
* The type of the status banner.
|
||||
*/
|
||||
type?: "success" | "info" | "critical";
|
||||
|
||||
/**
|
||||
* The banner avatar.
|
||||
*/
|
||||
avatar?: React.ReactNode;
|
||||
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Actions presented to the user in the right-hand side of the banner alongside the dismiss button.
|
||||
*/
|
||||
actions?: ReactNode;
|
||||
/**
|
||||
* Called when the user presses the "dismiss" button.
|
||||
*/
|
||||
onClose?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A banner component used for displaying user-facing information above the message composer.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Banner onClose={onCloseHandler} />
|
||||
* ```
|
||||
*/
|
||||
export function Banner({
|
||||
type,
|
||||
children,
|
||||
avatar,
|
||||
className,
|
||||
actions,
|
||||
onClose,
|
||||
...props
|
||||
}: PropsWithChildren<BannerProps>): ReactElement {
|
||||
const classes = classNames(styles.banner, className);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
switch (type) {
|
||||
case "critical":
|
||||
return <ErrorIcon fontSize={24} {...props} />;
|
||||
case "info":
|
||||
return <InfoIcon fontSize={24} {...props} />;
|
||||
case "success":
|
||||
return <CheckCircleIcon fontSize={24} {...props} />;
|
||||
default:
|
||||
return <InfoIcon fontSize={24} {...props} />;
|
||||
}
|
||||
}, [type, props]);
|
||||
|
||||
return (
|
||||
<div {...props} className={classes} data-type={type}>
|
||||
<div className={styles.icon}>{avatar ?? icon}</div>
|
||||
<span className={styles.content}>{children}</span>
|
||||
<div className={styles.actions}>
|
||||
{actions}
|
||||
{onClose && (
|
||||
<Button kind="secondary" size="sm" onClick={onClose}>
|
||||
{_t("action|dismiss")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`AvatarWithDetails renders a banner with an action 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
font-size="24"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
encryption|pinned_identity_changed
|
||||
</p>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
encryption|withdraw_verification_action
|
||||
</button>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AvatarWithDetails renders a banner with an avatar iamge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
>
|
||||
<img
|
||||
alt="Example"
|
||||
src="https://picsum.photos/32/32"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AvatarWithDetails renders a critical banner 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
font-size="24"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AvatarWithDetails renders a default banner 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
font-size="24"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AvatarWithDetails renders a info banner 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
data-type="info"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
font-size="24"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AvatarWithDetails renders a success banner 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
data-type="success"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
font-size="24"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m10.6 13.8-2.15-2.15a.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275.95.95 0 0 0-.275.7q0 .425.275.7L9.9 15.9q.3.3.7.3t.7-.3l5.65-5.65a.95.95 0 0 0 .275-.7.95.95 0 0 0-.275-.7.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275zM12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
8
packages/shared-components/src/composer/Banner/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export * from "./Banner";
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import { type Meta, type StoryFn } from "@storybook/react-vite";
|
||||
import React, { type JSX } from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import { useMockedViewModel } from "../../useMockedViewModel";
|
||||
import {
|
||||
HistoryVisibleBannerView,
|
||||
type HistoryVisibleBannerViewActions,
|
||||
type HistoryVisibleBannerViewSnapshot,
|
||||
} from "./HistoryVisibleBannerView";
|
||||
|
||||
type HistoryVisibleBannerProps = HistoryVisibleBannerViewSnapshot & HistoryVisibleBannerViewActions;
|
||||
|
||||
const HistoryVisibleBannerViewWrapper = ({ onClose, ...rest }: HistoryVisibleBannerProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(rest, {
|
||||
onClose,
|
||||
});
|
||||
return <HistoryVisibleBannerView vm={vm} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "composer/HistoryVisibleBannerView",
|
||||
component: HistoryVisibleBannerViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
args: {
|
||||
visible: true,
|
||||
onClose: fn(),
|
||||
},
|
||||
} as Meta<typeof HistoryVisibleBannerViewWrapper>;
|
||||
|
||||
const Template: StoryFn<typeof HistoryVisibleBannerViewWrapper> = (args) => (
|
||||
<HistoryVisibleBannerViewWrapper {...args} />
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
|
||||
import * as stories from "./HistoryVisibleBannerView.stories.tsx";
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
|
||||
describe("HistoryVisibleBannerView", () => {
|
||||
it("renders a history visible banner", () => {
|
||||
const dismissFn = jest.fn();
|
||||
|
||||
const { container } = render(<Default onClose={dismissFn} />);
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
const button = container.querySelector("button");
|
||||
expect(button).not.toBeNull();
|
||||
button?.click();
|
||||
expect(dismissFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Link } from "@vector-im/compound-web";
|
||||
import React, { type JSX } from "react";
|
||||
|
||||
import { useViewModel } from "../../useViewModel";
|
||||
import { _t } from "../../utils/i18n";
|
||||
import { type ViewModel } from "../../viewmodel";
|
||||
import { Banner } from "../Banner";
|
||||
|
||||
export interface HistoryVisibleBannerViewActions {
|
||||
/**
|
||||
* Called when the user dismisses the banner.
|
||||
*/
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface HistoryVisibleBannerViewSnapshot {
|
||||
/**
|
||||
* Whether the banner is currently visible.
|
||||
*/
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The view model for the banner.
|
||||
*/
|
||||
export type HistoryVisibleBannerViewModel = ViewModel<HistoryVisibleBannerViewSnapshot> &
|
||||
HistoryVisibleBannerViewActions;
|
||||
|
||||
interface HistoryVisibleBannerViewProps {
|
||||
/**
|
||||
* The view model for the banner.
|
||||
*/
|
||||
vm: HistoryVisibleBannerViewModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component to alert that history is shared to new members of the room.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <HistoryVisibleBannerView vm={historyVisibleBannerViewModel} />
|
||||
* ```
|
||||
*/
|
||||
export function HistoryVisibleBannerView({ vm }: Readonly<HistoryVisibleBannerViewProps>): JSX.Element {
|
||||
const { visible } = useViewModel(vm);
|
||||
|
||||
const contents = _t(
|
||||
"room|status_bar|history_visible",
|
||||
{},
|
||||
{
|
||||
a: substituteATag,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
<Banner type="info" onClose={() => vm.onClose()}>
|
||||
{contents}
|
||||
</Banner>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function substituteATag(sub: string): JSX.Element {
|
||||
return (
|
||||
<Link href="https://element.io/en/help#e2ee-history-sharing" target="_blank">
|
||||
{sub}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`HistoryVisibleBannerView renders a history visible banner 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
data-type="info"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
font-size="24"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<span>
|
||||
Messages you send will be shared with new members invited to this room.
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
data-size="medium"
|
||||
href="https://element.io/en/help#e2ee-history-sharing"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export * from "./HistoryVisibleBannerView";
|
||||
@@ -11,21 +11,26 @@ export * from "./audio/Clock";
|
||||
export * from "./audio/PlayPauseButton";
|
||||
export * from "./audio/SeekBar";
|
||||
export * from "./avatar/AvatarWithDetails";
|
||||
export * from "./composer/Banner";
|
||||
export * from "./composer/HistoryVisibleBannerView";
|
||||
export * from "./event-tiles/TextualEventView";
|
||||
export * from "./message-body/MediaBody";
|
||||
export * from "./pill-input/Pill";
|
||||
export * from "./pill-input/PillInput";
|
||||
export * from "./rich-list/RichItem";
|
||||
export * from "./rich-list/RichList";
|
||||
export * from "./room-list/RoomListSearchView";
|
||||
export * from "./utils/Box";
|
||||
export * from "./utils/Flex";
|
||||
|
||||
// Utils
|
||||
export * from "./utils/i18n";
|
||||
export * from "./utils/i18nContext";
|
||||
export * from "./utils/humanize";
|
||||
export * from "./utils/DateUtils";
|
||||
export * from "./utils/numbers";
|
||||
export * from "./utils/FormattingUtils";
|
||||
export * from "./utils/I18nApi";
|
||||
|
||||
// MVVM
|
||||
export * from "./viewmodel";
|
||||
|
||||
@@ -12,7 +12,7 @@ import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"
|
||||
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import styles from "./Pill.module.css";
|
||||
import { _t } from "../../utils/i18n";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
|
||||
export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
|
||||
/**
|
||||
@@ -39,6 +39,7 @@ export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick
|
||||
*/
|
||||
export function Pill({ className, children, label, onClick, ...props }: PropsWithChildren<PillProps>): JSX.Element {
|
||||
const id = useId();
|
||||
const { translate: _t } = useI18n();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -53,7 +54,13 @@ export function Pill({ className, children, label, onClick, ...props }: PropsWit
|
||||
{label}
|
||||
</span>
|
||||
{onClick && (
|
||||
<IconButton aria-describedby={id} size="16px" onClick={onClick} aria-label={_t("action|delete")}>
|
||||
<IconButton
|
||||
aria-describedby={id}
|
||||
size="16px"
|
||||
onClick={onClick}
|
||||
aria-label={_t("action|delete")}
|
||||
className="mx_Dialog_nonDialogButton"
|
||||
>
|
||||
<CloseIcon color="var(--cpd-color-icon-tertiary)" />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -18,14 +18,14 @@ exports[`Pill renders the pill 1`] = `
|
||||
<button
|
||||
aria-describedby="_r_0_"
|
||||
aria-label="Delete"
|
||||
class="_icon-button_1pz9o_8"
|
||||
class="_icon-button_1pz9o_8 mx_Dialog_nonDialogButton"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 16px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import React from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import { RichItem } from "./RichItem";
|
||||
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||
import { RichItem } from "./RichItem";
|
||||
|
||||
const currentTimestamp = new Date("2025-03-09T12:00:00Z").getTime();
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import React, { type HTMLAttributes, type JSX, memo } from "react";
|
||||
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||
|
||||
import styles from "./RichItem.module.css";
|
||||
import { humanizeTime } from "../../utils/humanize";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
|
||||
export interface RichItemProps extends HTMLAttributes<HTMLLIElement> {
|
||||
/**
|
||||
@@ -63,6 +63,8 @@ export const RichItem = memo(function RichItem({
|
||||
selected,
|
||||
...props
|
||||
}: RichItemProps): JSX.Element {
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<li
|
||||
className={styles.richItem}
|
||||
@@ -77,7 +79,7 @@ export const RichItem = memo(function RichItem({
|
||||
<span className={styles.description}>{description}</span>
|
||||
{timestamp && (
|
||||
<span role="timer" className={styles.timestamp}>
|
||||
{humanizeTime(timestamp)}
|
||||
{i18n.humanizeTime(timestamp)}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.view {
|
||||
/* From figma, this should be aligned with the room header */
|
||||
min-height: 64px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
|
||||
padding: 0 var(--cpd-space-3x);
|
||||
}
|
||||
|
||||
.search {
|
||||
/* The search button should take all the remaining space */
|
||||
flex: 1;
|
||||
/* !important is needed to override compound button in EW */
|
||||
font: var(--cpd-font-body-md-regular) !important;
|
||||
color: var(--cpd-color-text-secondary) !important;
|
||||
min-width: 0;
|
||||
|
||||
svg {
|
||||
fill: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.search_container {
|
||||
flex: 1;
|
||||
|
||||
/* Shrink and truncate the search text */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
kbd {
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.search_text {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: start;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||
import {
|
||||
RoomListSearchView,
|
||||
type RoomListSearchViewActions,
|
||||
type RoomListSearchViewSnapshot,
|
||||
} from "./RoomListSearchView";
|
||||
import { useMockedViewModel } from "../../useMockedViewModel";
|
||||
|
||||
type RoomListSearchProps = RoomListSearchViewSnapshot & RoomListSearchViewActions;
|
||||
|
||||
const RoomListSearchViewWrapper = ({
|
||||
onSearchClick,
|
||||
onDialPadClick,
|
||||
onExploreClick,
|
||||
...rest
|
||||
}: RoomListSearchProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(rest, {
|
||||
onSearchClick,
|
||||
onDialPadClick,
|
||||
onExploreClick,
|
||||
});
|
||||
return <RoomListSearchView vm={vm} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Room List/RoomListSearchView",
|
||||
component: RoomListSearchViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
displayExploreButton: true,
|
||||
displayDialButton: false,
|
||||
searchShortcut: "⌘ K",
|
||||
onSearchClick: fn(),
|
||||
onDialPadClick: fn(),
|
||||
onExploreClick: fn(),
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4",
|
||||
},
|
||||
},
|
||||
} as Meta<typeof RoomListSearchViewWrapper>;
|
||||
|
||||
const Template: StoryFn<typeof RoomListSearchViewWrapper> = (args) => <RoomListSearchViewWrapper {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
export const WithDialPad = Template.bind({});
|
||||
WithDialPad.args = {
|
||||
displayDialButton: true,
|
||||
};
|
||||
|
||||
export const WithoutExplore = Template.bind({});
|
||||
WithoutExplore.args = {
|
||||
displayExploreButton: false,
|
||||
};
|
||||
|
||||
export const AllButtons = Template.bind({});
|
||||
AllButtons.args = {
|
||||
displayExploreButton: true,
|
||||
displayDialButton: true,
|
||||
searchShortcut: "⌘ K",
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import * as stories from "./RoomListSearchView.stories";
|
||||
import {
|
||||
RoomListSearchView,
|
||||
type RoomListSearchViewActions,
|
||||
type RoomListSearchViewSnapshot,
|
||||
} from "./RoomListSearchView";
|
||||
import { MockViewModel } from "../../viewmodel/MockViewModel";
|
||||
|
||||
const { Default, WithDialPad, WithoutExplore, AllButtons } = composeStories(stories);
|
||||
|
||||
describe("RoomListSearchView", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Storybook snapshots", () => {
|
||||
it("renders the default state", () => {
|
||||
const { container } = render(<Default />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with dial pad button", () => {
|
||||
const { container } = render(<WithDialPad />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without explore button", () => {
|
||||
const { container } = render(<WithoutExplore />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with all buttons visible", () => {
|
||||
const { container } = render(<AllButtons />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("User interactions", () => {
|
||||
const onSearchClick = jest.fn();
|
||||
const onDialPadClick = jest.fn();
|
||||
const onExploreClick = jest.fn();
|
||||
|
||||
class TestViewModel extends MockViewModel<RoomListSearchViewSnapshot> implements RoomListSearchViewActions {
|
||||
public onSearchClick = onSearchClick;
|
||||
public onDialPadClick = onDialPadClick;
|
||||
public onExploreClick = onExploreClick;
|
||||
}
|
||||
|
||||
it("should call onSearchClick when search button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const vm = new TestViewModel({
|
||||
displayExploreButton: false,
|
||||
displayDialButton: false,
|
||||
searchShortcut: "⌘ K",
|
||||
});
|
||||
|
||||
render(<RoomListSearchView vm={vm} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Search ⌘ K" }));
|
||||
expect(onSearchClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onDialPadClick when dial pad button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const vm = new TestViewModel({
|
||||
displayExploreButton: false,
|
||||
displayDialButton: true,
|
||||
searchShortcut: "⌘ K",
|
||||
});
|
||||
|
||||
render(<RoomListSearchView vm={vm} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Open dial pad" }));
|
||||
expect(onDialPadClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onExploreClick when explore button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const vm = new TestViewModel({
|
||||
displayExploreButton: true,
|
||||
displayDialButton: false,
|
||||
searchShortcut: "⌘ K",
|
||||
});
|
||||
|
||||
render(<RoomListSearchView vm={vm} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Explore rooms" }));
|
||||
expect(onExploreClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, type MouseEventHandler } from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import ExploreIcon from "@vector-im/compound-design-tokens/assets/web/icons/explore";
|
||||
import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/search";
|
||||
import DialPadIcon from "@vector-im/compound-design-tokens/assets/web/icons/dial-pad";
|
||||
|
||||
import styles from "./RoomListSearchView.module.css";
|
||||
import { type ViewModel } from "../../viewmodel/ViewModel";
|
||||
import { useViewModel } from "../../useViewModel";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
|
||||
export interface RoomListSearchViewSnapshot {
|
||||
/**
|
||||
* Whether to display the explore button.
|
||||
*/
|
||||
displayExploreButton: boolean;
|
||||
/**
|
||||
* Whether to display the dial pad button.
|
||||
*/
|
||||
displayDialButton: boolean;
|
||||
/**
|
||||
* The keyboard shortcut text to display for the search action.
|
||||
* For example: "⌘ K" on macOS or "Ctrl K" on other platforms.
|
||||
*/
|
||||
searchShortcut: string;
|
||||
}
|
||||
|
||||
export interface RoomListSearchViewActions {
|
||||
/**
|
||||
* Handles the click event on the search button.
|
||||
*/
|
||||
onSearchClick: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Handles the click event on the dial pad button.
|
||||
*/
|
||||
onDialPadClick: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Handles the click event on the explore button.
|
||||
*/
|
||||
onExploreClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The view model for the room list search component.
|
||||
*/
|
||||
export type RoomListSearchViewModel = ViewModel<RoomListSearchViewSnapshot> & RoomListSearchViewActions;
|
||||
|
||||
interface RoomListSearchViewProps {
|
||||
/**
|
||||
* The view model for the room list search component.
|
||||
*/
|
||||
vm: RoomListSearchViewModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* A search component to be displayed at the top of the room list.
|
||||
* The component provides search functionality, optional dial pad access, and optional room exploration.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <RoomListSearchView vm={roomListSearchViewModel} />
|
||||
* ```
|
||||
*/
|
||||
export function RoomListSearchView({ vm }: Readonly<RoomListSearchViewProps>): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
const { displayExploreButton, displayDialButton, searchShortcut } = useViewModel(vm);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
data-testid="room-list-search"
|
||||
className={styles.view}
|
||||
role="search"
|
||||
gap="var(--cpd-space-2x)"
|
||||
align="center"
|
||||
>
|
||||
<Button
|
||||
id="room-list-search-button"
|
||||
className={styles.search}
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={SearchIcon}
|
||||
onClick={vm.onSearchClick}
|
||||
>
|
||||
<Flex className={styles["search_container"]} as="span" justify="space-between">
|
||||
<span className={styles["search_text"]}>{_t("action|search")}</span>
|
||||
<kbd>{searchShortcut}</kbd>
|
||||
</Flex>
|
||||
</Button>
|
||||
{displayDialButton && (
|
||||
<Button
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={DialPadIcon}
|
||||
iconOnly={true}
|
||||
aria-label={_t("left_panel|open_dial_pad")}
|
||||
onClick={vm.onDialPadClick}
|
||||
/>
|
||||
)}
|
||||
{displayExploreButton && (
|
||||
<Button
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={ExploreIcon}
|
||||
iconOnly={true}
|
||||
aria-label={_t("action|explore_rooms")}
|
||||
onClick={vm.onExploreClick}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders the default state 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8 search _has-icon_187yx_57"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Explore rooms"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _icon-only_187yx_50"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 12q0-.424.287-.713A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 12 13m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20m0 0q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675T12 20m1.675-5.85q.15-.075.275-.2t.2-.275l2.925-6.25q.125-.25-.062-.437-.188-.188-.438-.063l-6.25 2.925q-.15.075-.275.2t-.2.275l-2.925 6.25q-.125.25.063.438.186.186.437.062z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders with all buttons visible 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8 search _has-icon_187yx_57"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Open dial pad"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _icon-only_187yx_50"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 18.6c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M6.6 2.4c-.99 0-1.8.81-1.8 1.8S5.61 6 6.6 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M17.4 6c.99 0 1.8-.81 1.8-1.8s-.81-1.8-1.8-1.8-1.8.81-1.8 1.8.81 1.8 1.8 1.8M12 13.2c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m-5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8S11.01 6 12 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Explore rooms"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _icon-only_187yx_50"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 12q0-.424.287-.713A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 12 13m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20m0 0q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675T12 20m1.675-5.85q.15-.075.275-.2t.2-.275l2.925-6.25q.125-.25-.062-.437-.188-.188-.438-.063l-6.25 2.925q-.15.075-.275.2t-.2.275l-2.925 6.25q-.125.25.063.438.186.186.437.062z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders with dial pad button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8 search _has-icon_187yx_57"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Open dial pad"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _icon-only_187yx_50"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 18.6c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M6.6 2.4c-.99 0-1.8.81-1.8 1.8S5.61 6 6.6 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M17.4 6c.99 0 1.8-.81 1.8-1.8s-.81-1.8-1.8-1.8-1.8.81-1.8 1.8.81 1.8 1.8 1.8M12 13.2c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m-5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8S11.01 6 12 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Explore rooms"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _icon-only_187yx_50"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 12q0-.424.287-.713A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 12 13m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20m0 0q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675T12 20m1.675-5.85q.15-.075.275-.2t.2-.275l2.925-6.25q.125-.25-.062-.437-.188-.188-.438-.063l-6.25 2.925q-.15.075-.275.2t-.2.275l-2.925 6.25q-.125.25.063.438.186.186.437.062z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders without explore button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8 search _has-icon_187yx_57"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type { RoomListSearchViewModel, RoomListSearchViewSnapshot } from "./RoomListSearchView";
|
||||
export { RoomListSearchView } from "./RoomListSearchView";
|
||||
@@ -16,16 +16,24 @@ import React, { type ReactElement } from "react";
|
||||
import { render, type RenderOptions } from "@testing-library/react";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
import { I18nApi, I18nContext } from "../..";
|
||||
|
||||
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
|
||||
return ({ children }: { children: React.ReactNode }) => {
|
||||
if (Wrapper) {
|
||||
return (
|
||||
<I18nContext.Provider value={new I18nApi()}>
|
||||
<Wrapper>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</Wrapper>
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
} else {
|
||||
return <TooltipProvider>{children}</TooltipProvider>;
|
||||
return (
|
||||
<I18nContext.Provider value={new I18nApi()}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
22
packages/shared-components/src/utils/I18nApi.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type TranslationKey } from "../i18nKeys";
|
||||
import { I18nApi } from "./I18nApi";
|
||||
|
||||
describe("I18nApi", () => {
|
||||
it("can register a translation and use it", () => {
|
||||
const i18n = new I18nApi();
|
||||
i18n.register({
|
||||
"hello.world": {
|
||||
en: "Hello, World!",
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.translate("hello.world" as TranslationKey)).toBe("Hello, World!");
|
||||
});
|
||||
});
|
||||
@@ -6,16 +6,17 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api";
|
||||
import { registerTranslations } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t, getCurrentLanguage, type TranslationKey } from "../languageHandler.tsx";
|
||||
import { humanizeTime } from "./humanize";
|
||||
import { _t, getLocale, registerTranslations } from "./i18n";
|
||||
import { type TranslationKey } from "../i18nKeys";
|
||||
|
||||
export class I18nApi implements II18nApi {
|
||||
/**
|
||||
* Read the current language of the user in IETF Language Tag format
|
||||
*/
|
||||
public get language(): string {
|
||||
return getCurrentLanguage();
|
||||
return getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,4 +45,8 @@ export class I18nApi implements II18nApi {
|
||||
public translate(key: TranslationKey, variables?: Variables): string {
|
||||
return _t(key, variables);
|
||||
}
|
||||
|
||||
public humanizeTime(timeMillis: number): string {
|
||||
return humanizeTime(timeMillis, this);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { _t } from "./i18n";
|
||||
import { type I18nApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
import { _t as _tFromModule } from "./i18n";
|
||||
|
||||
// These are the constants we use for when to break the text
|
||||
const MILLISECONDS_RECENT = 15000;
|
||||
@@ -21,13 +23,15 @@ const HOURS_1_DAY = 26;
|
||||
* @param {number} timeMillis The time in millis to compare against.
|
||||
* @returns {string} The humanized time.
|
||||
*/
|
||||
export function humanizeTime(timeMillis: number): string {
|
||||
export function humanizeTime(timeMillis: number, i18nApi?: I18nApi): string {
|
||||
const now = Date.now();
|
||||
let msAgo = now - timeMillis;
|
||||
const minutes = Math.abs(Math.ceil(msAgo / 60000));
|
||||
const hours = Math.ceil(minutes / 60);
|
||||
const days = Math.ceil(hours / 24);
|
||||
|
||||
const _t = i18nApi?.translate ?? _tFromModule;
|
||||
|
||||
if (msAgo >= 0) {
|
||||
// Past
|
||||
if (msAgo <= MILLISECONDS_RECENT) return _t("time|few_seconds_ago");
|
||||
|
||||
27
packages/shared-components/src/utils/i18nContext.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
import { type I18nApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
export const I18nContext = createContext<I18nApi | null>(null);
|
||||
I18nContext.displayName = "I18nContext";
|
||||
|
||||
/**
|
||||
* A hook to get the i18n API from the context. Will throw if no i18n context is found.
|
||||
* @throws If no i18n context is found
|
||||
* @returns The i18n API from the context
|
||||
*/
|
||||
export function useI18n(): I18nApi {
|
||||
const i18n = useContext(I18nContext);
|
||||
|
||||
if (!i18n) {
|
||||
throw new Error("useI18n must be used within an I18nContext.Provider");
|
||||
}
|
||||
|
||||
return i18n;
|
||||
}
|
||||
@@ -11,3 +11,4 @@ export * from "./Snapshot";
|
||||
export * from "./ViewModelSubscriptions";
|
||||
export type * from "./ViewModel";
|
||||
export * from "./MockViewModel";
|
||||
export * from "./useCreateAutoDisposedViewModel";
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { renderHook } from "jest-matrix-react";
|
||||
|
||||
import { BaseViewModel } from "../BaseViewModel";
|
||||
import { useCreateAutoDisposedViewModel } from "../useCreateAutoDisposedViewModel";
|
||||
|
||||
class TestViewModel extends BaseViewModel<{ count: number }, { initial: number }> {
|
||||
public constructor(props: { initial: number }) {
|
||||
super(props, { count: props.initial });
|
||||
}
|
||||
|
||||
public increment(): void {
|
||||
const newCount = this.getSnapshot().count + 1;
|
||||
this.snapshot.set({ count: newCount });
|
||||
}
|
||||
}
|
||||
|
||||
describe("useAutoDisposedViewModel", () => {
|
||||
it("should return view-model", () => {
|
||||
const vmCreator = (): TestViewModel => new TestViewModel({ initial: 0 });
|
||||
const { result } = renderHook(() => useCreateAutoDisposedViewModel(vmCreator));
|
||||
const vm = result.current;
|
||||
expect(vm).toBeInstanceOf(TestViewModel);
|
||||
expect(vm.isDisposed).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it("should dispose view-model on unmount", () => {
|
||||
const vmCreator = (): TestViewModel => new TestViewModel({ initial: 0 });
|
||||
const { result, unmount } = renderHook(() => useCreateAutoDisposedViewModel(vmCreator));
|
||||
const vm = result.current;
|
||||
vm.increment();
|
||||
unmount();
|
||||
expect(vm.isDisposed).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it("should recreate view-model on react strict mode", async () => {
|
||||
const vmCreator = (): TestViewModel => new TestViewModel({ initial: 0 });
|
||||
const output = renderHook(() => useCreateAutoDisposedViewModel(vmCreator), { reactStrictMode: true });
|
||||
const vm = output.result.current;
|
||||
expect(vm.isDisposed).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { BaseViewModel } from "./BaseViewModel";
|
||||
|
||||
type VmCreator<B extends BaseViewModel<unknown, unknown>> = () => B;
|
||||
|
||||
/**
|
||||
* Instantiate a view-model that gets disposed when the calling react component unmounts.
|
||||
* In other words, this hook ties the lifecycle of a view-model to the lifecycle of a
|
||||
* react component.
|
||||
*
|
||||
* @param vmCreator A function that returns a view-model instance
|
||||
* @returns view-model instance from vmCreator
|
||||
* @example
|
||||
* const vm = useCreateAutoDisposedViewModel(() => new FooViewModel({prop1, prop2, ...});
|
||||
*/
|
||||
export function useCreateAutoDisposedViewModel<B extends BaseViewModel<unknown, unknown>>(vmCreator: VmCreator<B>): B {
|
||||
/**
|
||||
* The view-model instance may be replaced by a different instance in some scenarios.
|
||||
* We want to be sure that whichever react component called this hook gets re-rendered
|
||||
* when this happens, hence the state.
|
||||
*/
|
||||
const [viewModel, setViewModel] = useState<B>(vmCreator);
|
||||
|
||||
/**
|
||||
* Our intention here is to ensure that the dispose method of the view-model gets called
|
||||
* when the component that uses this hook unmounts.
|
||||
* We can do that by combining a useEffect cleanup with an empty dependency array.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let toDispose = viewModel;
|
||||
|
||||
/**
|
||||
* Because we use react strict mode, react will run our effects twice in dev mode to make
|
||||
* sure that they are pure.
|
||||
* This presents a complication - the vm instance that we created in our state initializer
|
||||
* will get disposed on the first cleanup.
|
||||
* So we'll recreate the view-model if it's already disposed.
|
||||
*/
|
||||
if (viewModel.isDisposed) {
|
||||
const newViewModel = vmCreator();
|
||||
// Change toDispose so that we don't end up disposing the already disposed vm.
|
||||
toDispose = newViewModel;
|
||||
setViewModel(newViewModel);
|
||||
}
|
||||
return () => {
|
||||
// Dispose the view-model when this component unmounts
|
||||
toDispose.dispose();
|
||||
};
|
||||
|
||||
/**
|
||||
* We explicitly provide an empty dependency array as we don't expect the viewModel/viewCreator to
|
||||
* change.
|
||||
* Or to put it in another way, the only reason to use this hook is to create/dispose the view-model
|
||||
* and that is something that should only happen at the start/end of the lifecycle of this component.
|
||||
*/
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
|
||||
// Pressing Control+F6 again will focus room search
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
// Pressing Control+F6 again will focus the message composer
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
@@ -44,7 +44,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
await expect(page.locator(".mx_HomePage")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||
@@ -75,7 +75,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
|
||||
// Pressing Control+F6 again will focus room search
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
// Pressing Control+F6 again will focus the room tile in the room list
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
@@ -97,7 +97,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
await expect(page.locator(".mx_RoomListItemView_selected")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||
@@ -131,7 +131,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
|
||||
// Pressing Control+F6 again will focus room search
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
// Pressing Control+F6 again will focus the room tile in the room list
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
@@ -153,7 +153,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
await expect(page.locator(".mx_RoomListItemView")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||
|
||||
@@ -49,7 +49,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
/**
|
||||
* Take snapshots of mx_EventTile_last on each layout, outputting log for reference/debugging.
|
||||
* @param detail The snapshot name. Used for outputting logs too.
|
||||
* @param monospace This changes the font used to render the UI from a default one to Inconsolata. Set to false by default.
|
||||
* @param monospace This changes the font used to render the UI from a default one to Fira Code. Set to false by default.
|
||||
*/
|
||||
const takeSnapshots = async (page: Page, app: ElementAppPage, detail: string, monospace = false) => {
|
||||
// Check that the audio player is rendered and its button becomes visible
|
||||
@@ -65,7 +65,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
if (monospace) {
|
||||
// Assert that the monospace timer is visible
|
||||
await expect(locator.locator("[role='timer']")).toHaveCSS("font-family", "Inconsolata");
|
||||
await expect(locator.locator("[role='timer']")).toHaveCSS("font-family", '"Fira Code"');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// Enable system font and monospace setting
|
||||
await app.settings.setValue("useBundledEmojiFont", null, SettingLevel.DEVICE, false);
|
||||
await app.settings.setValue("useSystemFont", null, SettingLevel.DEVICE, true);
|
||||
await app.settings.setValue("systemFont", null, SettingLevel.DEVICE, "Inconsolata");
|
||||
await app.settings.setValue("systemFont", null, SettingLevel.DEVICE, "Fira Code");
|
||||
}
|
||||
|
||||
// Check the status of the seek bar
|
||||
@@ -351,7 +351,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
const composer = thread.locator(".mx_MessageComposer--compact");
|
||||
// Assert that the reply preview contains audio ReplyTile the file info button
|
||||
await expect(
|
||||
composer.locator(".mx_ReplyPreview .mx_ReplyTile_audio .mx_MFileBody_info[role='button']"),
|
||||
composer.locator(".mx_ReplyPreview .mx_ReplyTile .mx_MFileBody_info[role='button']"),
|
||||
).toBeVisible();
|
||||
|
||||
// Select :smile: emoji and send it
|
||||
@@ -360,6 +360,6 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await composer.getByTestId("basicmessagecomposer").press("Enter");
|
||||
|
||||
// Assert that the file name is rendered on the file button
|
||||
await expect(threadTile.locator(".mx_ReplyTile_audio .mx_MFileBody_info[role='button']")).toBeVisible();
|
||||
await expect(threadTile.locator(".mx_ReplyTile .mx_MFileBody_info[role='button']")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,5 +168,19 @@ test.describe("Composer", () => {
|
||||
await composer.press("Enter");
|
||||
await expect(page.locator(".mx_EventTile_body", { hasText: "Bob" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("renders emoji autocomplete", { tag: "@screenshot" }, async ({ page }) => {
|
||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
|
||||
// Type ":+1" to trigger emoji autocomplete
|
||||
await composer.pressSequentially(":+1");
|
||||
|
||||
// Wait for autocomplete to appear
|
||||
const autocomplete = page.locator("#mx_Autocomplete");
|
||||
await expect(autocomplete).toBeVisible();
|
||||
|
||||
// Take a screenshot of the autocomplete
|
||||
await expect(autocomplete).toMatchScreenshot("emoji-autocomplete.png");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,10 @@ test.describe("Encryption state after registration", () => {
|
||||
"Pa$sW0rD!",
|
||||
);
|
||||
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
@@ -78,7 +81,10 @@ test.describe("Key backup reset from elsewhere", () => {
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
|
||||
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
@@ -21,7 +21,7 @@ const checkDMRoom = async (page: Page) => {
|
||||
};
|
||||
|
||||
const startDMWithBob = async (page: Page, bob: Bot) => {
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "New conversation" }).click();
|
||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
||||
await page.getByRole("option", { name: bob.credentials.displayName }).click();
|
||||
@@ -31,15 +31,11 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
|
||||
|
||||
const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => {
|
||||
// check the invite message
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
).not.toBeVisible();
|
||||
await expect(page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||
|
||||
// Bob sends a response
|
||||
await bob.sendMessage(bobRoomId, "Hoo!");
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
).not.toBeVisible();
|
||||
await expect(page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||
};
|
||||
|
||||
const bobJoin = async (page: Page, bob: Bot) => {
|
||||
|
||||
@@ -30,13 +30,10 @@ test.describe("Cryptography", function () {
|
||||
test.describe("decryption failure messages", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
|
||||
test("should handle device-relative historical messages", async ({
|
||||
homeserver,
|
||||
page,
|
||||
app,
|
||||
credentials,
|
||||
user,
|
||||
}) => {
|
||||
test(
|
||||
"should handle device-relative historical messages",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ homeserver, page, app, credentials, user }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
// Start with a logged-in session, without key backup, and send a message.
|
||||
@@ -51,7 +48,12 @@ test.describe("Cryptography", function () {
|
||||
await app.viewRoomByName("Test room");
|
||||
const lastTile = page.locator(".mx_EventTile").last();
|
||||
await expect(lastTile).toContainText("Historical messages are not available on this device");
|
||||
await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
await expect(lastTile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
await expect(lastTile).toMatchScreenshot("history-not-available.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
|
||||
// Now, we set up key backup, and then send another message.
|
||||
const secretStorageKey = await enableKeyBackup(app);
|
||||
@@ -77,8 +79,12 @@ test.describe("Cryptography", function () {
|
||||
expect(tiles.length).toBeGreaterThanOrEqual(2);
|
||||
// look at the last two tiles only
|
||||
for (const tile of tiles.slice(-2)) {
|
||||
await expect(tile).toContainText("You need to verify this device for access to historical messages");
|
||||
await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
await expect(tile).toContainText(
|
||||
"You need to verify this device for access to historical messages",
|
||||
);
|
||||
await expect(tile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
}
|
||||
|
||||
// Now verify our device (setting up key backup), and check what happens
|
||||
@@ -87,12 +93,17 @@ test.describe("Cryptography", function () {
|
||||
|
||||
// The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though.
|
||||
await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message");
|
||||
await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
|
||||
// The second message should now be decrypted, with a grey shield
|
||||
await expect(tilesAfterVerify[1]).toContainText("test2 test2");
|
||||
await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible();
|
||||
});
|
||||
await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.describe("non-joined historical messages", () => {
|
||||
test.skip(isDendrite, "does not yet support membership on events");
|
||||
@@ -186,7 +197,9 @@ test.describe("Cryptography", function () {
|
||||
// The first message from Bob was sent before Alice was in the room, so should
|
||||
// be different from the standard UTD message
|
||||
await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message");
|
||||
await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
|
||||
// The second message from Bob should be decryptable
|
||||
await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable");
|
||||
@@ -196,7 +209,9 @@ test.describe("Cryptography", function () {
|
||||
// in the room and is expected to be decryptable, so this should have the
|
||||
// standard UTD message
|
||||
await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message");
|
||||
await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
});
|
||||
|
||||
test("should be able to jump to a message sent before our last join event", async ({
|
||||
|
||||
@@ -68,7 +68,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
await doTwoWaySasVerification(page, verifier);
|
||||
|
||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png");
|
||||
await expect(page.locator(".mx_E2EIcon")).toMatchScreenshot("device-verified-e2eIcon.png");
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
@@ -130,7 +130,10 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
await page.unrouteAll({ behavior: "ignoreErrors" });
|
||||
});
|
||||
|
||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||
test(
|
||||
"Verify device with QR code during login",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, homeserver }) => {
|
||||
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
@@ -140,14 +143,21 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
const infoDialog = page.locator(".mx_InfoDialog");
|
||||
// feed the QR code into the verification request.
|
||||
const qrData = await readQrCode(infoDialog);
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("qr-code.png", {
|
||||
mask: [infoDialog.locator("img")],
|
||||
});
|
||||
const verifier = await verificationRequest.evaluateHandle(
|
||||
(request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)),
|
||||
[...qrData],
|
||||
);
|
||||
|
||||
// Confirm that the bot user scanned successfully
|
||||
await expect(infoDialog.getByText("Confirm that you see a green shield on your other device")).toBeVisible();
|
||||
await expect(
|
||||
infoDialog.getByText("Confirm that you see a green shield on your other device"),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-green-shield.png");
|
||||
await infoDialog.getByRole("button", { name: "Yes, I see a green shield" }).click();
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("got-it.png");
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
|
||||
// wait for the bot to see we have finished
|
||||
@@ -171,12 +181,17 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||
test(
|
||||
"Verify device with Security Phrase during login",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
await enterRecoveryKeyAndCheckVerified(page, app, "new passphrase");
|
||||
});
|
||||
await enterRecoveryKeyAndCheckVerified(page, app, "new passphrase", true);
|
||||
},
|
||||
);
|
||||
|
||||
test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
const recoveryKey = (await aliceBotClient.getRecoveryKey()).encodedPrivateKey;
|
||||
@@ -226,7 +241,12 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
});
|
||||
|
||||
/** Helper for the three tests above which verify by recovery key */
|
||||
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
|
||||
async function enterRecoveryKeyAndCheckVerified(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
recoveryKey: string,
|
||||
screenshot = false,
|
||||
) {
|
||||
await page.getByRole("button", { name: "Use recovery key" }).click();
|
||||
|
||||
// Enter the recovery key
|
||||
@@ -234,8 +254,12 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
|
||||
// (cf https://github.com/element-hq/element-web/issues/30089)
|
||||
await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey);
|
||||
if (screenshot) {
|
||||
await expect(page.locator(".mx_Dialog").filter({ hasText: "Enter your recovery key" })).toMatchScreenshot(
|
||||
"recovery-key.png",
|
||||
);
|
||||
}
|
||||
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
|
||||
@@ -77,11 +77,8 @@ test.describe("Cryptography", function () {
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("Unable to decrypt message");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("This message could not be decrypted");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-utd.png");
|
||||
|
||||
/* Should show a red padlock for an unencrypted message in an e2e room */
|
||||
await bob.evaluate(
|
||||
@@ -99,10 +96,8 @@ test.describe("Cryptography", function () {
|
||||
);
|
||||
|
||||
await expect(last).toContainText("test unencrypted");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Not encrypted");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png");
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
||||
|
||||
/* Should show no padlock for an unverified user */
|
||||
// bob sends a valid event
|
||||
@@ -133,11 +128,8 @@ test.describe("Cryptography", function () {
|
||||
/* should show red padlock for a message from an unverified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||
await expect(lastTile).toContainText("test encrypted from unverified");
|
||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastTileE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
await expect(lastTileE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
|
||||
|
||||
/* Should show a red padlock for a message from an unverified device.
|
||||
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
||||
@@ -153,21 +145,15 @@ test.describe("Cryptography", function () {
|
||||
await app.viewRoomByName("TestRoom");
|
||||
|
||||
await expect(last).toContainText("test encrypted from unverified");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("Should show a grey padlock for a key restored from backup", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
user: aliceCredentials,
|
||||
}) => {
|
||||
test(
|
||||
"Should show a grey padlock for a key restored from backup",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, homeserver, user: aliceCredentials }) => {
|
||||
test.slow();
|
||||
const securityKey = await enableKeyBackup(app);
|
||||
|
||||
@@ -200,17 +186,17 @@ test.describe("Cryptography", function () {
|
||||
/* go back to the test room and find Bob's message again */
|
||||
await app.viewRoomById(testRoomId);
|
||||
await expect(lastTile).toContainText("test encrypted 1");
|
||||
// The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning.
|
||||
// The gray shield would be a Compound info icon. The red shield would be a Compound error solid icon.
|
||||
// No shield would have no div mx_EventTile_e2eIcon at all.
|
||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/);
|
||||
await lastTileE2eIcon.hover();
|
||||
// The key is coming from backup, so it is not anymore possible to establish if the claimed device
|
||||
// creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device."
|
||||
// It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted.
|
||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||
await expect(lastTileE2eIcon).toHaveAccessibleName(
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
);
|
||||
});
|
||||
await expect(lastTileE2eIcon).toMatchScreenshot("event-shield-authenticity.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => {
|
||||
// bob has a second, not cross-signed, device
|
||||
@@ -224,7 +210,7 @@ test.describe("Cryptography", function () {
|
||||
|
||||
// the message should appear, decrypted, with no warning
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon"),
|
||||
).not.toBeVisible();
|
||||
|
||||
// bob sends an edit to the first message with his unverified device
|
||||
@@ -241,7 +227,7 @@ test.describe("Cryptography", function () {
|
||||
|
||||
// the edit should have a warning
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon"),
|
||||
).toBeVisible();
|
||||
|
||||
// a second edit from the verified device should be ok
|
||||
@@ -257,16 +243,14 @@ test.describe("Cryptography", function () {
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon"),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should show correct shields on events sent by devices which have since been deleted", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
}) => {
|
||||
test(
|
||||
"should show correct shields on events sent by devices which have since been deleted",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, homeserver }) => {
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
||||
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
||||
// his user info.
|
||||
@@ -294,22 +278,18 @@ test.describe("Cryptography", function () {
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 });
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
|
||||
|
||||
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
|
||||
await assertNoE2EIcon(penultimate, app);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should show correct shields on events sent by users with changed identity", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
}) => {
|
||||
test(
|
||||
"should show correct shields on events sent by users with changed identity",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, homeserver }) => {
|
||||
// Verify Bob
|
||||
await verify(app, bob);
|
||||
|
||||
@@ -322,13 +302,11 @@ test.describe("Cryptography", function () {
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("test encrypted from user that was previously verified");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Sender's verified identity was reset",
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Sender's verified identity was reset");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-identity-reset.png");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -343,8 +321,6 @@ async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) {
|
||||
const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon");
|
||||
if ((await e2eIcon.count()) > 0) {
|
||||
// uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error.
|
||||
await e2eIcon.focus();
|
||||
const tooltip = await app.getTooltipForElement(e2eIcon);
|
||||
throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`);
|
||||
await expect(e2eIcon).toHaveAccessibleName("None");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ test.describe("Key storage out of sync toast", () => {
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
@@ -68,7 +71,10 @@ test.describe("'Turn on key storage' toast", () => {
|
||||
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
|
||||
|
||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
@@ -438,7 +438,7 @@ export async function sendMessageInCurrentRoom(page: Page, message: string): Pro
|
||||
* @param isEncrypted - Whether the room should be encrypted
|
||||
*/
|
||||
export async function createRoom(page: Page, roomName: string, isEncrypted: boolean): Promise<void> {
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "New conversation" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
|
||||
@@ -29,5 +29,12 @@ test.describe("Devtools", () => {
|
||||
display: none;
|
||||
}`,
|
||||
});
|
||||
|
||||
// Try entering a value for the Developer.elementCallUrl setting
|
||||
const input = page.getByRole("textbox", { name: "Element Call URL" });
|
||||
await input.fill("https://example.com");
|
||||
await input.press("Enter");
|
||||
// expect EW NOT to reload
|
||||
await page.getByText("Saved").isVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,7 +73,10 @@ test.describe("Invite dialog", function () {
|
||||
"should support inviting a user to Direct Messages",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
|
||||
const other = page.locator(".mx_InviteDialog_other");
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe("Header section of the room list", () => {
|
||||
const roomListHeader = getHeaderSection(page);
|
||||
await expect(roomListHeader).toMatchScreenshot("room-list-header.png");
|
||||
|
||||
const composeMenu = roomListHeader.getByRole("button", { name: "Add" });
|
||||
const composeMenu = roomListHeader.getByRole("button", { name: "New conversation" });
|
||||
await composeMenu.click();
|
||||
|
||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
||||
@@ -55,7 +55,7 @@ test.describe("Header section of the room list", () => {
|
||||
await expect(roomListHeader).toMatchScreenshot("room-list-space-header.png");
|
||||
|
||||
await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible();
|
||||
await expect(roomListHeader.getByRole("button", { name: "Add" })).toBeVisible();
|
||||
await expect(roomListHeader.getByRole("button", { name: "New conversation" })).toBeVisible();
|
||||
|
||||
const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" });
|
||||
await spaceMenu.click();
|
||||
|
||||
@@ -299,9 +299,7 @@ test.describe("Room list", () => {
|
||||
const publicRoom = roomListView.getByRole("option", { name: "low priority room" });
|
||||
|
||||
// Make room low priority
|
||||
await publicRoom.hover();
|
||||
const roomItemMenu = publicRoom.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await publicRoom.click({ button: "right" });
|
||||
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();
|
||||
|
||||
// Should have low priority decoration
|
||||
@@ -309,13 +307,16 @@ test.describe("Room list", () => {
|
||||
"This is a low priority room",
|
||||
);
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
// focus the header to avoid to have hover decoration
|
||||
await page.getByTestId("room-list-header").click();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-low-priority.png");
|
||||
});
|
||||
|
||||
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "New video room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("video room");
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
@@ -427,7 +428,9 @@ test.describe("Room list", () => {
|
||||
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 page
|
||||
.getByRole("switch", { name: "Show all activity in the room list (dots or number of unread messages)" })
|
||||
.check();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Switch to the other room to avoid the notification to be cleared
|
||||
@@ -447,12 +450,11 @@ test.describe("Room list", () => {
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "mark as unread" });
|
||||
await room.hover();
|
||||
await room.getByRole("button", { name: "More Options" }).click();
|
||||
await room.click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
// focus the header to avoid to have hover decoration
|
||||
await page.getByTestId("room-list-header").click();
|
||||
|
||||
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
|
||||
});
|
||||
|
||||
@@ -46,24 +46,21 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
|
||||
|
||||
await submitShareLocation(page);
|
||||
|
||||
await page.locator(".mx_RoomView_body .mx_EventTile .mx_MLocationBody").click({
|
||||
position: {
|
||||
x: 225,
|
||||
y: 150,
|
||||
},
|
||||
});
|
||||
await page.getByRole("button", { name: "Map marker" }).click();
|
||||
|
||||
// Wait for map to load
|
||||
await expect(page.getByRole("region", { name: "Map" })).toMatchScreenshot(
|
||||
const dialog = page.getByRole("dialog");
|
||||
|
||||
// wait for the dialog to be visible
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// screenshot the map within the dialog
|
||||
await expect(dialog.getByRole("region", { name: "Map" })).toMatchScreenshot(
|
||||
"location-pin-drop-message-map.png",
|
||||
);
|
||||
|
||||
// clicking location tile opens maximised map
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
|
||||
await app.closeDialog();
|
||||
|
||||
await expect(page.locator(".mx_Marker")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Map marker" })).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ test.describe("Login", () => {
|
||||
await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible();
|
||||
|
||||
// Start the login process
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
|
||||
// first pick the homeserver, as otherwise the user picker won't be visible
|
||||
@@ -148,8 +149,6 @@ test.describe("Login", () => {
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
|
||||
// cy.percySnapshot("Login");
|
||||
await expect(axe).toHaveNoViolations();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||
|
||||
30
playwright/e2e/login/login.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { logIntoElement } from "../crypto/utils.ts";
|
||||
|
||||
test.describe(`With force_verification: true`, () => {
|
||||
test.use({
|
||||
config: {
|
||||
force_verification: true,
|
||||
},
|
||||
});
|
||||
|
||||
test("Can reload after login", async ({ page, credentials }) => {
|
||||
// The page should reload fine when going to the base client URL
|
||||
// Regression test for https://github.com/element-hq/element-web/issues/31203
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// We should auto-upload the E2EE keys, and show a welcome page
|
||||
await expect(page.getByRole("heading", { name: `Welcome ${credentials.displayName}` })).toBeVisible();
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByRole("heading", { name: `Welcome ${credentials.displayName}` })).toBeVisible();
|
||||
});
|
||||
});
|
||||