Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ef02fa39e | |||
| 68cc500705 | |||
| 1ec05298cf | |||
| eaa34e6dfa | |||
| fefe093ce0 | |||
| ee4277fd95 | |||
| a15efcc6d0 | |||
| b7a2e8c64e | |||
| 93a8b67ed0 | |||
| 3b08b5c582 | |||
| f236c26356 | |||
| b8ad0b93db | |||
| 3f021472c2 | |||
| 1c42173e5b | |||
| 0b91be5a78 | |||
| c91a24f17b | |||
| 20de09e80f | |||
| 52748d6d35 | |||
| 92a6db5912 | |||
| d9a4858b1d | |||
| fbac316991 | |||
| b9638695b7 | |||
| e2dad68169 | |||
| 466f60ead5 | |||
| 28464b4d12 | |||
| 420576f4ae | |||
| e811cf0f84 | |||
| 7e5a3a530d | |||
| f9778fcbd3 | |||
| 575b3b5400 | |||
| cac682247c | |||
| 82b270616f | |||
| 6f0cd7621b | |||
| f5c6477ef7 | |||
| 1eb07ba750 | |||
| 45ed3c500b | |||
| 7f408bd6cf | |||
| 9cc80c5f36 | |||
| bdee36ca7c | |||
| 9640c330e5 | |||
| 33764d39ba | |||
| 775332b179 | |||
| b2a5f4d58b | |||
| 71d9312452 | |||
| fe1e0df5ad | |||
| 04800c15af | |||
| fb060721dc | |||
| f079224dd6 | |||
| 5e8024009c | |||
| 2e6cf8734b | |||
| deb3355811 | |||
| d9be851965 | |||
| 2db10037f2 | |||
| 133951b663 | |||
| 06f70d1d7c | |||
| 7ad6b4b411 | |||
| 239527996a | |||
| 795780da66 | |||
| ce741f055c | |||
| 7d72775af9 | |||
| 540d71f49c | |||
| 220e93596a | |||
| 2c90eee2dd | |||
| edd4eab195 | |||
| 15c409491d | |||
| d060b77e8f | |||
| 6e9d168dd2 | |||
| cb7e8f4910 | |||
| a5af6cf23f | |||
| 9db08a4574 | |||
| 2f817f32ce | |||
| 13696af194 | |||
| 4ee04d0661 | |||
| 7f057faaad | |||
| d7a58216ae | |||
| 4dd128fd18 | |||
| 126b216d44 | |||
| b8ecc0e07e | |||
| 4668c15ea1 | |||
| 867a6850e4 | |||
| adc5ee22cc | |||
| aa6509e01c | |||
| 770ff42496 | |||
| db352ef876 | |||
| acca876697 | |||
| d9e7948920 | |||
| d7feaa0b2a | |||
| ebd7cdb09d | |||
| df091a7d3e | |||
| 1407d0f046 | |||
| 40fb7f0ca7 | |||
| fea10b3c19 | |||
| ddad82075a | |||
| c078a596f9 | |||
| d62206b7e8 | |||
| 914c959e31 | |||
| be7be39d0f | |||
| 4bd1d4144f | |||
| 0c47ca2a45 | |||
| 5e6ee49509 | |||
| 05e7203f1b | |||
| 5d1cb24a6c | |||
| f509dc031f | |||
| 13ded7db84 | |||
| 56dcb668d1 | |||
| 885305aa46 | |||
| cf7bf71d01 | |||
| 7398a83ae4 | |||
| cbe3eb1709 | |||
| 8e6045a687 | |||
| 494bc1a468 | |||
| 004dbcd062 | |||
| 3f472c8812 | |||
| fe73a0358c | |||
| ebd5df633e | |||
| ce9c66ba4c | |||
| aa84b2e07c | |||
| d1762ed29d | |||
| c37fef459d | |||
| 71c5b71f5c | |||
| dad8072ff8 | |||
| 87d529701c | |||
| 63bf04384a | |||
| e15f80c371 | |||
| 2a669d492d | |||
| ff3f069122 | |||
| 6f0369e623 | |||
| 7a69ab8be4 | |||
| 4da149e56f | |||
| e696f92bd3 | |||
| 48c360f688 | |||
| 3ee50c59f8 | |||
| ba2386ae41 | |||
| 7e3a6d9c42 | |||
| 773662e018 | |||
| 040c348700 | |||
| 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 |
@@ -25,7 +25,10 @@
|
||||
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||
/packages/shared-components/src/i18n/strings
|
||||
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||
/packages/shared-components/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||
|
||||
# Ignore the synapse & mas plugins as this is updated by GHA for docker image updating
|
||||
/playwright/testcontainers/synapse.ts
|
||||
/playwright/testcontainers/mas.ts
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
- [ ] I have read through [review guidelines](https://github.com/element-hq/element-web/blob/develop/docs/review.md) and [CONTRIBUTING.md](https://github.com/element-hq/element-web/blob/develop/CONTRIBUTING.md).
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: webapp-${{ matrix.image }}
|
||||
path: webapp
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
VERSION: ${{ github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Download package
|
||||
run: |
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
|
||||
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
|
||||
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: element-web.deb
|
||||
path: element-web.deb
|
||||
|
||||
@@ -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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
|
||||
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: webapp
|
||||
path: dist/develop.tar.gz
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
running-workflow-name: "Build & Deploy develop.element.io"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$
|
||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload).)*$
|
||||
|
||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
||||
# as the expires after 24h and requires auth to download.
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
env:
|
||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Load GPG key
|
||||
run: |
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
env:
|
||||
TEST_TAG: vectorim/element-web:test
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||
|
||||
@@ -32,25 +32,10 @@ jobs:
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and load
|
||||
id: test-build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
@@ -96,18 +81,70 @@ 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: |
|
||||
vectorim/element-web
|
||||
ghcr.io/element-hq/element-web
|
||||
oci-push.vpn.infra.element.io/element-web
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
flavor: |
|
||||
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Connect to Tailscale
|
||||
uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
||||
audience: ${{ secrets.TS_AUDIENCE }}
|
||||
tags: tag:github-actions
|
||||
|
||||
- name: Compute vault jwt role name
|
||||
id: vault-jwt-role
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
echo "role_name=github_service_management_$( echo "${{ github.repository }}" | sed -r 's|[/-]|_|g')" | tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Get team registry token
|
||||
id: import-secrets
|
||||
uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
url: https://vault.infra.ci.i.element.dev
|
||||
role: ${{ steps.vault-jwt-role.outputs.role_name }}
|
||||
path: service-management/github-actions
|
||||
jwtGithubAudience: https://vault.infra.ci.i.element.dev
|
||||
method: jwt
|
||||
secrets: |
|
||||
services/web-repositories/secret/data/oci.element.io username | OCI_USERNAME ;
|
||||
services/web-repositories/secret/data/oci.element.io password | OCI_PASSWORD ;
|
||||
|
||||
- name: Login to oci.element.io Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: oci-push.vpn.infra.element.io
|
||||
username: ${{ steps.import-secrets.outputs.OCI_USERNAME }}
|
||||
password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
@@ -139,16 +176,3 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: vectorim/element-web
|
||||
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
repository: element-hq/element-web-pro
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
event-type: image-built
|
||||
# Stable way to determine the :version
|
||||
client-payload: |-
|
||||
{
|
||||
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
|
||||
}
|
||||
|
||||
@@ -17,23 +17,23 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Fetch element-desktop
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
repository: element-hq/element-desktop
|
||||
path: element-desktop
|
||||
|
||||
- name: Fetch element-web
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
path: element-web
|
||||
|
||||
- name: Fetch matrix-js-sdk
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Download HTML report
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -50,11 +50,11 @@ jobs:
|
||||
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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/*"
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: webapp
|
||||
path: webapp
|
||||
@@ -122,18 +122,18 @@ jobs:
|
||||
- runAllTests: false
|
||||
project: Pinecone
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: webapp
|
||||
path: webapp
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: yarn.lock
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
|
||||
path: blob-report
|
||||
@@ -194,13 +194,13 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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"
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
if: inputs.skip != true
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: all-blob-reports-*
|
||||
path: all-blob-reports
|
||||
@@ -228,7 +228,7 @@ jobs:
|
||||
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
|
||||
- name: Upload HTML report
|
||||
if: always() && inputs.skip != true
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: html-report
|
||||
path: playwright-report
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
Exercise caution. Use test accounts.
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/playwright-image-updates
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
REPOS: matrix-js-sdk element-web element-desktop
|
||||
steps:
|
||||
- name: Checkout Element Desktop
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: "sudo apt-get install -y tree"
|
||||
|
||||
- name: Download Diffs
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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/*"
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
- name: Upload received images & diffs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: received-images
|
||||
path: packages/shared-components/playwright/shared-component-received
|
||||
|
||||
@@ -22,9 +22,9 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -41,8 +41,8 @@ jobs:
|
||||
- name: Typecheck Shared Components
|
||||
run: "yarn --cwd packages/shared-components run lint:types"
|
||||
|
||||
i18n_lint:
|
||||
name: "i18n Check"
|
||||
i18n_lint_ew:
|
||||
name: "i18n Check (Element Web)"
|
||||
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main
|
||||
permissions:
|
||||
pull-requests: read
|
||||
@@ -59,11 +59,20 @@ jobs:
|
||||
devtools|settings|elementCallUrl
|
||||
labs|sliding_sync_description
|
||||
|
||||
i18n_lint_shared_components:
|
||||
name: "i18n Check (Shared Components)"
|
||||
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main
|
||||
permissions:
|
||||
pull-requests: read
|
||||
with:
|
||||
path: "packages/shared-components"
|
||||
hardcoded-words: "Element"
|
||||
|
||||
rethemendex_lint:
|
||||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
@@ -73,9 +82,9 @@ jobs:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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 +106,9 @@ jobs:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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 +124,9 @@ jobs:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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 +142,9 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -39,12 +39,12 @@ jobs:
|
||||
runner: [1, 2]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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"
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
|
||||
- name: Jest Cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
|
||||
with:
|
||||
path: /tmp/jest_cache
|
||||
key: ${{ hashFiles('**/yarn.lock') }}
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: coverage-${{ matrix.runner }}
|
||||
path: |
|
||||
@@ -118,12 +118,12 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
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"
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
run: "yarn install"
|
||||
|
||||
- name: Jest Cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
|
||||
with:
|
||||
path: /tmp/jest_cache
|
||||
key: ${{ hashFiles('**/yarn.lock') }}
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: coverage-sharedcomponents
|
||||
path: |
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
name: Move PRs asking for design review to the design board
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
|
||||
- uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2
|
||||
id: find_team_members
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
TEAM: "design"
|
||||
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
|
||||
- uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2
|
||||
id: add_to_project
|
||||
if: steps.any_matching_reviewers.outputs.match == 'true'
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
name: Move PRs asking for design review to the design board
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
|
||||
- uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2
|
||||
id: find_team_members
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
TEAM: "product"
|
||||
- uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
|
||||
- uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2
|
||||
id: add_to_project
|
||||
if: steps.any_matching_reviewers.outputs.match == 'true'
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ jobs:
|
||||
update:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
- 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@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/jitsi-update
|
||||
|
||||
@@ -1,3 +1,84 @@
|
||||
Changes in [1.12.9](https://github.com/element-hq/element-web/releases/tag/v1.12.9) (2026-01-27)
|
||||
================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Allow local log downloads when a rageshake URL is not configured. ([#31716](https://github.com/element-hq/element-web/pull/31716)). Contributed by @Half-Shot.
|
||||
* Improve icon rendering accessibility ([#31776](https://github.com/element-hq/element-web/pull/31776)). Contributed by @t3chguy.
|
||||
* Show "Bob shared this message" on messages shared via MSC4268 ([#31684](https://github.com/element-hq/element-web/pull/31684)). Contributed by @richvdh.
|
||||
* Update the way we render icons for accessibility ([#31731](https://github.com/element-hq/element-web/pull/31731)). Contributed by @t3chguy.
|
||||
* Switch from css masks to rendering svg ([#31681](https://github.com/element-hq/element-web/pull/31681)). Contributed by @t3chguy.
|
||||
* Support for stable MSC4191 account management action parameter ([#31701](https://github.com/element-hq/element-web/pull/31701)). Contributed by @hughns.
|
||||
* Support for stable m.oauth UIA stage from MSC4312 ([#31704](https://github.com/element-hq/element-web/pull/31704)). Contributed by @hughns.
|
||||
* Switch to Compound icons to replace old icons ([#31667](https://github.com/element-hq/element-web/pull/31667)). Contributed by @t3chguy.
|
||||
* Switch from svg masks to svg rendering in more places ([#31652](https://github.com/element-hq/element-web/pull/31652)). Contributed by @t3chguy.
|
||||
* Switch from svg masks to svg rendering in more places ([#31650](https://github.com/element-hq/element-web/pull/31650)). Contributed by @t3chguy.
|
||||
* Update notification icons using Compound icons ([#31671](https://github.com/element-hq/element-web/pull/31671)). Contributed by @t3chguy.
|
||||
* Memoise ListView context ([#31668](https://github.com/element-hq/element-web/pull/31668)). Contributed by @t3chguy.
|
||||
* Switch emoji picker to use emoji for header icons ([#31645](https://github.com/element-hq/element-web/pull/31645)). Contributed by @t3chguy.
|
||||
* Replace icons with Compound alternatives ([#31642](https://github.com/element-hq/element-web/pull/31642)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix avatar decorations in thread activity centre not being atop avatar ([#31789](https://github.com/element-hq/element-web/pull/31789)). Contributed by @t3chguy.
|
||||
* Fix room settings roles tab getting confused if power level change fails ([#31768](https://github.com/element-hq/element-web/pull/31768)). Contributed by @t3chguy.
|
||||
* Custom themes now import highlights in css ([#31758](https://github.com/element-hq/element-web/pull/31758)). Contributed by @Philldomd.
|
||||
* Use correct translation for url preview settings ([#31740](https://github.com/element-hq/element-web/pull/31740)). Contributed by @florianduros.
|
||||
* Fix error shown if accepting a 3pid invite ([#31735](https://github.com/element-hq/element-web/pull/31735)). Contributed by @dbkr.
|
||||
* Ensure correct focus configuration for Element Call before allowing users to call. ([#31490](https://github.com/element-hq/element-web/pull/31490)). Contributed by @Half-Shot.
|
||||
* Fix emoji font in emoji picker header buttons ([#31679](https://github.com/element-hq/element-web/pull/31679)). Contributed by @t3chguy.
|
||||
* fix flaky test by waiting for chat panel before counting messages ([#31633](https://github.com/element-hq/element-web/pull/31633)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [1.12.8](https://github.com/element-hq/element-web/releases/tag/v1.12.8) (2026-01-13)
|
||||
================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Remove `element_call.participant_limit` config and associated code. ([#31638](https://github.com/element-hq/element-web/pull/31638)). Contributed by @Half-Shot.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Switch to rendering svg icons rather than masking them ([#31557](https://github.com/element-hq/element-web/pull/31557)). Contributed by @t3chguy.
|
||||
* Update history visibility UX ([#31635](https://github.com/element-hq/element-web/pull/31635)). Contributed by @langleyd.
|
||||
* Show correct call icon for joining a call. ([#31489](https://github.com/element-hq/element-web/pull/31489)). Contributed by @Half-Shot.
|
||||
* Update StopGapWidgetDriver to support sticky events ([#31205](https://github.com/element-hq/element-web/pull/31205)). Contributed by @Half-Shot.
|
||||
* Remove release announcements for new sounds \& room list ([#31544](https://github.com/element-hq/element-web/pull/31544)). Contributed by @t3chguy.
|
||||
* Add button to restore from backup into /devtools ([#31581](https://github.com/element-hq/element-web/pull/31581)). Contributed by @mxandreas.
|
||||
* Switch to non-solid compound icons for room settings \& composer ([#31561](https://github.com/element-hq/element-web/pull/31561)). Contributed by @t3chguy.
|
||||
* Support encrypted state events MSC4362 ([#31513](https://github.com/element-hq/element-web/pull/31513)). Contributed by @andybalaam.
|
||||
* Update prop type \& documentation for HistoryVisibleBanner and VM. ([#31545](https://github.com/element-hq/element-web/pull/31545)). Contributed by @kaylendog.
|
||||
* Switch to Compound icons in more places ([#31560](https://github.com/element-hq/element-web/pull/31560)). Contributed by @t3chguy.
|
||||
* Switch to rendering svg icons rather than masking them ([#31550](https://github.com/element-hq/element-web/pull/31550)). Contributed by @t3chguy.
|
||||
* Make AccessibleButton contrast control compatible ([#31308](https://github.com/element-hq/element-web/pull/31308)). Contributed by @t3chguy.
|
||||
* Switch to compound-design-tokens for platform icons ([#31543](https://github.com/element-hq/element-web/pull/31543)). Contributed by @t3chguy.
|
||||
* Switch to rendering svg icons rather than masking them ([#31531](https://github.com/element-hq/element-web/pull/31531)). Contributed by @t3chguy.
|
||||
* Switch to rendering svg icons rather than css masking ([#31517](https://github.com/element-hq/element-web/pull/31517)). Contributed by @t3chguy.
|
||||
* Auto approve matrix rtc member event (`m.rtc.member`) (sticky events) ([#31452](https://github.com/element-hq/element-web/pull/31452)). Contributed by @toger5.
|
||||
* Size Autocomplete relative to the RoomView height rather than the viewport height ([#31425](https://github.com/element-hq/element-web/pull/31425)). Contributed by @langleyd.
|
||||
* Implement UI for history visibility acknowledgement. ([#31156](https://github.com/element-hq/element-web/pull/31156)). Contributed by @kaylendog.
|
||||
* Export disposing hook from package ([#31498](https://github.com/element-hq/element-web/pull/31498)). Contributed by @MidhunSureshR.
|
||||
* Change `header-panel-bg-hover` to use `var(--cpd-color-bg-action-secondary-hovered)` for better custom theming ([#31457](https://github.com/element-hq/element-web/pull/31457)). Contributed by @th0mcat.
|
||||
* Improve icon rendering in iconized context menu ([#31458](https://github.com/element-hq/element-web/pull/31458)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Fix space settings visibility tab crashing ([#31705](https://github.com/element-hq/element-web/pull/31705)). Contributed by @RiotRobot.
|
||||
* Fix expand/collapse reply preview not showing in some cases ([#31639](https://github.com/element-hq/element-web/pull/31639)). Contributed by @t3chguy.
|
||||
* Fix bundled font or custom font not applied after theme switch ([#31591](https://github.com/element-hq/element-web/pull/31591)). Contributed by @florianduros.
|
||||
* Add ol override CSS for markdown-body ([#31618](https://github.com/element-hq/element-web/pull/31618)). Contributed by @niamu.
|
||||
* Fix reaction left margin in timeline card ([#31625](https://github.com/element-hq/element-web/pull/31625)). Contributed by @t3chguy.
|
||||
* Open right panel timeline when jumping to event with maximised widget ([#31626](https://github.com/element-hq/element-web/pull/31626)). Contributed by @t3chguy.
|
||||
* Fix Compound Link elements not having an underline. ([#31583](https://github.com/element-hq/element-web/pull/31583)). Contributed by @Half-Shot.
|
||||
* Recalculate mentions metadata of forwarded messages based on message body ([#31193](https://github.com/element-hq/element-web/pull/31193)). Contributed by @twassman.
|
||||
* Fix Room Preview Card Layout ([#31611](https://github.com/element-hq/element-web/pull/31611)). Contributed by @germain-gg.
|
||||
* Fix: WidgetMessaging not properly closed causing side effects and bugs ([#31598](https://github.com/element-hq/element-web/pull/31598)). Contributed by @BillCarsonFr.
|
||||
* Handle cross-signing keys missing locally and/or from secret storage ([#31367](https://github.com/element-hq/element-web/pull/31367)). Contributed by @uhoreg.
|
||||
* fix: Allow wrapping in `Banner` component. ([#31532](https://github.com/element-hq/element-web/pull/31532)). Contributed by @kaylendog.
|
||||
* Update algorithm for history visible banner. ([#31577](https://github.com/element-hq/element-web/pull/31577)). Contributed by @kaylendog.
|
||||
* Fix styling issue when using EW modules ([#31533](https://github.com/element-hq/element-web/pull/31533)). Contributed by @florianduros.
|
||||
* Prevent history visible banner from displaying in threads. ([#31535](https://github.com/element-hq/element-web/pull/31535)). Contributed by @kaylendog.
|
||||
* Make the feedback icon be the right color in dark theme ([#31527](https://github.com/element-hq/element-web/pull/31527)). Contributed by @robintown.
|
||||
|
||||
|
||||
Changes in [1.12.7](https://github.com/element-hq/element-web/releases/tag/v1.12.7) (2025-12-16)
|
||||
================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -184,6 +184,16 @@ Please ensure your changes match the cosmetic style of the existing project,
|
||||
and **_never_** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
## Shared Components
|
||||
|
||||
When creating new UI components, consider whether they should be added to the shared components package (`packages/shared-components`) rather than directly in the main `src/` directory. Components should be placed in shared components if they:
|
||||
|
||||
- Are reusable across different parts of the application
|
||||
- Could potentially be used by other Element projects (Element Desktop, Aurora, Element modules...)
|
||||
- Follow established patterns and don't have tight coupling to specific application logic
|
||||
|
||||
For more details, see the [shared components README](./packages/shared-components/README.md).
|
||||
|
||||
## Attribution
|
||||
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.20-labs@sha256:dbcde2ebc4abc8bb5c3c499b9c9a6876842bf5da243951cd2697f921a7aeb6a9
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:b36a1eab6bdeb43cf4808370d18b6706452e810e3563b1ce669d2965af3c0464 AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:32bde4fc7635942cafb9681e5479a0ba4b2d53b279e44a67ba9303a71fecd706 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:8e23ab31c214ee1d7f832d63b2ee768d5cbc270a94a2cba0752fed93ad83e345
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:2c49851f9b34ef35567dc3cbbb56d06d1f56dbb764e75eeb4a599223ee64819c
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -194,6 +194,17 @@ To add a new translation, head to the [translating doc](docs/translating.md).
|
||||
|
||||
For a developer guide, see the [translating dev doc](docs/translating-dev.md).
|
||||
|
||||
# Extending Element Web with Modules
|
||||
|
||||
Element Web supports a module system that allows you to extend or modify functionality at runtime. Modules are loaded dynamically and provide a safe, predictable API for customization.
|
||||
|
||||
## What are modules?
|
||||
|
||||
Modules are extensions that can add or modify Element Web's functionality. They are:
|
||||
|
||||
- Built using the [`@element-hq/element-web-module-api`](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api)
|
||||
- Loaded in EW via [config.json](docs/config.md#modules)
|
||||
|
||||
# Triaging issues
|
||||
|
||||
Issues are triaged by community members and the Web App Team, following the [triage process](https://github.com/element-hq/element-meta/wiki/Triage-process).
|
||||
|
||||
@@ -272,6 +272,8 @@ Inheriting all the rules of TypeScript, the following additionally apply:
|
||||
18. Components should serve a single, or near-single, purpose.
|
||||
19. Prefer to derive information from component properties rather than establish state.
|
||||
20. Do not use `React.Component::forceUpdate`.
|
||||
21. Prefer to use [compound typography components](https://compound.element.io/?path=/docs/compound-web_typography--docs) instead of raw HTML elements for text. This ensures consistent font usage and letter spacing across the app.
|
||||
22. If you can't use 21, don't forget to apply the correct CSS classes for font and letter spacing.
|
||||
|
||||
## Stylesheets (\*.pcss = PostCSS + Plugins)
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
},
|
||||
"element_call": {
|
||||
"url": "https://call.element.io",
|
||||
"participant_limit": 8,
|
||||
"brand": "Element Call"
|
||||
},
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
|
||||
|
||||
@@ -8,7 +8,13 @@ General description of the pattern can be found [here](https://en.wikipedia.org/
|
||||
|
||||
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
|
||||
|
||||
### Practical guidelines for MVVM in element-web
|
||||
## Why are we using MVVM?
|
||||
|
||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||
|
||||
## Practical guidelines for MVVM in element-web
|
||||
|
||||
A first documentation and implementation of MVVM was done in [MVVM-v1.md](MVVM-v1.md). This v1 version is now deprecated and this document describes the current implementation.
|
||||
|
||||
@@ -19,12 +25,12 @@ This is anywhere your data or business logic comes from. If your view model is a
|
||||
#### View
|
||||
|
||||
1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/packages/shared-components). Develop it in storybook!
|
||||
2. Views are simple react components (eg: `FooView`).
|
||||
3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store.
|
||||
2. Views are simple react components (eg: `FooView`) with very little state and logic.
|
||||
3. Views must call `useViewModel` hook with the corresponding view model passed in as argument. This allows the view to re-render when something has changed in the view model. This entire mechanism is powered by [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore).
|
||||
4. Views should define the interface of the view model they expect:
|
||||
|
||||
```tsx
|
||||
// Snapshot is the return type of your view model
|
||||
// Snapshot is the data that your view-model provides which is rendered by the view.
|
||||
interface FooViewSnapshot {
|
||||
value: string;
|
||||
}
|
||||
@@ -34,16 +40,16 @@ This is anywhere your data or business logic comes from. If your view model is a
|
||||
doSomething: () => void;
|
||||
}
|
||||
|
||||
// ViewModel is a type defining the methods needed for `useSyncExternalStore`
|
||||
// ViewModel is an object (usually a class) that implements both the interfaces listed above.
|
||||
// https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/ViewModel.ts
|
||||
type FooViewModel = ViewModel<FooViewSnapshot> & FooViewActions;
|
||||
|
||||
interface FooViewProps {
|
||||
// Ideally the view only depends on the view model i.e you don't expect any other props here.
|
||||
vm: FooViewModel;
|
||||
}
|
||||
|
||||
function FooView({ vm }: FooViewProps) {
|
||||
// useViewModel is a helper function that uses useSyncExternalStore under the hood
|
||||
const { value } = useViewModel(vm);
|
||||
return (
|
||||
<button type="button" onClick={() => vm.doSomething()}>
|
||||
@@ -82,8 +88,131 @@ This is anywhere your data or business logic comes from. If your view model is a
|
||||
|
||||
4. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/audio/AudioPlayerViewModel.ts)
|
||||
|
||||
### Benefits
|
||||
### `useViewModel` hook
|
||||
|
||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||
Your view must call this hook with the view-model as argument. Think of this as your view subscribing to the view model.<br>
|
||||
This hook returns the snapshot from your view-model.
|
||||
|
||||
## Disposables and helper hooks
|
||||
|
||||
Disposables provide a mechanism for tracking and releases resources. This is necessary for avoiding memory leaks.
|
||||
|
||||
### Lifecycle of a view model
|
||||
|
||||
The lifecycle of a given view model is from its creation (usually through the constructor i.e `new FooViewModel(prop1, prop2)`) to the time the `dispose` method on it is called (eg: `fooViewModel.dispose()`). It is the responsibility of whoever creates the view model to call the dispose method when the view model is no longer necessary.
|
||||
|
||||
Disposable work by tracking anything that needs to be disposed of and then sequentially disposing them when `viewModel.dispose()` is called.
|
||||
|
||||
### How to use disposables
|
||||
|
||||
Consider the following scenarios:
|
||||
|
||||
#### Scenario 1: Your view model listens to some event on an event emitter <br>
|
||||
|
||||
In the example given below, how do you ensure that the listener on `props.emitter` is removed when the view model is disposed?
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
props.emitter.on("my-event", this.doSomething());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use disposables to remove the listener when the view-model is disposed:
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
this.disposables.trackListener(props.emitter, "my-event", this.doSomething());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario 2: Your view model creates sub view models <br>
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
this.barViewModel = new BarViewModel(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, we want to ensure that when `FooViewModel.dispose()` is called, it also disposes any sub view models (in this case `BarViewModel`):
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
this.barViewModel = this.disposables.track(new BarViewModel(...));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario 3: Tracking and disposing arbitrary resources <br>
|
||||
|
||||
A disposable is:
|
||||
|
||||
- a function
|
||||
- an object with `dispose` method (like a view model)
|
||||
|
||||
You can therefore use disposables to track any resource that must be eventually relinquished, eg:
|
||||
|
||||
```ts
|
||||
class Call {
|
||||
....
|
||||
public endCall();
|
||||
public stopConnections();
|
||||
}
|
||||
|
||||
class CallViewModel {
|
||||
...
|
||||
constructor(props: Props) {
|
||||
const call = new Call();
|
||||
// When the view model is disposed, the following call methods will be called
|
||||
this.disposables.track(() => {
|
||||
call.endCall();
|
||||
call.stopConnections();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Disposing view models from non-MVVMed react components
|
||||
|
||||
While we eventually want all our UI code to use MVVM, the current reality is that most of the existing code is just normal react components. We follow a bottoms up approach when it comes to moving code over to MVVM i.e we deal with child components before dealing with parent components.
|
||||
|
||||
This means that you need to dispose child view models from the non-MVVMed parent component.
|
||||
|
||||
#### Class component:
|
||||
|
||||
Create the view model in `componentDidMount()` and dispose the view model in `componentWillUnmount()`:
|
||||
|
||||
```ts
|
||||
class FooComponent extends Component {
|
||||
componentDidMount() {
|
||||
this.barViewModel = new BarViewModel(...);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.barViewModel.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Functional Component:
|
||||
|
||||
Use the `useCreateAutoDisposedViewModel` hook:
|
||||
|
||||
```ts
|
||||
export function FooComponent(props) {
|
||||
const vm = useCreateAutoDisposedViewModel(() => new BarViewModel(...));
|
||||
return <BarView vm={vm}>;
|
||||
}
|
||||
```
|
||||
|
||||
This hook will call the `dispose` method on the view model when `FooComponent` is unmounted.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
# Build
|
||||
|
||||
- [Customisations](customisations.md)
|
||||
- [Modules](modules.md)
|
||||
- [Deprecated Modules](deprecated-modules.md)
|
||||
- [Native Node modules](native-node-modules.md)
|
||||
|
||||
# Contribution
|
||||
@@ -40,6 +40,8 @@
|
||||
- [Feature flags](feature-flags.md)
|
||||
- [OIDC and delegated authentication](oidc.md)
|
||||
- [Release Process](release.md)
|
||||
- [MVVM](MVVM.md)
|
||||
- [Settings](settings.md)
|
||||
|
||||
# Deep dive
|
||||
|
||||
|
||||
@@ -391,8 +391,6 @@ The VoIP and Jitsi options are:
|
||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
||||
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
||||
- `participant_limit`: The maximum number of users who can join a call; if
|
||||
this number is exceeded, the user will not be able to join a given call.
|
||||
- `brand`: Optional name for the app. Defaults to `Element Call`. This is
|
||||
used throughout the application in various strings/locations.
|
||||
- `guest_spa_url`: Optional URL for an Element Call single-page app (SPA),
|
||||
@@ -409,6 +407,7 @@ If you run your own rageshake server to collect bug reports, the following optio
|
||||
1. `bug_report_endpoint_url`: URL for where to submit rageshake logs to. Rageshakes include feedback submissions and bug reports. When
|
||||
not present in the config, the app will disable all rageshake functionality. Set to `https://rageshakes.element.io/api/submit` to submit
|
||||
rageshakes to us, or use your own rageshake server.
|
||||
You may also set the value to `"local"` if you wish to only store logs locally, in order to download them for debugging.
|
||||
2. `uisi_autorageshake_app`: If a user has enabled the "automatically send debug logs on decryption errors" flag, this option will be sent
|
||||
alongside the rageshake so the rageshake server can filter them by app name. By default, this will be `element-auto-uisi`
|
||||
(in contrast to other rageshakes submitted by the app, which use `element-web`).
|
||||
@@ -588,6 +587,22 @@ Currently, the following UI feature flags are supported:
|
||||
- `UIFeature.allowCreatingPublicRooms` - Whether or not public rooms can be created.
|
||||
- `UIFeature.allowCreatingPublicSpaces` - Whether or not public spaces can be created.
|
||||
|
||||
## Modules
|
||||
|
||||
`modules`: An optional array of module paths to load at runtime. Each entry is a URL or path to a JavaScript module entry point that will be dynamically imported when Element Web starts.
|
||||
|
||||
**Note:** This is separate from the build-time module system configured via `build_config.yaml`. Runtime modules are loaded dynamically from the paths specified in `config.json`, while build-time modules are bundled during compilation.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"modules": ["https://example.com/my-module.js", "/path/to/local-module.js"]
|
||||
}
|
||||
```
|
||||
|
||||
Each module URL is loaded using dynamic import (`import()`). The modules are loaded in order after Element Web initializes but before the application fully starts. Modules must be accessible from the browser and should export a compatible module format that works with the [Module API](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api).
|
||||
|
||||
## Undocumented / developer options
|
||||
|
||||
The following are undocumented or intended for developer use only.
|
||||
@@ -596,4 +611,3 @@ The following are undocumented or intended for developer use only.
|
||||
2. `sync_timeline_limit`
|
||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Module system
|
||||
# Deprecated Module system
|
||||
|
||||
> [!CAUTION]
|
||||
> DEPRECATED. Use [Element web module api](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) instead.
|
||||
|
||||
The module system in Element Web is a way to add or modify functionality of Element Web itself, bundled at compile time
|
||||
for the app. This means that modules are loaded as part of the `yarn build` process but have an effect on user experience
|
||||
@@ -112,3 +112,25 @@ Enables knock feature for rooms. This allows users to ask to join a room.
|
||||
## New room list (`feature_new_room_list`) [In Development]
|
||||
|
||||
Enable the new room list that is currently in development.
|
||||
|
||||
## Exclude insecure devices when sending/receiving messages (`feature_exclude_insecure_devices`)
|
||||
|
||||
Do not send or receive messages to/from devices that are not properly verified. Users with unverified devices will not
|
||||
receive your messages at all on those devices, and if they send messages, you will not be able to read them, but you
|
||||
will be aware that a message exists.
|
||||
|
||||
## Share encrypted history with new members (`feature_share_history_on_invite`) [In Development]
|
||||
|
||||
When inviting users to an encrypted room with shared history (i.e. a room with the "Who can read history?" setting set
|
||||
to "Members only (since the point in time of selecting this option)"), send the keys for previous messages to the
|
||||
invitee so they can read them.
|
||||
|
||||
Both the inviter and the invitee must set this labs flag, before the invitation is sent.
|
||||
|
||||
## Encrypted state events (MSC4362) (`feature_msc4362_encrypted_state_events`)
|
||||
|
||||
Encrypt most of the state events in the room, including the room name and topic.
|
||||
|
||||
WARNING: this means that users joining a room who do not have access to its history will not be able to see the name or
|
||||
topic of the room, or any other room state information. It also means the room name and topic are not available before
|
||||
joining a room.
|
||||
|
||||
@@ -2,216 +2,485 @@
|
||||
|
||||
## Contents
|
||||
|
||||
- How to run the tests
|
||||
- How the tests work
|
||||
- How to write great Playwright tests
|
||||
- Visual testing
|
||||
- [Overview](#overview)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Running the Tests](#running-the-tests)
|
||||
- [Element Web E2E Tests](#element-web-e2e-tests)
|
||||
- [Shared Components Tests](#shared-components-tests)
|
||||
- [Projects](#projects)
|
||||
- [How the Tests Work](#how-the-tests-work)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Homeserver Setup](#homeserver-setup)
|
||||
- [Fixtures](#fixtures)
|
||||
- [Writing Tests](#writing-tests)
|
||||
- [Getting a Homeserver](#getting-a-homeserver)
|
||||
- [Logging In](#logging-in)
|
||||
- [Joining a Room](#joining-a-room)
|
||||
- [Using matrix-js-sdk](#using-matrix-js-sdk)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Visual Testing](#visual-testing)
|
||||
- [Test Tags](#test-tags)
|
||||
- [Supported Container Runtimes](#supported-container-runtimes)
|
||||
|
||||
## Overview
|
||||
|
||||
Element Web contains two sets of Playwright tests:
|
||||
|
||||
1. **Element Web E2E Tests** (`playwright/e2e/`) - Full end-to-end tests of the Element Web application with real homeserver instances
|
||||
2. **Shared Components Tests** (`packages/shared-components/`) - Visual regression tests for the shared component library using Storybook
|
||||
|
||||
Both test suites run automatically in CI on every pull request and on every merge to develop & master.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running Playwright tests, ensure you have the following set up:
|
||||
|
||||
### 1. Install Playwright Browsers and System Dependencies
|
||||
|
||||
Follow the Playwright installation instructions:
|
||||
|
||||
- **Browsers:** <https://playwright.dev/docs/browsers#install-browsers>
|
||||
- **System dependencies:** <https://playwright.dev/docs/browsers#install-system-dependencies>
|
||||
|
||||
```sh
|
||||
yarn playwright install --with-deps
|
||||
```
|
||||
|
||||
### 2. Container Runtime
|
||||
|
||||
See [Supported Container Runtimes](#supported-container-runtimes) for details on supported container runtimes (Docker, Podman, Colima).
|
||||
|
||||
### 3. Element Web Server (for E2E tests)
|
||||
|
||||
Element Web E2E tests require an instance running on `http://localhost:8080` (configured in `playwright.config.ts`).
|
||||
|
||||
You can either:
|
||||
|
||||
- **Run manually:** `yarn start` in a separate terminal (not working for screenshot tests running in a docker environment).
|
||||
- **Auto-start:** Playwright will start the webserver automatically if it's not already running
|
||||
|
||||
## Running the Tests
|
||||
|
||||
Our Playwright tests run automatically as part of our CI along with our other tests,
|
||||
on every pull request and on every merge to develop & master.
|
||||
### Element Web E2E Tests
|
||||
|
||||
You may need to follow instructions to set up your development environment for running
|
||||
Playwright by following <https://playwright.dev/docs/browsers#install-browsers> and
|
||||
<https://playwright.dev/docs/browsers#install-system-dependencies>.
|
||||
Our main Playwright tests run against a full Element Web instance with Synapse/Dendrite homeservers.
|
||||
|
||||
However the Playwright tests are run, an element-web instance must be running on
|
||||
http://localhost:8080 (this is configured in `playwright.config.ts`) - this is what will
|
||||
be tested. When running Playwright tests yourself, the standard `yarn start` from the
|
||||
element-web project is fine: leave it running it a different terminal as you would
|
||||
when developing. Alternatively if you followed the development set up from element-web then
|
||||
Playwright will be capable of running the webserver on its own if it isn't already running.
|
||||
**Run all E2E tests:**
|
||||
|
||||
The tests use [testcontainers](https://node.testcontainers.org/) to launch Homeserver (Synapse or Dendrite)
|
||||
instances to test against, so you'll also need to one of the
|
||||
[supported container runtimes](#supporter-container-runtimes)
|
||||
installed and working in order to run the Playwright tests.
|
||||
|
||||
There are a few different ways to run the tests yourself. The simplest is to run:
|
||||
|
||||
```shell
|
||||
```sh
|
||||
yarn run test:playwright
|
||||
```
|
||||
|
||||
This will run the Playwright tests once, non-interactively.
|
||||
**Run a specific test file:**
|
||||
|
||||
You can also run individual tests this way too, as you'd expect:
|
||||
|
||||
```shell
|
||||
yarn run test:playwright --spec playwright/e2e/register/register.spec.ts
|
||||
```sh
|
||||
yarn run test:playwright playwright/e2e/register/register.spec.ts
|
||||
```
|
||||
|
||||
Playwright also has its own UI that you can use to run and debug the tests.
|
||||
To launch it:
|
||||
**Run tests interactively with Playwright UI:**
|
||||
|
||||
```shell
|
||||
yarn run test:playwright:open --headed --debug
|
||||
```sh
|
||||
yarn run test:playwright:open
|
||||
```
|
||||
|
||||
See more command line options at <https://playwright.dev/docs/test-cli>.
|
||||
**Run screenshot tests only:**
|
||||
|
||||
## Projects
|
||||
> [!WARNING]
|
||||
> This command run the playwright tests in a docker environment.
|
||||
|
||||
By default, Playwright will run all "Projects", this means tests will run against Chrome, Firefox and "Safari" (Webkit).
|
||||
We only run tests against Chrome in pull request CI, but all projects in the merge queue.
|
||||
Some tests are excluded from running on certain browsers due to incompatibilities in the test harness.
|
||||
```sh
|
||||
yarn run test:playwright:screenshots
|
||||
```
|
||||
|
||||
For more information about visual testing, see [Visual Testing](playwright#visual-testing).
|
||||
|
||||
**Additional command line options:** <https://playwright.dev/docs/test-cli>
|
||||
|
||||
### Shared Components Tests
|
||||
|
||||
The shared-components package uses Playwright (via Storybook test runner) to validate component rendering across different states and configurations.
|
||||
|
||||
**Run Storybook tests:**
|
||||
|
||||
```sh
|
||||
cd packages/shared-components
|
||||
yarn test:storybook
|
||||
```
|
||||
|
||||
**Run Storybook tests in CI mode:**
|
||||
|
||||
```sh
|
||||
cd packages/shared-components
|
||||
yarn test:storybook:ci
|
||||
```
|
||||
|
||||
**Update Storybook screenshots:**
|
||||
|
||||
```sh
|
||||
cd packages/shared-components
|
||||
yarn test:storybook:update
|
||||
```
|
||||
|
||||
This uses the same Docker-based screenshot rendering as Element Web to ensure consistency across platforms.
|
||||
|
||||
### Projects
|
||||
|
||||
By default, Playwright runs tests against all "Projects": Chrome, Firefox, "Safari" (Webkit), Dendrite and Picone.
|
||||
|
||||
- Chrome, Firefox, Safari run against Synapse
|
||||
- Dendrite and Picone run against Chrome
|
||||
|
||||
Misc:
|
||||
|
||||
- **Pull Request CI:** Tests run only against Chrome
|
||||
- **Merge Queue:** Tests run against all projects
|
||||
- Some tests are excluded from certain browsers due to incompatibilities (see [Test Tags](#test-tags))
|
||||
|
||||
## How the Tests Work
|
||||
|
||||
Everything Playwright-related lives in the `playwright/` subdirectory
|
||||
as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
|
||||
### Test Structure
|
||||
|
||||
`playwright/testcontainers` contains the testcontainers which start instances
|
||||
of Synapse/Dendrite. These servers are what Element-web runs against in the tests.
|
||||
**Element Web tests** are located in the `playwright/` subdirectory:
|
||||
|
||||
Synapse can be launched with different configurations in order to test element
|
||||
in different configurations. You can specify `synapseConfig` as such:
|
||||
- `playwright/e2e/` - E2E test files
|
||||
- `playwright/testcontainers/` - Testcontainers for Synapse/Dendrite instances
|
||||
- `playwright/snapshots/` - Visual regression test screenshots
|
||||
- `playwright/pages/` - Page object models
|
||||
- `playwright/plugins/` - Custom Playwright plugins
|
||||
|
||||
**Shared components tests** are located in `packages/shared-components/`:
|
||||
|
||||
- `packages/shared-components/playwright/snapshots/` - Storybook screenshot baselines
|
||||
- `packages/shared-components/.storybook/` - Storybook configuration
|
||||
|
||||
The shared components use Storybook's test runner (powered by Playwright) to validate component rendering across different states and configurations.
|
||||
|
||||
### Homeserver Setup
|
||||
|
||||
Homeservers (Synapse or Dendrite) are launched by Playwright workers and reused for all tests matching the worker configuration.
|
||||
|
||||
**Configure Synapse options:**
|
||||
|
||||
```typescript
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
// The config options to pass to the Synapse instance
|
||||
// Configuration options for the Synapse instance
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration.
|
||||
Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as
|
||||
they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
|
||||
We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
|
||||
The logs from testcontainers will be attached to any reports output from Playwright.
|
||||
**Important notes:**
|
||||
|
||||
- Homeservers are reused between tests for efficiency
|
||||
- Please use unique names for any rooms put into the room directory as they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
|
||||
- We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
|
||||
- Homeserver logs are attached to Playwright test reports
|
||||
|
||||
### Fixtures
|
||||
|
||||
We heavily leverage [Playwright fixtures](https://playwright.dev/docs/test-fixtures) to provide:
|
||||
|
||||
- Homeserver instances (`homeserver`)
|
||||
- Logged-in users (`user`)
|
||||
- Bot users (`bot`)
|
||||
- Application state (`app`)
|
||||
|
||||
See [Writing Tests](#writing-tests) for usage examples.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Mostly this is the same advice as for writing any other Playwright test: the Playwright
|
||||
docs are well worth a read if you're not already familiar with Playwright testing, eg.
|
||||
https://playwright.dev/docs/best-practices. To avoid your tests being flaky it is also
|
||||
recommended to use [auto-retrying assertions](https://playwright.dev/docs/test-assertions#auto-retrying-assertions).
|
||||
For general Playwright best practices, see:
|
||||
|
||||
### Getting a Synapse
|
||||
- <https://playwright.dev/docs/best-practices>
|
||||
- <https://playwright.dev/docs/test-assertions#auto-retrying-assertions> (recommended for avoiding flaky tests)
|
||||
|
||||
We heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
|
||||
To acquire a homeserver within a test just add the `homeserver` fixture to the test:
|
||||
### Getting a Homeserver
|
||||
|
||||
Use the `homeserver` fixture to acquire a Homeserver instance:
|
||||
|
||||
```typescript
|
||||
test("should do something", async ({ homeserver }) => {
|
||||
// homeserver is a Synapse/Dendrite instance
|
||||
// homeserver is a ready-to-use Synapse/Dendrite instance
|
||||
});
|
||||
```
|
||||
|
||||
This returns an object with information about the Homeserver instance, including what port
|
||||
it was started on and the ID that needs to be passed to shut it down again. It also
|
||||
returns the registration shared secret (`registrationSecret`) that can be used to
|
||||
register users via the REST API. The Homeserver has been ensured ready to go by awaiting
|
||||
its internal health-check.
|
||||
**The fixture provides:**
|
||||
|
||||
Homeserver instances should be reasonably cheap to start (you may see the first one take a
|
||||
while as it pulls the Docker image).
|
||||
You do not need to explicitly clean up the instance as it will be cleaned up by the fixture.
|
||||
- Server port information
|
||||
- Instance ID for shutdown
|
||||
- Registration shared secret (`registrationSecret`) for registering users via REST API
|
||||
|
||||
Homeserver instances are:
|
||||
|
||||
- Reasonably cheap to start (first run may be slow while pulling Docker image)
|
||||
- Automatically cleaned up by the fixture
|
||||
|
||||
### Logging In
|
||||
|
||||
We again heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
|
||||
To acquire a logged-in user within a test just add the `user` fixture to the test:
|
||||
Use the `user` fixture to get a logged-in user:
|
||||
|
||||
```typescript
|
||||
test("should do something", async ({ user }) => {
|
||||
// user is a logged in user
|
||||
// user is logged in and ready to use
|
||||
});
|
||||
```
|
||||
|
||||
You can specify a display name for the user via `test.use` `displayName`,
|
||||
otherwise a random one will be generated.
|
||||
This will register a random userId using the registrationSecret with a random password
|
||||
and the given display name. The user fixture will contain details about the credentials for if
|
||||
they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them
|
||||
and the app loaded (path `/`).
|
||||
**Customize the user:**
|
||||
|
||||
```typescript
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test("should do something", async ({ user }) => {
|
||||
// user is logged in as "Alice"
|
||||
});
|
||||
```
|
||||
|
||||
**What the fixture does:**
|
||||
|
||||
- Registers a random userId with the `registrationSecret`
|
||||
- Generates a random password (or uses specified display name)
|
||||
- Seeds localStorage with credentials
|
||||
- Loads the app at path `/`
|
||||
- Provides user details for User-Interactive Auth if needed
|
||||
|
||||
### Joining a Room
|
||||
|
||||
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
||||
way to do this may be to get an access token for the user and use this to create a room with the REST
|
||||
API before logging the user in.
|
||||
You can make use of the bot fixture and the `client` field on the app fixture to do this.
|
||||
To start with a user in a room:
|
||||
|
||||
### Try to write tests from the users' perspective
|
||||
```typescript
|
||||
test("should send a message", async ({ user, app, bot }) => {
|
||||
// Use the bot client to create a room
|
||||
const roomId = await bot.createRoom({
|
||||
name: "Test Room",
|
||||
invite: [user.userId],
|
||||
});
|
||||
|
||||
Like for instance a user will not look for a button by querying a CSS selector.
|
||||
Instead, you should work with roles / labels etc, see https://playwright.dev/docs/locators.
|
||||
// Accept the invite using the app client
|
||||
await app.client.joinRoom(roomId);
|
||||
|
||||
// Now ready to test messaging
|
||||
});
|
||||
```
|
||||
|
||||
**Best practice:** Use the REST API (via `bot` or `app.client`) to set up room state rather than driving the UI.
|
||||
|
||||
### Using matrix-js-sdk
|
||||
|
||||
Due to the way we run the Playwright tests in CI, at this time you can only use the matrix-js-sdk module
|
||||
exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
|
||||
This may be revisited in the future.
|
||||
Due to CI constraints, use the matrix-js-sdk module exposed on `window.matrixcs`:
|
||||
|
||||
## Good Test Hygiene
|
||||
```typescript
|
||||
const matrixcs = window.matrixcs;
|
||||
```
|
||||
|
||||
This section mostly summarises general good Playwright testing practice, and should not be news to anyone
|
||||
already familiar with Playwright.
|
||||
**Limitation:** Only accessible when the app is loaded. This may be revisited in the future.
|
||||
|
||||
1. Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's
|
||||
wrong when they fail.
|
||||
1. Don't depend on state from other tests: any given test should be able to run in isolation.
|
||||
1. Try to avoid driving the UI for anything other than the UI you're trying to test. e.g. if you're
|
||||
testing that the user can send a reaction to a message, it's best to send a message using a REST
|
||||
API, then react to it using the UI, rather than using the element-web UI to send the message.
|
||||
1. Avoid explicit waits. Playwright locators & assertions will implicitly wait for the specified
|
||||
element to appear and all assertions are retried until they either pass or time out, so you should
|
||||
never need to manually wait for an element.
|
||||
- For example, for asserting about editing an already-edited message, you can't wait for the
|
||||
'edited' element to appear as there was already one there, but you can assert that the body
|
||||
of the message is what is should be after the second edit and this assertion will pass once
|
||||
it becomes true. You can then assert that the 'edited' element is still in the DOM.
|
||||
- You can also wait for other things like network requests in the
|
||||
browser to complete (https://playwright.dev/docs/api/class-page#page-wait-for-response).
|
||||
Needing to wait for things can also be because of race conditions in the app itself, which ideally
|
||||
shouldn't be there!
|
||||
### Best Practices
|
||||
|
||||
This is a small selection - the Playwright best practices guide, linked above, has more good advice, and we
|
||||
should generally try to adhere to them.
|
||||
For more guidance, see the [Playwright best practices guide](https://playwright.dev/docs/best-practices).
|
||||
|
||||
## Screenshot testing
|
||||
#### 1. Test from the User's Perspective
|
||||
|
||||
When we previously used Cypress we also dabbled with Percy, and whilst powerful it did not
|
||||
lend itself well to being executed on all PRs without needing to budget it substantially.
|
||||
Work with roles, labels, and accessible elements rather than CSS selectors:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
|
||||
// Avoid
|
||||
await page.locator(".mx_MessageComposer_sendButton").click();
|
||||
```
|
||||
|
||||
See <https://playwright.dev/docs/locators> for more guidance.
|
||||
|
||||
#### 2. Test Well-Isolated Functionality
|
||||
|
||||
- Focus on specific, well-defined units of functionality
|
||||
- Easier to debug when tests fail
|
||||
- More maintainable over time
|
||||
|
||||
#### 3. Maintain Test Independence
|
||||
|
||||
- Each test should run successfully in isolation
|
||||
- Don't depend on state from other tests
|
||||
- Clean up after your test if needed
|
||||
|
||||
#### 4. Minimize UI Driving for Setup
|
||||
|
||||
- Use REST APIs to set up test state when possible
|
||||
- Only drive the UI for the functionality you're actually testing
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Testing reactions - good approach
|
||||
test("should react to a message", async ({ page, app, bot }) => {
|
||||
// Send message via API
|
||||
const eventId = await bot.sendMessage(roomId, "Hello");
|
||||
|
||||
// Test the reaction UI
|
||||
await page.getByText("Hello").hover();
|
||||
await page.getByRole("button", { name: "React" }).click();
|
||||
await page.getByLabel("😀").click();
|
||||
|
||||
// Verify reaction was sent
|
||||
await expect(page.getByLabel("😀 1")).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
#### 5. Avoid Explicit Waits
|
||||
|
||||
Playwright locators and assertions automatically wait and retry:
|
||||
|
||||
```typescript
|
||||
// Good - implicit waiting
|
||||
await expect(page.getByText("Message sent")).toBeVisible();
|
||||
|
||||
// Avoid - explicit waits
|
||||
await page.waitForTimeout(1000);
|
||||
```
|
||||
|
||||
**For dynamic content:**
|
||||
|
||||
```typescript
|
||||
// Assert on the final state - Playwright will wait for it
|
||||
await expect(page.getByRole("textbox")).toHaveValue("Edited message");
|
||||
await expect(page.getByText("edited")).toBeVisible();
|
||||
```
|
||||
|
||||
**When you do need to wait:**
|
||||
|
||||
```typescript
|
||||
// Wait for network requests
|
||||
await page.waitForResponse("**/messages");
|
||||
|
||||
// Wait for specific conditions
|
||||
await page.waitForFunction(() => window.matrixcs !== undefined);
|
||||
```
|
||||
|
||||
## Visual Testing
|
||||
|
||||
Playwright has built-in support for [visual comparison testing](https://playwright.dev/docs/test-snapshots).
|
||||
Screenshots are saved in `playwright/snapshots` and are rendered in a Linux Docker environment for stability.
|
||||
|
||||
One must be careful to exclude any dynamic content from the screenshot, such as timestamps, avatars, etc,
|
||||
via the `mask` option. See the [Playwright docs](https://playwright.dev/docs/test-snapshots#masking).
|
||||
**Screenshot location:** `playwright/snapshots/`
|
||||
|
||||
Some UI elements render differently between test runs, such as BaseAvatar when
|
||||
there is no avatar set, choosing a colour from the theme palette based on the
|
||||
hash of the user/room's Matrix ID. To avoid this creating flaky tests we inject
|
||||
some custom CSS, for this to happen we use the custom assertion `toMatchScreenshot`
|
||||
instead of the native `toHaveScreenshot`.
|
||||
**Rendering environment:** Linux Docker (for consistency across environments)
|
||||
|
||||
If you are running Linux and are unfortunate that the screenshots are not rendering identically,
|
||||
you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you.
|
||||
### Test Tag for Screenshots
|
||||
|
||||
All screenshot tests must use the `@screenshot` tag:
|
||||
|
||||
```typescript
|
||||
test("should render message list", { tag: "@screenshot" }, async ({ page }) => {
|
||||
await expect(page).toMatchScreenshot("message-list.png");
|
||||
});
|
||||
```
|
||||
|
||||
**Purpose of `@screenshot` tag:**
|
||||
|
||||
- Allows running only screenshot tests via `test:playwright:screenshots`
|
||||
- Speeds up screenshot test runs and updates
|
||||
|
||||
### Taking Screenshots
|
||||
|
||||
Use the custom `toMatchScreenshot` assertion (not the native `toHaveScreenshot`):
|
||||
|
||||
```typescript
|
||||
await expect(page).toMatchScreenshot("my-screenshot.png");
|
||||
```
|
||||
|
||||
**Why a custom assertion?** We inject custom CSS to stabilize dynamic UI elements (e.g., BaseAvatar color selection based on Matrix ID hash).
|
||||
|
||||
### Masking Dynamic Content
|
||||
|
||||
Always mask dynamic content that changes between runs:
|
||||
|
||||
```typescript
|
||||
await expect(page).toMatchScreenshot("chat.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp"), page.locator(".mx_BaseAvatar")],
|
||||
});
|
||||
```
|
||||
|
||||
Common elements to mask:
|
||||
|
||||
- Timestamps
|
||||
- Avatars (when dynamic)
|
||||
- Animated elements
|
||||
- User-generated IDs
|
||||
|
||||
See [Playwright masking docs](https://playwright.dev/docs/test-snapshots#masking) for more details.
|
||||
|
||||
### Updating Screenshots
|
||||
|
||||
This command runs only tests tagged with `@screenshot` in the Docker environment.
|
||||
When you need to update screenshot baselines (e.g., after intentional UI changes):
|
||||
|
||||
```sh
|
||||
yarn run test:playwright:screenshots
|
||||
```
|
||||
|
||||
**Important:** Always use this command to update screenshots rather than running tests locally with `--update-snapshots`.
|
||||
|
||||
**Why?** Screenshots must be rendered in a consistent Linux Docker environment because:
|
||||
|
||||
- Font rendering differs between operating systems (macOS, Windows, Linux)
|
||||
- Subpixel rendering varies across systems
|
||||
- Browser rendering engines have platform-specific differences
|
||||
|
||||
Using `test:playwright:screenshots` ensures screenshots are generated in the same Docker environment used in CI, preventing false failures due to rendering differences.
|
||||
|
||||
## Test Tags
|
||||
|
||||
We use test tags to categorise tests for running subsets more efficiently.
|
||||
Test tags categorize tests for efficient subset execution.
|
||||
|
||||
- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue.
|
||||
- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection.
|
||||
- `@no-$project`: Tests which are unsupported in $Project. These tests will be skipped when running in $Project.
|
||||
### Available Tags
|
||||
|
||||
Anything testing Matrix media will need to have `@no-firefox` and `@no-webkit` as those rely on the service worker which
|
||||
has to be disabled in Playwright on Firefox & Webkit to retain routing functionality.
|
||||
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
|
||||
there at this time.
|
||||
- **`@mergequeue`**: Slow or flaky tests covering rarely-updated app areas
|
||||
- Not run on every PR commit
|
||||
- Run in the Merge Queue
|
||||
|
||||
If you wish to run all tests in a PR, you can give it the label `X-Run-All-Tests`.
|
||||
- **`@screenshot`**: Tests using `toMatchScreenshot` for visual regression testing
|
||||
- See the [Visual Testing](#visual-testing) section for detailed usage
|
||||
|
||||
## Supporter container runtimes
|
||||
- **`@no-firefox`**: Tests unsupported in Firefox
|
||||
- Automatically skipped in Firefox project
|
||||
- Common reason: Service worker required (disabled in Playwright Firefox for routing)
|
||||
|
||||
We use testcontainers to spin up various instances of Synapse, Matrix Authentication Service, and more.
|
||||
It supports Docker out of the box but also has support for Podman, Colima, Rancher, you just need to follow some instructions to achieve it:
|
||||
https://node.testcontainers.org/supported-container-runtimes/
|
||||
- **`@no-webkit`**: Tests unsupported in Webkit
|
||||
- Automatically skipped in Webkit project
|
||||
- Common reasons: Service worker required, microphone functionality unavailable
|
||||
|
||||
If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path
|
||||
within `$HOME` to allow bind mounting temporary directories into the Docker containers.
|
||||
### Running All Tests in a PR
|
||||
|
||||
Add the `X-Run-All-Tests` label to your pull request to run all tests, including `@mergequeue` tests.
|
||||
|
||||
## Supported Container Runtimes
|
||||
|
||||
We use [testcontainers](https://node.testcontainers.org/) to manage Synapse, Matrix Authentication Service, and other service instances.
|
||||
|
||||
**Supported runtimes:**
|
||||
|
||||
- Docker (default, recommended)
|
||||
- Podman
|
||||
- Colima
|
||||
See setup instructions: <https://node.testcontainers.org/supported-container-runtimes/>
|
||||
|
||||
### Platform-Specific Configuration
|
||||
|
||||
**Colima users:**
|
||||
|
||||
If using Colima, you may need to set the `TMPDIR` environment variable to allow bind mounting temporary directories:
|
||||
|
||||
```sh
|
||||
export TMPDIR=/tmp/colima
|
||||
# or
|
||||
export TMPDIR=$HOME/tmp
|
||||
```
|
||||
|
||||
**macOS users:**
|
||||
|
||||
Docker Desktop and Colima are both well-supported on macOS.
|
||||
|
||||
> [!CAUTION]
|
||||
> Do not set `DOCKER_HOST` when running tests. Element Web uses [element-web-playwright-common](https://github.com/element-hq/element-modules/tree/main/packages/element-web-playwright-common), and setting `DOCKER_HOST` causes issues with testcontainers when running in the container VM.
|
||||
|
||||
@@ -58,6 +58,12 @@ We are aiming for a set of common strings to be shared then some more localised
|
||||
Edits to existing strings should be performed only via Localazy.
|
||||
There you can also require all translations to be redone if the meaning of the string has changed significantly.
|
||||
|
||||
## Removing existing strings
|
||||
|
||||
You can remove an existing string by removing the key from `en_EN.json`. Do not modify other language json files for this purpose.
|
||||
|
||||
Localazy will mark the keys you removed as deprecated. See https://localazy.com/docs/general/editing-source-language#source-key-states for more information about the difference between deprecated keys and deleted keys.
|
||||
|
||||
## Adding variables inside a string.
|
||||
|
||||
1. Extend your `_t()` call. Instead of `_t(TKEY)` use `_t(TKEY, {})`
|
||||
@@ -66,7 +72,7 @@ There you can also require all translations to be redone if the meaning of the s
|
||||
1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name.
|
||||
|
||||
- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two).
|
||||
- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink.
|
||||
- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally `<a>` rather than making a hyperlink.
|
||||
- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })`.
|
||||
|
||||
## Things to know/Style Guides
|
||||
@@ -78,4 +84,4 @@ There you can also require all translations to be redone if the meaning of the s
|
||||
- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages.
|
||||
- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence.
|
||||
- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments.
|
||||
- Don't forget curly braces when you assign an expression to JSX attributes in the render method)
|
||||
- Don't forget curly braces when you assign an expression to JSX attributes in the render method.
|
||||
|
||||
@@ -11,7 +11,7 @@ import { env } from "process";
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
testEnvironment: "jsdom",
|
||||
testEnvironment: "jest-fixed-jsdom",
|
||||
testEnvironmentOptions: {
|
||||
url: "http://localhost/",
|
||||
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
||||
@@ -39,11 +39,10 @@ const config: Config = {
|
||||
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||
"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|matrix-web-i18n)).+$",
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||
|
||||
@@ -43,7 +43,8 @@ export default {
|
||||
// Embedded into webapp
|
||||
"@element-hq/element-call-embedded",
|
||||
// Transitive dep of jest
|
||||
"jsdom",
|
||||
"@jest/globals",
|
||||
"vitest-environment-jest-fixed-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
|
||||
|
||||
@@ -18,6 +18,18 @@
|
||||
"file": "element-web.json",
|
||||
"excludes": ["src/i18n/strings/en_EN.json"],
|
||||
"lang": "${autodetectLang}"
|
||||
},
|
||||
{
|
||||
"pattern": "packages/shared-components/src/i18n/strings/en_EN.json",
|
||||
"file": "shared-components.json",
|
||||
"lang": "inherited"
|
||||
},
|
||||
{
|
||||
"group": "existing",
|
||||
"pattern": "packages/shared-components/src/i18n/strings/*.json",
|
||||
"file": "shared-components.json",
|
||||
"excludes": ["packages/shared-components/src/i18n/strings/en_EN.json"],
|
||||
"lang": "${autodetectLang}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -27,6 +39,10 @@
|
||||
{
|
||||
"conditions": "equals: ${file}, element-web.json",
|
||||
"output": "src/i18n/strings/${langLsrUnderscore}.json"
|
||||
},
|
||||
{
|
||||
"conditions": "equals: ${file}, shared-components.json",
|
||||
"output": "packages/shared-components/src/i18n/strings/${langLsrUnderscore}.json"
|
||||
}
|
||||
],
|
||||
"includeSourceLang": "${includeSourceLang|false}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.12.7",
|
||||
"version": "1.12.9",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -29,9 +29,9 @@
|
||||
"UserFriendlyError"
|
||||
],
|
||||
"scripts": {
|
||||
"i18n": "matrix-gen-i18n src res packages/shared-components/src && yarn i18n:sort && yarn i18n:lint",
|
||||
"i18n:sort": "jq --sort-keys '.' src/i18n/strings/en_EN.json > src/i18n/strings/en_EN.json.tmp && mv src/i18n/strings/en_EN.json.tmp src/i18n/strings/en_EN.json",
|
||||
"i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null",
|
||||
"i18n": "matrix-gen-i18n src res && yarn i18n:sort && yarn i18n:lint",
|
||||
"i18n:sort": "matrix-sort-i18n src/i18n/strings/en_EN.json && yarn --cwd packages/shared-components i18n:sort",
|
||||
"i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null && yarn --cwd packages/shared-components i18n:lint",
|
||||
"i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && yarn i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
||||
"make-component": "node scripts/make-react-component.js",
|
||||
"rethemendex": "./res/css/rethemendex.sh",
|
||||
@@ -69,12 +69,12 @@
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.2.0",
|
||||
"@types/react": "19.2.6",
|
||||
"**/pretty-format/react-is": "19.2.3",
|
||||
"@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.30001756",
|
||||
"caniuse-lite": "1.0.30001762",
|
||||
"testcontainers": "^11.0.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
@@ -85,15 +85,15 @@
|
||||
"@element-hq/web-shared-components": "link:packages/shared-components",
|
||||
"@fontsource/fira-code": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
"@formatjs/intl-segmenter": "^12.0.0",
|
||||
"@matrix-org/analytics-events": "^0.30.0",
|
||||
"@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.4.1",
|
||||
"@vector-im/compound-web": "^8.3.1",
|
||||
"@vector-im/compound-design-tokens": "6.8.0",
|
||||
"@vector-im/compound-web": "^8.3.5",
|
||||
"@vector-im/matrix-wysiwyg": "2.40.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
@@ -117,7 +117,7 @@
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"html-react-parser": "^5.2.2",
|
||||
"is-ip": "^3.1.0",
|
||||
"is-ip": "^5.0.0",
|
||||
"js-xxhash": "^5.0.0",
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
@@ -129,15 +129,15 @@
|
||||
"lodash": "^4.17.21",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-js-sdk": "39.4.0",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"matrix-js-sdk": "40.1.0",
|
||||
"matrix-widget-api": "^1.15.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.297.2",
|
||||
"posthog-js": "1.313.0",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
@@ -179,9 +179,10 @@
|
||||
"@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",
|
||||
"@element-hq/element-web-playwright-common": "^2.0.0",
|
||||
"@casualbot/jest-sonar-reporter": "2.5.0",
|
||||
"@element-hq/element-call-embedded": "0.16.3",
|
||||
"@element-hq/element-web-playwright-common": "2.2.3",
|
||||
"@fetch-mock/jest": "^0.2.20",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
@@ -210,10 +211,9 @@
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/modernizr": "^3.5.3",
|
||||
"@types/node": "18",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.2.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
@@ -228,10 +228,9 @@
|
||||
"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",
|
||||
"cronstrue": "^3.0.0",
|
||||
"css-loader": "^7.0.0",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
@@ -250,25 +249,23 @@
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"express": "^5.0.0",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"husky": "^9.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"jest-fixed-jsdom": "^0.0.11",
|
||||
"jest-mock": "^30.0.0",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"jsqr": "^1.4.0",
|
||||
"knip": "^5.36.2",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"matrix-web-i18n": "3.5.2",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimist": "^1.2.6",
|
||||
"modernizr": "^3.12.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright-core": "^1.51.0",
|
||||
"postcss": "8.4.46",
|
||||
@@ -281,7 +278,7 @@
|
||||
"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",
|
||||
@@ -294,7 +291,7 @@
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^11.0.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript": "5.9.3",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Even though this (at time of writing) is identical Element Web's
|
||||
// .prettierrc.js, shared components needs its own because otherwise
|
||||
// this refers to element web's copy of eslint-plugin-matrix-org which
|
||||
// would require element-web's modules to be installed.
|
||||
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
|
||||
@@ -10,16 +10,14 @@ import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/com
|
||||
import React from "react";
|
||||
import { GlobeIcon } from "@storybook/icons";
|
||||
|
||||
// We can't import `shared/i18n.tsx` directly here.
|
||||
// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
|
||||
import json from "../../../webapp/i18n/languages.json";
|
||||
const languages = Object.keys(json).filter((lang) => lang !== "default");
|
||||
const languages = JSON.parse(process.env.STORYBOOK_LANGUAGES);
|
||||
|
||||
/**
|
||||
* Returns the title of a language in the user's locale.
|
||||
*/
|
||||
function languageTitle(language: string): string {
|
||||
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
|
||||
const normalisedLang = language.toLowerCase().replace("_", "-");
|
||||
return new Intl.DisplayNames([normalisedLang], { type: "language", style: "short" }).of(normalisedLang) || language;
|
||||
}
|
||||
|
||||
export const languageAddon: Addon = {
|
||||
|
||||
@@ -7,12 +7,15 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
||||
import { mergeConfig } from "vite";
|
||||
|
||||
// Get a list of available languages so the language selector can display them at runtime
|
||||
const languages = fs.readdirSync("src/i18n/strings").map((f) => f.slice(0, -5));
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
staticDirs: ["../../../webapp"],
|
||||
addons: ["@storybook/addon-docs", "@storybook/addon-designs", "@storybook/addon-a11y"],
|
||||
framework: "@storybook/react-vite",
|
||||
core: {
|
||||
@@ -29,8 +32,42 @@ const config: StorybookConfig = {
|
||||
$webapp: path.resolve("../../webapp"),
|
||||
},
|
||||
},
|
||||
// Needed for counterpart to work
|
||||
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
||||
plugins: [
|
||||
// Needed for counterpart to work
|
||||
nodePolyfills({ include: ["process", "util"] }),
|
||||
{
|
||||
name: "language-middleware",
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (req.url === "/i18n/languages.json") {
|
||||
// Dynamically generate a languages.json file based on what files are available
|
||||
const langJson: Record<string, string> = {};
|
||||
for (const lang of languages) {
|
||||
const normalizedLanguage = lang.toLowerCase().replace("_", "-");
|
||||
const languageParts = normalizedLanguage.split("-");
|
||||
if (languageParts.length === 2 && languageParts[0] === languageParts[1]) {
|
||||
langJson[languageParts[0]] = `${lang}.json`;
|
||||
} else {
|
||||
langJson[normalizedLanguage] = `${lang}.json`;
|
||||
}
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify(langJson));
|
||||
} else if (req.url?.startsWith("/i18n/")) {
|
||||
// Serve the individual language files, which annoyingly can't be a simple
|
||||
// static dir because the directory structure in src doesn't match what
|
||||
// the app requests.
|
||||
const langFile = req.url.split("/").pop();
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
fs.createReadStream(`src/i18n/strings/${langFile}`).pipe(res);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
server: {
|
||||
allowedHosts: ["localhost", ".docker.internal"],
|
||||
},
|
||||
@@ -42,5 +79,9 @@ const config: StorybookConfig = {
|
||||
url: "https://element-hq.github.io/compound-web/",
|
||||
},
|
||||
},
|
||||
env: (config) => ({
|
||||
...config,
|
||||
STORYBOOK_LANGUAGES: JSON.stringify(languages),
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -20,7 +20,7 @@ const config: TestRunnerConfig = {
|
||||
|
||||
// If you want to take screenshot of multiple browsers, use
|
||||
// page.context().browser().browserType().name() to get the browser name to prefix the file name
|
||||
const image = await page.screenshot();
|
||||
const image = await page.screenshot({ animations: "disabled" });
|
||||
expect(image).toMatchImageSnapshot({
|
||||
customSnapshotsDir,
|
||||
customSnapshotIdentifier: `${context.id}-${process.platform}`,
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
# @element-hq/web-shared-components
|
||||
|
||||
Shared React components library for Element Web, Aurora, Element
|
||||
modules... This package provides opinionated UI components built on top of the
|
||||
[Compound Design System](https://compound.element.io) and [Compound
|
||||
Web](https://github.com/element-hq/compound-web). This is not a design system
|
||||
by itself, but rather a set of larger components.
|
||||
|
||||
## Installation in a new project
|
||||
|
||||
When adding this library to a new project, as well as installing
|
||||
`@element-hq/web-shared-components` as normal, you will also need to add
|
||||
[compound-web](https://github.com/element-hq/compound-web) as a peer
|
||||
dependency:
|
||||
|
||||
```bash
|
||||
yarn add @element-hq/web-shared-components
|
||||
yarn add @vector-im/compound-web
|
||||
```
|
||||
|
||||
(This avoids problems where we end up with different versions of compound-web in the
|
||||
top-level project and web-shared-components).
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Import
|
||||
|
||||
Both JavaScript and CSS can be imported as follows:
|
||||
|
||||
```javascript
|
||||
import { RoomListHeaderView, useViewModel } from "@element-hq/web-shared-components";
|
||||
import "@element-hq/web-shared-components/dist/element-web-shared-components.css";
|
||||
```
|
||||
|
||||
or in CSS file:
|
||||
|
||||
```css
|
||||
@import url("@element-hq/web-shared-components");
|
||||
```
|
||||
|
||||
### Using Components
|
||||
|
||||
There are two kinds of components in this library:
|
||||
|
||||
- _regular_ react component which doesn't follow specific pattern.
|
||||
- _view_ component(MVVM pattern).
|
||||
|
||||
> [!TIP]
|
||||
> These components are available in the project storybook.
|
||||
|
||||
#### Regular Components
|
||||
|
||||
These components can be used directly by passing props. Example:
|
||||
|
||||
```tsx
|
||||
import { Flex } from "@element-hq/web-shared-components";
|
||||
function MyApp() {
|
||||
return <Flex align="center" />;
|
||||
}
|
||||
```
|
||||
|
||||
#### View (MVVM) Components
|
||||
|
||||
These components follow the [MVVM pattern](../../docs/MVVM.md). A ViewModel
|
||||
instance should be provided as a prop.
|
||||
|
||||
Here's a basic example:
|
||||
|
||||
```jsx
|
||||
import { ViewExample } from "@element-hq/web-shared-components";
|
||||
|
||||
function MyApp() {
|
||||
const viewModel = new ViewModelExample();
|
||||
return <ViewExample vm={viewModel} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Utilities
|
||||
|
||||
#### Internationalization
|
||||
|
||||
- `useI18n()` - Hook for translations
|
||||
- `I18nApi` - Internationalization API utilities
|
||||
|
||||
#### Date & Time
|
||||
|
||||
- `DateUtils` - Date formatting and manipulation
|
||||
- `humanize` - Human-readable time formatting
|
||||
|
||||
#### Formatting
|
||||
|
||||
- `FormattingUtils` - Text and data formatting utilities
|
||||
- `numbers` - Number formatting utilities
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >= 20.0.0
|
||||
- Yarn 1.22.22+
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Build the library
|
||||
yarn prepare
|
||||
```
|
||||
|
||||
### Running Storybook
|
||||
|
||||
```bash
|
||||
yarn storybook
|
||||
```
|
||||
|
||||
### Write components
|
||||
|
||||
Most components should be written as [MVVM pattern](../../docs/MVVM.md) view
|
||||
components. See existing components for examples. The exceptions are low level
|
||||
components that don't need a view model.
|
||||
|
||||
### Tests
|
||||
|
||||
Two types of tests are available: unit tests and visual regression tests.
|
||||
|
||||
### Unit Tests
|
||||
|
||||
These tests cover the logic of the components and utilities. Built with Jest
|
||||
and React Testing Library.
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
### Visual Regression Tests
|
||||
|
||||
These tests ensure the UI components render correctly. They need Storybook to
|
||||
be running and they will run in docker using [Playwright](../../playwright.md).
|
||||
|
||||
First run storybook:
|
||||
|
||||
```bash
|
||||
yarn storybook
|
||||
```
|
||||
|
||||
Then, in another terminal, run:
|
||||
|
||||
```bash
|
||||
yarn test:storybook:update
|
||||
```
|
||||
|
||||
Each story will be rendered and a screenshot will be taken and compared to the
|
||||
existing baseline. If there are visual changes or AXE violation, the test will
|
||||
fail.
|
||||
|
||||
### Translations
|
||||
|
||||
First see our [translation guide](../../docs/translation.md) and [translation dev guide](../../docs/translation-dev.md).
|
||||
To generate translation strings for this package, run:
|
||||
|
||||
```bash
|
||||
yarn i18n
|
||||
```
|
||||
@@ -10,7 +10,7 @@ import { env } from "process";
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
testEnvironment: "jsdom",
|
||||
testEnvironment: "jest-fixed-jsdom",
|
||||
testEnvironmentOptions: {
|
||||
url: "http://localhost/",
|
||||
},
|
||||
@@ -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|@storybook|storybook)).+$",
|
||||
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|@storybook|storybook|matrix-web-i18n)).+$",
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@element-hq/web-shared-components",
|
||||
"version": "0.0.0-test.11",
|
||||
"version": "0.0.0-test.12",
|
||||
"description": "Shared components for Element",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -34,8 +34,11 @@
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"i18n": "matrix-gen-i18n src && yarn i18n:sort && yarn i18n:lint",
|
||||
"i18n:sort": "jq --sort-keys '.' src/i18n/strings/en_EN.json > src/i18n/strings/en_EN.json.tmp && mv src/i18n/strings/en_EN.json.tmp src/i18n/strings/en_EN.json",
|
||||
"i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null",
|
||||
"test": "jest",
|
||||
"prepare": "patch-package && yarn --cwd ../.. build:res && node scripts/gatherTranslationKeys.ts && vite build",
|
||||
"prepare": "patch-package && vite build",
|
||||
"storybook": "storybook dev -p 6007",
|
||||
"build-storybook": "storybook build",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
@@ -43,24 +46,24 @@
|
||||
"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 yarn --with-node-modules && 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 /work/scripts/storybook-screenshot-update.sh --with-node-modules"
|
||||
},
|
||||
"resolutions": {
|
||||
"playwright": "1.57.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-hq/element-web-module-api": "^1.8.0",
|
||||
"@vector-im/compound-design-tokens": "^6.3.0",
|
||||
"@vector-im/compound-design-tokens": "^6.4.3",
|
||||
"classnames": "^2.5.1",
|
||||
"counterpart": "^0.18.6",
|
||||
"lodash": "^4.17.21",
|
||||
"matrix-web-i18n": "^3.4.0",
|
||||
"matrix-web-i18n": "3.5.2",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-merge-refs": "^3.0.2",
|
||||
"temporal-polyfill": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-hq/element-web-playwright-common": "^2.0.0",
|
||||
"@element-hq/element-web-playwright-common": "2.2.3",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@storybook/addon-a11y": "^10.0.7",
|
||||
"@storybook/addon-designs": "^11.0.1",
|
||||
@@ -93,6 +96,6 @@
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"peerDependencies": {
|
||||
"@vector-im/compound-web": "^8.2.5"
|
||||
"@vector-im/compound-web": "^8.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 57 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 |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Gathers all the translation keys from element-web's en_EN.json into a TypeScript type definition file
|
||||
// that exports a type `TranslationKey` which is a union of all supported translation keys.
|
||||
// This prevents having to import the json file and make typescript do the work as this results in vite-dts
|
||||
// generating an import to the json file in the .d.ts which doesn't work at runtime: this way, the type
|
||||
// gets put into the bundle.
|
||||
// XXX: It should *not* be in the 'src' directory, being a generated file, but if it isn't then the type
|
||||
// bundler won't bundle the types and will leave the file as a relative import, which will break.
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const i18nStringsPath = path.resolve(__dirname, "../../../src/i18n/strings/en_EN.json");
|
||||
const outPath = path.resolve(__dirname, "../src/i18nKeys.d.ts");
|
||||
|
||||
function gatherKeys(obj: any, prefix: string[] = []): string[] {
|
||||
if (typeof obj !== "object" || obj === null) return [];
|
||||
let keys: string[] = [];
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
|
||||
// add the path (for both leaves and intermediates as then we include plurals)
|
||||
keys.push([...prefix, key].join("|"));
|
||||
if (typeof value === "object" && value !== null) {
|
||||
// If the value is an object, recurse
|
||||
keys = keys.concat(gatherKeys(value, [...prefix, key]));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const json = JSON.parse(fs.readFileSync(i18nStringsPath, "utf8"));
|
||||
const keys = gatherKeys(json);
|
||||
const typeDef =
|
||||
"/*\n" +
|
||||
" * Copyright 2025 Element Creations Ltd.\n" +
|
||||
" *\n" +
|
||||
" * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial\n" +
|
||||
" * Please see LICENSE files in the repository root for full details.\n" +
|
||||
" */\n" +
|
||||
"\n" +
|
||||
"// This file is auto-generated by gatherTranslationKeys.ts\n" +
|
||||
"// Do not edit manually.\n\n" +
|
||||
"export type TranslationKey =\n" +
|
||||
keys.map((k) => ` | \"${k}\"`).join("\n") +
|
||||
";\n";
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, typeDef, "utf8");
|
||||
console.log(`Wrote ${keys.length} keys to ${outPath}`);
|
||||
}
|
||||
|
||||
if (import.meta.url.startsWith("file:")) {
|
||||
const modulePath = fileURLToPath(import.meta.url);
|
||||
if (process.argv[1] === modulePath) {
|
||||
main();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Update storybook screenshots
|
||||
#
|
||||
# This script should be used as the entrypoint parameter of the `playwright-screenshots` script. It
|
||||
# installs the yarn dependencies, and then runs `test-storybook` to update the storybook screenshots.
|
||||
#
|
||||
# It expects to find a storybook instance running at :6007 on the host machine. It also requires that
|
||||
# `playwright-screenshots` is given the `--with-node-modules` parameter.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# test-storybook --url http://localhost:6007/
|
||||
# playwright-screenshots --entrypoint /work/scripts/storybook-screenshot-update.sh --with-node-modules
|
||||
#
|
||||
#
|
||||
# Note: even though this script is small, it is important because the alternative is running
|
||||
# `playwright-screenshots` twice in quick succession (once to do `yarn install`, a second to do the
|
||||
# actual updates): and that fails, because running `playwright-screenshots` without actually starting
|
||||
# Testcontainers leaves a ryuk container hanging around for up to 60s, which will block the second
|
||||
# invocation.
|
||||
|
||||
set -e
|
||||
|
||||
# First install dependencies. We have to do this within the playwright container rather than the host,
|
||||
# because we have which must be built for the right architecture (and some environments use a VM
|
||||
# to run docker containers, meaning that things inside a container use a different architecture than
|
||||
# those on the host).
|
||||
yarn
|
||||
|
||||
# Now run the screenshot update
|
||||
/work/node_modules/.bin/test-storybook --url http://host.docker.internal:6007/ --updateSnapshot
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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 as _TranslationKey } from "matrix-web-i18n";
|
||||
|
||||
import type Translations from "../i18n/strings/en_EN.json";
|
||||
|
||||
declare global {
|
||||
type TranslationKey = _TranslationKey<typeof Translations>;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ exports[`AudioPlayerView renders the audio player in default state 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="_r_0_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
class="_icon-button_1215g_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -107,7 +107,7 @@ exports[`AudioPlayerView renders the audio player in error state 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="_r_i_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
class="_icon-button_1215g_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -203,7 +203,7 @@ exports[`AudioPlayerView renders the audio player without media name 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="_r_6_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
class="_icon-button_1215g_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -294,7 +294,7 @@ exports[`AudioPlayerView renders the audio player without size 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="_r_c_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
class="_icon-button_1215g_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
||||
@@ -6,7 +6,7 @@ exports[`PlayPauseButton renders the button in default state 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="_r_0_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
class="_icon-button_1215g_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -38,7 +38,7 @@ exports[`PlayPauseButton renders the button in playing state 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-label="Pause"
|
||||
aria-labelledby="_r_6_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
class="_icon-button_1215g_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
padding: var(--cpd-space-4x);
|
||||
|
||||
border-top: 1px solid var(--cpd-color-gray-400);
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.banner[data-type="success"] {
|
||||
@@ -90,4 +88,6 @@
|
||||
flex-direction: row;
|
||||
gap: var(--cpd-space-1x);
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ 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",
|
||||
@@ -46,17 +45,14 @@ 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>,
|
||||
},
|
||||
)}
|
||||
Alice's (<b>@alice:example.com</b>) identity was reset. <a href="https://example.org">Learn more</a>
|
||||
</p>
|
||||
),
|
||||
actions: <Button kind="primary">{_t("encryption|withdraw_verification_action")}</Button>,
|
||||
actions: (
|
||||
<Button kind="primary" size="sm">
|
||||
Withdraw verification
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,3 +67,19 @@ export const WithoutClose: Story = {
|
||||
onClose: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLoadsOfContent: Story = {
|
||||
args: {
|
||||
type: "info",
|
||||
children: (
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quis massa facilisis, venenatis risus
|
||||
consectetur, sagittis libero. Aenean et scelerisque justo. Nunc luctus, mi sed facilisis suscipit, magna
|
||||
ante pharetra sem, eu rutrum purus quam quis arcu. Sed eleifend arcu vitae magna sodales, sit amet
|
||||
fermentum urna dictum. Mauris vel velit pulvinar enim mollis tincidunt. Vivamus egestas rhoncus
|
||||
sagittis. Curabitur auctor vehicula massa, et cursus lacus laoreet a. Maecenas et sollicitudin lectus,
|
||||
in ligula.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import React, {
|
||||
type ReactNode,
|
||||
type PropsWithChildren,
|
||||
useMemo,
|
||||
type HTMLAttributes,
|
||||
} from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle";
|
||||
@@ -32,8 +33,6 @@ interface BannerProps {
|
||||
*/
|
||||
avatar?: React.ReactNode;
|
||||
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Actions presented to the user in the right-hand side of the banner alongside the dismiss button.
|
||||
*/
|
||||
@@ -60,26 +59,26 @@ export function Banner({
|
||||
actions,
|
||||
onClose,
|
||||
...props
|
||||
}: PropsWithChildren<BannerProps>): ReactElement {
|
||||
}: PropsWithChildren<BannerProps & HTMLAttributes<HTMLDivElement>>): ReactElement {
|
||||
const classes = classNames(styles.banner, className);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
const icon = useMemo((): ReactElement => {
|
||||
switch (type) {
|
||||
case "critical":
|
||||
return <ErrorIcon fontSize={24} {...props} />;
|
||||
return <ErrorIcon fontSize={24} />;
|
||||
case "info":
|
||||
return <InfoIcon fontSize={24} {...props} />;
|
||||
return <InfoIcon fontSize={24} />;
|
||||
case "success":
|
||||
return <CheckCircleIcon fontSize={24} {...props} />;
|
||||
return <CheckCircleIcon fontSize={24} />;
|
||||
default:
|
||||
return <InfoIcon fontSize={24} {...props} />;
|
||||
return <InfoIcon fontSize={24} />;
|
||||
}
|
||||
}, [type, props]);
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<div {...props} className={classes} data-type={type}>
|
||||
<div className={styles.icon}>{avatar ?? icon}</div>
|
||||
<span className={styles.content}>{children}</span>
|
||||
<div className={styles.content}>{children}</div>
|
||||
<div className={styles.actions}>
|
||||
{actions}
|
||||
{onClose && (
|
||||
|
||||
@@ -26,27 +26,36 @@ exports[`AvatarWithDetails renders a banner with an action 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
encryption|pinned_identity_changed
|
||||
Alice's (
|
||||
<b>
|
||||
@alice:example.com
|
||||
</b>
|
||||
) identity was reset.
|
||||
<a
|
||||
href="https://example.org"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
encryption|withdraw_verification_action
|
||||
Withdraw verification
|
||||
</button>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
@@ -72,18 +81,18 @@ exports[`AvatarWithDetails renders a banner with an avatar iamge 1`] = `
|
||||
src="https://picsum.photos/32/32"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
@@ -118,18 +127,18 @@ exports[`AvatarWithDetails renders a critical banner 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
@@ -168,18 +177,18 @@ exports[`AvatarWithDetails renders a default banner 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
@@ -219,18 +228,18 @@ exports[`AvatarWithDetails renders a info banner 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
@@ -265,18 +274,18 @@ exports[`AvatarWithDetails renders a success banner 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
||||
@@ -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>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<span>
|
||||
This room has been configured so that new members can read history.
|
||||
<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>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_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";
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Panel posunu zvuku"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Smazat",
|
||||
"dismiss": "Zavřít",
|
||||
"explore_rooms": "Procházet místnosti",
|
||||
"pause": "Pozastavit",
|
||||
"play": "Přehrát",
|
||||
"search": "Hledání"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Otevřít číselník"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "před jedním dnem",
|
||||
"about_hour_ago": "asi před hodinou",
|
||||
"about_minute_ago": "před minutou",
|
||||
"few_seconds_ago": "před pár vteřinami",
|
||||
"in_about_day": "asi za den",
|
||||
"in_about_hour": "asi za hodinu",
|
||||
"in_about_minute": "asi za minutu",
|
||||
"in_few_seconds": "za pár vteřin",
|
||||
"in_n_days": "za %(num)s dní",
|
||||
"in_n_hours": "za %(num)s hodin",
|
||||
"in_n_minutes": "za %(num)s minut",
|
||||
"n_days_ago": "před %(num)s dny",
|
||||
"n_hours_ago": "před %(num)s hodinami",
|
||||
"n_minutes_ago": "před %(num)s minutami"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Audio přehrávač",
|
||||
"error_downloading_audio": "Chyba při stahování audia",
|
||||
"unnamed_audio": "Nepojmenovaný audio soubor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Bar chwilio sain"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Dileu",
|
||||
"dismiss": "Gwrthod",
|
||||
"explore_rooms": "Archwilio Ystafelloedd",
|
||||
"pause": "Oedi",
|
||||
"play": "Chwarae",
|
||||
"retry": "Ceisio eto",
|
||||
"search": "Chwilio"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Agor y pad deialu"
|
||||
},
|
||||
"room": {
|
||||
"status_bar": {
|
||||
"delete_all": "Dileu'r cyfan",
|
||||
"exceeded_resource_limit_description": "Cysylltwch â gweinyddwr eich gwasanaeth i barhau i ddefnyddio'r gwasanaeth.",
|
||||
"exceeded_resource_limit_title": "Dyw eich neges heb ei anfon oherwydd bod y gweinydd cartref hwn wedi mynd y tu hwnt i derfyn ei adnoddau.",
|
||||
"failed_to_create_room_title": "Methwyd dechrau sgwrs gyda'r defnyddiwr hwn",
|
||||
"history_visible": "Mae'r ystafell hon wedi'i ffurfweddu fel y gall aelodau newydd ddarllen hanes. <a>Dysgu Mwy</a>",
|
||||
"homeserver_blocked_title": "Dyw eich neges heb ei anfon oherwydd bod y gweinydd cartref hwn wedi'i rwystro gan ei weinyddwr.",
|
||||
"monthly_user_limit_reached_title": "Dyw eich neges heb ei anfon oherwydd bod y gweinydd cartref hwn wedi cyrraedd ei Derfyn Defnyddiwr Gweithredol Misol.",
|
||||
"requires_consent_agreement_title": "Does dim modd i chi anfon unrhyw negeseuon nes i chi adolygu a chytuno â'n telerau ac amodau.",
|
||||
"retry_all": "Ail-geisio popeth",
|
||||
"select_messages_to_retry": "Gallwch ddewis pob neges neu negeseuon unigol i geisio eto neu eu dileu",
|
||||
"server_connectivity_lost_description": "Bydd negeseuon sy'n cael eu hanfon yn cael eu cadw nes bod eich cysylltiad wedi dychwelyd.",
|
||||
"server_connectivity_lost_title": "Mae'r cysylltiad â'r gweinydd wedi'i golli.",
|
||||
"some_messages_not_sent": "Dyw rhai o'ch negeseuon heb eu hanfon"
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"tac_button": "Adolygwch y telerau a'r amodau"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "tua diwrnod yn ôl",
|
||||
"about_hour_ago": "tua awr yn ol",
|
||||
"about_minute_ago": "tua munud yn ôl",
|
||||
"few_seconds_ago": "ychydig eiliadau yn ôl",
|
||||
"in_about_day": "tua diwrnod o nawr",
|
||||
"in_about_hour": "tuag awr o hyn",
|
||||
"in_about_minute": "tua munud o nawr",
|
||||
"in_few_seconds": "ychydig eiliadau o nawr",
|
||||
"in_n_days": "%(num)s diwrnod o nawr",
|
||||
"in_n_hours": "%(num)s awr o nawr",
|
||||
"in_n_minutes": "%(num)s munud o nawr",
|
||||
"n_days_ago": "%(num)s diwrnod yn ôl",
|
||||
"n_hours_ago": "%(num)s awr yn ôl",
|
||||
"n_minutes_ago": "%(num)s munud yn ôl"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Chwaraewr sain",
|
||||
"error_downloading_audio": "Gwall wrth llwytho i lawrsain",
|
||||
"unnamed_audio": "Sain dienw"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Progressionsmarkør for lydafspiller"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Slet",
|
||||
"dismiss": "Afvis",
|
||||
"explore_rooms": "Udforsk rum",
|
||||
"pause": "Pausér",
|
||||
"play": "Afspil",
|
||||
"search": "Søg"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "omkring en dag siden",
|
||||
"about_hour_ago": "for omkring en time siden",
|
||||
"about_minute_ago": "for omkring et minut siden",
|
||||
"few_seconds_ago": "for et par sekunder siden",
|
||||
"in_about_day": "om cirka en dag fra nu",
|
||||
"in_about_hour": "omkring en time fra nu",
|
||||
"in_about_minute": "omkring et minut fra nu",
|
||||
"in_few_seconds": "et par sekunder fra nu",
|
||||
"in_n_days": "%(num)s dage fra nu",
|
||||
"in_n_hours": "%(num)s timer fra nu",
|
||||
"in_n_minutes": "%(num)s minutter fra nu",
|
||||
"n_days_ago": "%(num)s dage siden",
|
||||
"n_hours_ago": "%(num)s timer siden",
|
||||
"n_minutes_ago": "%(num)s minutter siden"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "Fejl ved download af lyd",
|
||||
"unnamed_audio": "Unavngiven lyd"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Audio-Suchleiste"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Löschen",
|
||||
"dismiss": "Ausblenden",
|
||||
"explore_rooms": "Chats erkunden",
|
||||
"pause": "Pausieren",
|
||||
"play": "Abspielen",
|
||||
"retry": "Erneut versuchen",
|
||||
"search": "Suchen"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Wähltastatur öffnen"
|
||||
},
|
||||
"room": {
|
||||
"status_bar": {
|
||||
"delete_all": "Alle löschen",
|
||||
"exceeded_resource_limit_title": "Deine Nachricht konnte nicht versendet werden, da dein Homeserver ein Ressourcenlimit überschritten hat.",
|
||||
"failed_to_create_room_title": "Es konnte kein Chat mit diesem Nutzer gestartet werden",
|
||||
"history_visible": "Diese Gruppe wurde konfiguriert, neuen Mitgliedern Zugriff auf den vergangenen Nachrichtenverlauf zu gestatten. <a>Mehr erfahren</a>",
|
||||
"homeserver_blocked_title": "Deine Nachricht konnte nicht versendet werden, da der Admin deinen Homeserver gesperrt hat.",
|
||||
"monthly_user_limit_reached_title": "Deine Nachricht konnte nicht versendet werden, da dein Homeserver das monatliche Nutzerlimit erreicht hat.",
|
||||
"requires_consent_agreement_title": "Du kannst erst dann Nachrichten verschicken, wenn du unsere Geschäftsbedingungen gelesen und akzeptiert hast.",
|
||||
"select_messages_to_retry": "Du kannst einzelne oder alle Nachrichten erneut senden oder löschen",
|
||||
"server_connectivity_lost_description": "Nachrichten werden gespeichert und gesendet, wenn die Internetverbindung wiederhergestellt ist.",
|
||||
"server_connectivity_lost_title": "Verbindung zum Server wurde unterbrochen.",
|
||||
"some_messages_not_sent": "Einige Nachrichten konnten nicht gesendet werden"
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"tac_button": "Geschäftsbedingungen anzeigen"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "vor etwa einem Tag",
|
||||
"about_hour_ago": "vor etwa einer Stunde",
|
||||
"about_minute_ago": "vor etwa einer Minute",
|
||||
"few_seconds_ago": "vor ein paar Sekunden",
|
||||
"in_about_day": "in etwa einem Tag",
|
||||
"in_about_hour": "in etwa einer Stunde",
|
||||
"in_about_minute": "in etwa einer Minute",
|
||||
"in_few_seconds": "in ein paar Sekunden",
|
||||
"in_n_days": "in %(num)s Tagen",
|
||||
"in_n_hours": "in %(num)s Stunden",
|
||||
"in_n_minutes": "In etwa %(num)s Minuten",
|
||||
"n_days_ago": "vor %(num)s Tagen",
|
||||
"n_hours_ago": "vor %(num)s Stunden",
|
||||
"n_minutes_ago": "vor %(num)s Minuten"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Audio-Player",
|
||||
"error_downloading_audio": "Fehler beim Herunterladen der Audiodatei",
|
||||
"unnamed_audio": "Unbenannte Audiodatei"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "Διαγραφή",
|
||||
"dismiss": "Απόρριψη",
|
||||
"explore_rooms": "Εξερευνήστε αίθουσες",
|
||||
"pause": "Παύση",
|
||||
"play": "Αναπαραγωγή",
|
||||
"search": "Αναζήτηση"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Άνοιγμα πληκτρολογίου κλήσης"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "σχεδόν μία μέρα πριν",
|
||||
"about_hour_ago": "σχεδόν μία ώρα πριν",
|
||||
"about_minute_ago": "σχεδόν ένα λεπτό πριν",
|
||||
"few_seconds_ago": "λίγα δευτερόλεπτα πριν",
|
||||
"in_about_day": "περίπου μια μέρα από τώρα",
|
||||
"in_about_hour": "περίπου μία ώρα από τώρα",
|
||||
"in_about_minute": "περίπου ένα λεπτό από τώρα",
|
||||
"in_few_seconds": "λίγα δευτερόλεπτα από τώρα",
|
||||
"in_n_days": "%(num)s μέρες από τώρα",
|
||||
"in_n_hours": "%(num)s ώρες από τώρα",
|
||||
"in_n_minutes": "%(num)s λεπτά από τώρα",
|
||||
"n_days_ago": "%(num)s μέρες πριν",
|
||||
"n_hours_ago": "%(num)s ώρες πριν",
|
||||
"n_minutes_ago": "%(num)s λεπτά πριν"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "Σφάλμα λήψης ήχου",
|
||||
"unnamed_audio": "Ήχος χωρίς όνομα"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Audio seek bar"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Delete",
|
||||
"dismiss": "Dismiss",
|
||||
"explore_rooms": "Explore rooms",
|
||||
"pause": "Pause",
|
||||
"play": "Play",
|
||||
"retry": "Retry",
|
||||
"search": "Search"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Open dial pad"
|
||||
},
|
||||
"room": {
|
||||
"status_bar": {
|
||||
"delete_all": "Delete all",
|
||||
"exceeded_resource_limit_description": "Please contact your service administrator to continue using the service.",
|
||||
"exceeded_resource_limit_title": "Your message wasn't sent because this homeserver has exceeded a resource limit.",
|
||||
"failed_to_create_room_title": "Could not start a chat with this user",
|
||||
"history_visible": "This room has been configured so that new members can read history. <a>Learn More</a>",
|
||||
"homeserver_blocked_title": "Your message wasn't sent because this homeserver has been blocked by its administrator.",
|
||||
"monthly_user_limit_reached_title": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit.",
|
||||
"requires_consent_agreement_title": "You can't send any messages until you review and agree to our terms and conditions.",
|
||||
"retry_all": "Retry all",
|
||||
"select_messages_to_retry": "You can select all or individual messages to retry or delete",
|
||||
"server_connectivity_lost_description": "Sent messages will be stored until your connection has returned.",
|
||||
"server_connectivity_lost_title": "Connectivity to the server has been lost.",
|
||||
"some_messages_not_sent": "Some of your messages have not been sent"
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"tac_button": "Review terms and conditions"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "about a day ago",
|
||||
"about_hour_ago": "about an hour ago",
|
||||
"about_minute_ago": "about a minute ago",
|
||||
"few_seconds_ago": "a few seconds ago",
|
||||
"in_about_day": "about a day from now",
|
||||
"in_about_hour": "about an hour from now",
|
||||
"in_about_minute": "about a minute from now",
|
||||
"in_few_seconds": "a few seconds from now",
|
||||
"in_n_days": "%(num)s days from now",
|
||||
"in_n_hours": "%(num)s hours from now",
|
||||
"in_n_minutes": "%(num)s minutes from now",
|
||||
"n_days_ago": "%(num)s days ago",
|
||||
"n_hours_ago": "%(num)s hours ago",
|
||||
"n_minutes_ago": "%(num)s minutes ago"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Audio player",
|
||||
"error_downloading_audio": "Error downloading audio",
|
||||
"unnamed_audio": "Unnamed audio"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "Forigi",
|
||||
"dismiss": "Rezigni",
|
||||
"explore_rooms": "Esplori ĉambrojn",
|
||||
"pause": "Paŭzigi",
|
||||
"play": "Ludi",
|
||||
"search": "Serĉi"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Malfermi ciferplaton"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "antaŭ ĉirkaŭ tago",
|
||||
"about_hour_ago": "antaŭ ĉirkaŭ horo",
|
||||
"about_minute_ago": "antaŭ ĉirkaŭ minuto",
|
||||
"few_seconds_ago": "antaŭ kelkaj sekundoj",
|
||||
"in_about_day": "ĉirkaŭ tagon de nun",
|
||||
"in_about_hour": "ĉirkaŭ horon de nun",
|
||||
"in_about_minute": "ĉirkaŭ minuton de nun",
|
||||
"in_few_seconds": "kelkajn sekundojn de nun",
|
||||
"in_n_days": "%(num)s tagojn de nun",
|
||||
"in_n_hours": "%(num)s horojn de nun",
|
||||
"in_n_minutes": "%(num)s minutojn de nun",
|
||||
"n_days_ago": "antaŭ %(num)s tagoj",
|
||||
"n_hours_ago": "antaŭ %(num)s horoj",
|
||||
"n_minutes_ago": "antaŭ %(num)s minutoj"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "Eraris elŝuto de sondosiero",
|
||||
"unnamed_audio": "Sennoma sondosiero"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Barra de búsqueda de audio"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Borrar",
|
||||
"dismiss": "Omitir",
|
||||
"explore_rooms": "Explorar salas",
|
||||
"pause": "Pausar",
|
||||
"play": "Reproducir",
|
||||
"search": "Buscar"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Abrir teclado numérico"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "hace aprox. un día",
|
||||
"about_hour_ago": "hace aprox. una hora",
|
||||
"about_minute_ago": "hace aproximadamente un minuto",
|
||||
"few_seconds_ago": "hace unos segundos",
|
||||
"in_about_day": "dentro de un día",
|
||||
"in_about_hour": "dentro de una hora",
|
||||
"in_about_minute": "dentro de un minuto",
|
||||
"in_few_seconds": "dentro de unos segundos",
|
||||
"in_n_days": "dentro de %(num)s días",
|
||||
"in_n_hours": "dentro de %(num)s horas",
|
||||
"in_n_minutes": "dentro de %(num)s minutos",
|
||||
"n_days_ago": "hace %(num)s días",
|
||||
"n_hours_ago": "hace %(num)s horas",
|
||||
"n_minutes_ago": "hace %(num)s minutos"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Reproductor de audio",
|
||||
"error_downloading_audio": "Error al descargar el audio",
|
||||
"unnamed_audio": "Audio sin título"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Heli kerimisriba"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Kustuta",
|
||||
"dismiss": "Loobu",
|
||||
"explore_rooms": "Tutvu jututubadega",
|
||||
"pause": "Peata",
|
||||
"play": "Esita",
|
||||
"search": "Otsing"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Ava numbriklahvistik"
|
||||
},
|
||||
"room": {
|
||||
"status_bar": {
|
||||
"history_visible": "See jututuba on seadistatud sel viisil, et uued liikmed saavad lugeda varasemat ajalugu.<a> Lisateave</a>"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "umbes päev tagasi",
|
||||
"about_hour_ago": "umbes tund aega tagasi",
|
||||
"about_minute_ago": "umbes minut tagasi",
|
||||
"few_seconds_ago": "mõni sekund tagasi",
|
||||
"in_about_day": "umbes päeva pärast",
|
||||
"in_about_hour": "umbes tunni pärast",
|
||||
"in_about_minute": "umbes minuti pärast",
|
||||
"in_few_seconds": "mõne sekundi pärast",
|
||||
"in_n_days": "%(num)s päeva pärast",
|
||||
"in_n_hours": "%(num)s tunni pärast",
|
||||
"in_n_minutes": "%(num)s minuti pärast",
|
||||
"n_days_ago": "%(num)s päeva tagasi",
|
||||
"n_hours_ago": "%(num)s tundi tagasi",
|
||||
"n_minutes_ago": "%(num)s minutit tagasi"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Meediaesitaja",
|
||||
"error_downloading_audio": "Helifaili allalaadimine ei õnnestunud",
|
||||
"unnamed_audio": "Nimetu helifail"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "پاککردن",
|
||||
"dismiss": "نادیده بگیر",
|
||||
"explore_rooms": "جستجو در اتاق ها",
|
||||
"pause": "متوقفکردن",
|
||||
"play": "اجرا کردن",
|
||||
"search": "جستجو"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "باز کردن صفحه شمارهگیری"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "حدود یک روز قبل",
|
||||
"about_hour_ago": "حدود یک ساعت قبل",
|
||||
"about_minute_ago": "حدود یک دقیقه قبل",
|
||||
"few_seconds_ago": "چند ثانیه قبل",
|
||||
"in_about_day": "حدود یک روز دیگر",
|
||||
"in_about_hour": "حدود یک ساعت دیگر",
|
||||
"in_about_minute": "حدود یک دقیقه دیگر",
|
||||
"in_few_seconds": "چند ثانیه دیگر",
|
||||
"in_n_days": "%(num)s روز دیگر",
|
||||
"in_n_hours": "%(num)s ساعت دیگر",
|
||||
"in_n_minutes": "%(num)s دقیقه دیگر",
|
||||
"n_days_ago": "%(num)s روز قبل",
|
||||
"n_hours_ago": "%(num)s ساعت قبل",
|
||||
"n_minutes_ago": "%(num)s دقیقه قبل"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Äänen siirtymispalkki"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Poista",
|
||||
"dismiss": "Hylkää",
|
||||
"explore_rooms": "Selaa huoneita",
|
||||
"pause": "Keskeytä",
|
||||
"play": "Toista",
|
||||
"search": "Haku"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Avaa näppäimistö"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "noin päivä sitten",
|
||||
"about_hour_ago": "noin tunti sitten",
|
||||
"about_minute_ago": "noin minuutti sitten",
|
||||
"few_seconds_ago": "muutama sekunti sitten",
|
||||
"in_about_day": "noin päivä sitten",
|
||||
"in_about_hour": "noin tunti sitten",
|
||||
"in_about_minute": "noin minuutti sitten",
|
||||
"in_few_seconds": "muutama sekunti sitten",
|
||||
"in_n_days": "%(num)s päivää sitten",
|
||||
"in_n_hours": "%(num)s tuntia sitten",
|
||||
"in_n_minutes": "%(num)s minuuttia sitten",
|
||||
"n_days_ago": "%(num)s päivää sitten",
|
||||
"n_hours_ago": "%(num)s tuntia sitten",
|
||||
"n_minutes_ago": "%(num)s minuuttia sitten"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "Virhe ääntä ladattaessa",
|
||||
"unnamed_audio": "Nimetön ääni"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Barre de recherche audio"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Supprimer",
|
||||
"dismiss": "Ignorer",
|
||||
"explore_rooms": "Parcourir les salons",
|
||||
"pause": "Pause",
|
||||
"play": "Lecture",
|
||||
"retry": "Réessayer",
|
||||
"search": "Rechercher"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Ouvrir le pavé de numérotation"
|
||||
},
|
||||
"room": {
|
||||
"status_bar": {
|
||||
"delete_all": "Tout supprimer",
|
||||
"exceeded_resource_limit_description": "Veuillez contacter votre administrateur pour continuer à utiliser le service.",
|
||||
"exceeded_resource_limit_title": "Votre message n'a pas pu être envoyé car ce serveur d'accueil a dépassé sa limite de ressources.",
|
||||
"failed_to_create_room_title": "Impossible de démarrer une discussion avec cet utilisateur",
|
||||
"history_visible": "Ce salon a été configuré afin que les nouveaux membres puissent lire l'historique.<a> En savori plus</a>",
|
||||
"homeserver_blocked_title": "Votre message n'a pas pu être envoyé car ce serveur d'accueil a été bloqué par son administrateur.",
|
||||
"monthly_user_limit_reached_title": "Votre message n'a pas pu être envoyé car ce serveur d'accueil a atteint sa limite mensuelle d'utilisateurs actifs.",
|
||||
"requires_consent_agreement_title": "Vous ne pouvez envoyer aucun message tant que vous n'aurez pas consulté et accepté nos conditions générales.",
|
||||
"retry_all": "Tout réessayer",
|
||||
"select_messages_to_retry": "Vous pouvez choisir de renvoyer ou supprimer tous les messages ou seulement certains",
|
||||
"server_connectivity_lost_description": "Les messages envoyés seront stockés jusqu’à ce que votre connexion revienne.",
|
||||
"server_connectivity_lost_title": "La connexion avec le serveur a été perdue.",
|
||||
"some_messages_not_sent": "Certains de vos messages n’ont pas été envoyés"
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"tac_button": "Voir les conditions générales"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "il y a environ un jour",
|
||||
"about_hour_ago": "il y a environ une heure",
|
||||
"about_minute_ago": "il y a environ une minute",
|
||||
"few_seconds_ago": "il y a quelques secondes",
|
||||
"in_about_day": "dans un jour environ",
|
||||
"in_about_hour": "dans une heure environ",
|
||||
"in_about_minute": "dans une minute environ",
|
||||
"in_few_seconds": "dans quelques secondes",
|
||||
"in_n_days": "dans %(num)s jours",
|
||||
"in_n_hours": "dans %(num)s heures",
|
||||
"in_n_minutes": "dans %(num)s minutes",
|
||||
"n_days_ago": "il y a %(num)s jours",
|
||||
"n_hours_ago": "il y a %(num)s heures",
|
||||
"n_minutes_ago": "il y a %(num)s minutes"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Lecteur audio",
|
||||
"error_downloading_audio": "Erreur lors du téléchargement de l’audio",
|
||||
"unnamed_audio": "Audio sans nom"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "Eliminar",
|
||||
"dismiss": "Rexeitar",
|
||||
"explore_rooms": "Explorar salas",
|
||||
"pause": "Deter",
|
||||
"play": "Reproducir",
|
||||
"search": "Busca"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Abrir marcador"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "onte",
|
||||
"about_hour_ago": "fai unha hora",
|
||||
"about_minute_ago": "fai un minuto",
|
||||
"few_seconds_ago": "fai uns segundos",
|
||||
"in_about_day": "foi onte",
|
||||
"in_about_hour": "fará unha hora",
|
||||
"in_about_minute": "haberá un minuto",
|
||||
"in_few_seconds": "hai só uns segundos",
|
||||
"in_n_days": "fará %(num)s días",
|
||||
"in_n_hours": "fará %(num)s horas",
|
||||
"in_n_minutes": "fará %(num)s minutos",
|
||||
"n_days_ago": "fai %(num)s días",
|
||||
"n_hours_ago": "fai %(num)s horas",
|
||||
"n_minutes_ago": "fai %(num)s minutos"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "Erro ao descargar o audio",
|
||||
"unnamed_audio": "Audio sen nome"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "מחק",
|
||||
"dismiss": "התעלם",
|
||||
"explore_rooms": "גלה חדרים",
|
||||
"search": "חפש"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "פתח לוח חיוג"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "בערך לפני יום",
|
||||
"about_hour_ago": "בערך לפני כשעה",
|
||||
"about_minute_ago": "לפני בערך דקה",
|
||||
"few_seconds_ago": "לפני מספר שניות",
|
||||
"in_about_day": "בערך בעוד יום מעכשיו",
|
||||
"in_about_hour": "בערך בעוד כשעה",
|
||||
"in_about_minute": "בערך עוד דקה אחת",
|
||||
"in_few_seconds": "בעוד מספר שניות מעכשיו",
|
||||
"in_n_days": "בעוד %(num)s ימים מעכשיו",
|
||||
"in_n_hours": "בעוד %(num)s שעות",
|
||||
"in_n_minutes": "בעוד %(num)s דקות",
|
||||
"n_days_ago": "לפני %(num)s ימים",
|
||||
"n_hours_ago": "לפני %(num)s שעות",
|
||||
"n_minutes_ago": "לפני %(num)s דקות"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Hang keresősávja"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Törlés",
|
||||
"dismiss": "Eltüntetés",
|
||||
"explore_rooms": "Szobák felderítése",
|
||||
"pause": "Szünet",
|
||||
"play": "Lejátszás",
|
||||
"search": "Keresés"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Számlap megnyitása"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "egy napja",
|
||||
"about_hour_ago": "egy órája",
|
||||
"about_minute_ago": "egy perce",
|
||||
"few_seconds_ago": "néhány másodperce",
|
||||
"in_about_day": "egy nap múlva",
|
||||
"in_about_hour": "egy óra múlva",
|
||||
"in_about_minute": "egy perc múlva",
|
||||
"in_few_seconds": "másodpercek múlva",
|
||||
"in_n_days": "%(num)s nap múlva",
|
||||
"in_n_hours": "%(num)s óra múlva",
|
||||
"in_n_minutes": "%(num)s perc múlva",
|
||||
"n_days_ago": "%(num)s nappal ezelőtt",
|
||||
"n_hours_ago": "%(num)s órával ezelőtt",
|
||||
"n_minutes_ago": "%(num)s perccel ezelőtt"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Hanglejátszó",
|
||||
"error_downloading_audio": "Hiba a hang letöltésekor",
|
||||
"unnamed_audio": "Névtelen hang"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Աուդիո որոնման գոտի"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Ջնջել",
|
||||
"dismiss": "Հեռացնել",
|
||||
"explore_rooms": "Փնտրել սենյակներ",
|
||||
"pause": "Դադար",
|
||||
"play": "Միացնել",
|
||||
"search": "Որոնել"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Բացեք թվերի հավաքման վահանակը"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "մոտ մեկ օր առաջ",
|
||||
"about_hour_ago": "մոտ մեկ ժամ առաջ",
|
||||
"about_minute_ago": "մոտ մեկ րոպե առաջ",
|
||||
"few_seconds_ago": "մի քանի վայրկյան առաջ",
|
||||
"in_about_day": "մոտ մեկ օր անց",
|
||||
"in_about_hour": "մոտ մեկ ժամ անց",
|
||||
"in_about_minute": "մոտ մեկ րոպե անց",
|
||||
"in_few_seconds": "մի քանի վայրկյան անց",
|
||||
"in_n_days": "%(num)s օր անց",
|
||||
"in_n_hours": "%(num)s ժամ անց",
|
||||
"in_n_minutes": "%(num)s րոպեներ անց",
|
||||
"n_days_ago": "%(num)s օր առաջ",
|
||||
"n_hours_ago": "%(num)s ժամ առաջ",
|
||||
"n_minutes_ago": "%(num)s րոպե առաջ"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Աուդիո նվագարկիչ",
|
||||
"error_downloading_audio": "Աուդիո ներբեռնման սխալ",
|
||||
"unnamed_audio": "Անանուն աուդիո"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Bilah pencarian audio"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Hapus",
|
||||
"dismiss": "Abaikan",
|
||||
"explore_rooms": "Jelajahi ruangan",
|
||||
"pause": "Jeda",
|
||||
"play": "Mainkan",
|
||||
"search": "Cari"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Buka tombol penyetel"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "1 hari yang lalu",
|
||||
"about_hour_ago": "1 jam yang lalu",
|
||||
"about_minute_ago": "1 menit yang lalu",
|
||||
"few_seconds_ago": "beberapa detik yang lalu",
|
||||
"in_about_day": "1 hari dari sekarang",
|
||||
"in_about_hour": "1 jam dari sekarang",
|
||||
"in_about_minute": "1 menit dari sekarang",
|
||||
"in_few_seconds": "beberapa detik dari sekarang",
|
||||
"in_n_days": "%(num)s hari dari sekarang",
|
||||
"in_n_hours": "%(num)s jam dari sekarang",
|
||||
"in_n_minutes": "%(num)s dari sekarang",
|
||||
"n_days_ago": "%(num)s hari yang lalu",
|
||||
"n_hours_ago": "%(num)s jam yang lalu",
|
||||
"n_minutes_ago": "%(num)s menit yang lalu"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"audio_player": "Pemutar audio",
|
||||
"error_downloading_audio": "Terjadi kesalahan mengunduh audio",
|
||||
"unnamed_audio": "Audio tidak dinamai"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "Eyða",
|
||||
"dismiss": "Hunsa",
|
||||
"explore_rooms": "Kanna spjallrásir",
|
||||
"pause": "Bið",
|
||||
"play": "Spila",
|
||||
"search": "Leita"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Opna talnaborð"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "fyrir um degi síðan",
|
||||
"about_hour_ago": "fyrir um klukkustund síðan",
|
||||
"about_minute_ago": "fyrir um það bil mínútu síðan",
|
||||
"few_seconds_ago": "fyrir örfáum sekúndum síðan",
|
||||
"in_about_day": "eftir um það bil einn dag",
|
||||
"in_about_hour": "eftir um það bil klukkustund",
|
||||
"in_about_minute": "eftir um það bil mínútu",
|
||||
"in_few_seconds": "eftir nokkrar sekúndur",
|
||||
"in_n_days": "eftir %(num)s daga",
|
||||
"in_n_hours": "eftir %(num)s klukkustundir",
|
||||
"in_n_minutes": "eftir %(num)s mínútur",
|
||||
"n_days_ago": "fyrir %(num)s dögum síðan",
|
||||
"n_hours_ago": "fyrir %(num)s klukkustundum síðan",
|
||||
"n_minutes_ago": "fyrir %(num)s mínútum síðan"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "Villa við að sækja hljóð",
|
||||
"unnamed_audio": "Nafnlaust hljóð"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"a11y": {
|
||||
"seek_bar_label": "Barra di ricerca audio"
|
||||
},
|
||||
"action": {
|
||||
"delete": "Elimina",
|
||||
"dismiss": "Chiudi",
|
||||
"explore_rooms": "Esplora stanze",
|
||||
"pause": "Pausa",
|
||||
"play": "Riproduci",
|
||||
"search": "Cerca"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "Apri tastierino"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "circa un giorno fa",
|
||||
"about_hour_ago": "circa un'ora fa",
|
||||
"about_minute_ago": "circa un minuto fa",
|
||||
"few_seconds_ago": "pochi secondi fa",
|
||||
"in_about_day": "circa un giorno da adesso",
|
||||
"in_about_hour": "circa un'ora da adesso",
|
||||
"in_about_minute": "circa un minuto da adesso",
|
||||
"in_few_seconds": "pochi secondi da adesso",
|
||||
"in_n_days": "%(num)s giorni da adesso",
|
||||
"in_n_hours": "%(num)s ore da adesso",
|
||||
"in_n_minutes": "%(num)s minuti da adesso",
|
||||
"n_days_ago": "%(num)s giorni fa",
|
||||
"n_hours_ago": "%(num)s ore fa",
|
||||
"n_minutes_ago": "%(num)s minuti fa"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "Errore di scaricamento dell'audio",
|
||||
"unnamed_audio": "Audio senza nome"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "削除",
|
||||
"dismiss": "閉じる",
|
||||
"explore_rooms": "ルームを探す",
|
||||
"pause": "一時停止",
|
||||
"play": "再生",
|
||||
"search": "検索"
|
||||
},
|
||||
"left_panel": {
|
||||
"open_dial_pad": "ダイヤルパッドを開く"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "約1日前",
|
||||
"about_hour_ago": "約1時間前",
|
||||
"about_minute_ago": "約1分前",
|
||||
"few_seconds_ago": "数秒前",
|
||||
"in_about_day": "今から約1日前",
|
||||
"in_about_hour": "今から約1時間前",
|
||||
"in_about_minute": "今から約1分前",
|
||||
"in_few_seconds": "今から数秒前",
|
||||
"in_n_days": "今から%(num)s日前",
|
||||
"in_n_hours": "今から%(num)s時間前",
|
||||
"in_n_minutes": "今から%(num)s分前",
|
||||
"n_days_ago": "%(num)s日前",
|
||||
"n_hours_ago": "%(num)s時間前",
|
||||
"n_minutes_ago": "%(num)s分前"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "音声をダウンロードする際にエラーが発生しました",
|
||||
"unnamed_audio": "名前のない音声"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"action": {
|
||||
"delete": "წაშლა",
|
||||
"dismiss": "დახურვა",
|
||||
"explore_rooms": "ოთახების დათავლიერება",
|
||||
"pause": "პაუზა",
|
||||
"play": "დაკვრა",
|
||||
"search": "ძიება"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "დაახლოებით ერთი დღის წინ",
|
||||
"about_hour_ago": "დაახლოებით ერთი საათის წინ",
|
||||
"about_minute_ago": "დაახლოებით ერთი წუთის წინ",
|
||||
"few_seconds_ago": "რამდენიმე წამის წინ",
|
||||
"in_about_day": "დაახლოებით ერთი დღის შემდეგ",
|
||||
"in_about_hour": "დაახლოებით ერთი საათის შემდეგ",
|
||||
"in_about_minute": "დაახლოებით ერთი წუთის შემდეგ",
|
||||
"in_few_seconds": "რამდენიმე წამის შემდეგ",
|
||||
"in_n_days": "%(num)sდღეებიდან",
|
||||
"in_n_hours": "%(num)sსაათის შემდეგ",
|
||||
"in_n_minutes": "%(num)sწუთის შემდეგ",
|
||||
"n_days_ago": "%(num)sდღის წინ",
|
||||
"n_hours_ago": "%(num)sსაათის წინ",
|
||||
"n_minutes_ago": "%(num)sწუთის წინ"
|
||||
},
|
||||
"timeline": {
|
||||
"m.audio": {
|
||||
"error_downloading_audio": "შეცდომა აუდიოს ჩამოტვირთვისას",
|
||||
"unnamed_audio": "უსახელო აუდიო"
|
||||
}
|
||||
}
|
||||
}
|
||||