Compare commits

..

3 Commits

Author SHA1 Message Date
David Baker
435975e8bc Move tests 2025-10-15 18:37:49 +01:00
David Baker
ade2de5971 Also add the file 2025-10-15 18:27:55 +01:00
David Baker
5aec76e604 Move some message utils out to their own file
In another attempt at import cycle breaking
2025-10-15 18:25:48 +01:00
1149 changed files with 16975 additions and 31908 deletions

View File

@@ -1,16 +1,8 @@
/*
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.
*/
module.exports = {
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
},
env: {
browser: true,
@@ -168,10 +160,6 @@ module.exports = {
group: ["@vector-im/compound-design-tokens/icons/*"],
message: "Please use @vector-im/compound-design-tokens/assets/web/icons/* instead",
},
{
group: ["**/packages/shared-components/**", "../packages/shared-components/**"],
message: "Please use @element-hq/web-shared-components",
},
],
},
],

3
.github/labels.yml vendored
View File

@@ -279,6 +279,3 @@
- name: "Z-Flaky-Test-Disabled"
description: "The flaking test has been disabled"
color: "ededed"
- name: "Z-Skip-Coverage"
description: "Skip SonarQube coverage for this PR"
color: "ededed"

View File

@@ -42,7 +42,7 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp-${{ matrix.image }}
path: webapp

View File

@@ -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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: element-web.deb
path: element-web.deb

View File

@@ -26,7 +26,7 @@ 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp
path: dist/develop.tar.gz

View File

@@ -34,7 +34,7 @@ jobs:
env:
SITE: ${{ inputs.site || 'staging.element.io' }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Load GPG key
run: |

View File

@@ -20,7 +20,7 @@ jobs:
env:
TEST_TAG: vectorim/element-web:test
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
@@ -29,7 +29,7 @@ jobs:
if: github.event_name != 'pull_request'
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
@@ -96,7 +96,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
if: github.event_name != 'pull_request'
with:
images: |
@@ -141,7 +141,7 @@ jobs:
repository: vectorim/element-web
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
if: github.event_name != 'pull_request'
with:
repository: element-hq/element-web-pro

View File

@@ -17,18 +17,18 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Fetch element-desktop
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: element-hq/element-desktop
path: element-desktop
- name: Fetch element-web
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
path: element-web
- name: Fetch matrix-js-sdk
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
@@ -43,13 +43,13 @@ jobs:
working-directory: element-web
run: |
yarn install --frozen-lockfile
yarn node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
yarn ts-node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
- name: Setup mdBook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
with:
mdbook-version: "0.5.1"
mdbook-version: "0.4.10"
- name: Install mdbook extensions
run: cargo install mdbook-combiner mdbook-mermaid

View File

@@ -25,7 +25,7 @@ jobs:
actions: read
steps:
- name: Download HTML report
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -50,7 +50,7 @@ jobs:
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: element-hq/element-web
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp
path: webapp
@@ -122,13 +122,13 @@ jobs:
- runAllTests: false
project: Pinecone
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
repository: element-hq/element-web
- name: 📥 Download artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: webapp
path: webapp
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
path: blob-report
@@ -194,7 +194,7 @@ jobs:
if: always()
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
if: inputs.skip != true
with:
persist-credentials: false
@@ -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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: html-report
path: playwright-report

View File

@@ -28,7 +28,7 @@ jobs:
Exercise caution. Use test accounts.
- name: 📥 Download artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- 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@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/playwright-image-updates

View File

@@ -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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
if: inputs.matrix-js-sdk
with:
repository: matrix-org/matrix-js-sdk

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: 🧮 Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: 🔧 Set up node environment
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
@@ -21,14 +21,8 @@ jobs:
cache: "yarn"
node-version-file: ".node-version"
registry-url: "https://registry.npmjs.org"
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
# Need to setup element web too as it needs the translations
- name: 🛠️ Setup EW
run: yarn install --pure-lockfile
env:
NODE_AUTH_TOKEN: ${{ secrets.ELEMENT_NPM_TOKEN }}
- name: 🛠️ Setup
# When running `install` it also calls the `prepare` step which generates
@@ -37,4 +31,4 @@ jobs:
- name: 🚀 Publish to npm
working-directory: packages/shared-components
run: npm publish --access public --tag test --provenance
run: npm publish --access public --provenance

View File

@@ -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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -21,7 +21,7 @@ jobs:
issues: read
pull-requests: read
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
repository: element-hq/element-web
@@ -34,6 +34,10 @@ jobs:
- name: Install element web dependencies
run: yarn install --frozen-lockfile
- name: Build Element Web resources
# Needed to prepare language files
run: "yarn build:res"
- name: Install dependencies
working-directory: packages/shared-components
run: yarn install --frozen-lockfile
@@ -55,12 +59,18 @@ jobs:
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: "yarn playwright install --with-deps --only-shell"
- name: Build storybook dependencies
# When the first test is ran, it will fail because the dependencies are not yet built.
# This step is to ensure that the dependencies are built before running the tests.
run: "yarn --cwd packages/shared-components test:storybook:ci"
continue-on-error: true
- name: Run Visual tests
run: "yarn --cwd packages/shared-components test:storybook:ci"
- name: Upload received images & diffs
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: received-images
path: packages/shared-components/playwright/shared-component-received

View File

@@ -22,7 +22,7 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -35,6 +35,10 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
- name: Build Element Web resources
# Needed to prepare language files for shared components
run: "yarn build:res"
- name: Install Shared Component Dependencies
run: "yarn --cwd packages/shared-components install"
@@ -63,7 +67,7 @@ jobs:
name: "Rethemendex Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- run: ./res/css/rethemendex.sh
@@ -73,7 +77,7 @@ jobs:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -87,6 +91,10 @@ jobs:
- name: Run Linter
run: "yarn run lint:js"
- name: Build Element Web resources
# Needed to prepare language files for shared components
run: "yarn build:res"
- name: Install Shared Component Deps
run: "yarn --cwd packages/shared-components install --frozen-lockfile"
@@ -97,7 +105,7 @@ jobs:
name: "Style Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -115,7 +123,7 @@ jobs:
name: "Workflow Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -133,7 +141,7 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:

View File

@@ -29,8 +29,8 @@ env:
permissions: {}
jobs:
jest_ew:
name: Jest (Element Web)
jest:
name: Jest
runs-on: ubuntu-24.04
strategy:
fail-fast: false
@@ -39,7 +39,7 @@ jobs:
runner: [1, 2]
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
@@ -84,7 +84,7 @@ jobs:
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-${{ matrix.runner }}
path: |
@@ -93,13 +93,13 @@ jobs:
complete:
name: jest-tests
needs: jest_ew
needs: jest
if: always()
runs-on: ubuntu-24.04
permissions:
statuses: write
steps:
- if: needs.jest_ew.result != 'skipped' && needs.jest_ew.result != 'success'
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1
- name: Skip SonarCloud in merge queue
@@ -112,56 +112,3 @@ jobs:
context: SonarCloud Code Analysis
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
jest_sc:
name: Jest (Shared Components)
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
- name: Yarn cache
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
node-version: "lts/*"
cache: "yarn"
- name: Install EW Deps
run: "yarn install"
- name: Install Shared Component Deps
working-directory: "packages/shared-components"
run: "yarn install"
- name: Jest Cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: /tmp/jest_cache
key: ${{ hashFiles('**/yarn.lock') }}
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
- name: Run tests
working-directory: "packages/shared-components"
run: |
yarn test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--ci \
--max-workers ${{ steps.cpu-cores.outputs.count }} \
--cacheDirectory /tmp/jest_cache
env:
# tell jest to use coloured output
FORCE_COLOR: true
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: coverage-sharedcomponents
path: |
packages/shared-components/coverage
!packages/shared-components/coverage/lcov-report

View File

@@ -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@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.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@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.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@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.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@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
id: add_to_project
if: steps.any_matching_reviewers.outputs.match == 'true'
with:

View File

@@ -9,7 +9,7 @@ jobs:
update:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
@@ -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@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/jitsi-update

View File

@@ -26,13 +26,13 @@ jobs:
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
PUBLIC_DISCUSSION_ROOM_ID: "!xUW4PpAe1CmThA3r2wI8IrgwwsK006-zqWdJCljpd10"
ANNOUNCEMENT_ROOM_ID: "!ars5ndgI6IIYZXECiJ-u8YljHNzShJn3nHdB-3rYI2M"
PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org"
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
with:
script: |
const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_DISCUSSION_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
const repo = context.repo;
const { data } = await github.rest.repos.getLatestRelease({
@@ -71,23 +71,18 @@ jobs:
const data = await res.json();
console.log(roomId, "got event", data);
if (!regex.test(data.topic)) {
core.setFailed("Topic format is incorrect for room " + roomId);
return;
}
const topic = data.topic.replace(regex, releaseTopic);
if (topic === data.topic) {
console.log(roomId, "nothing to do");
return;
}
if (data["org.matrix.msc3765.topic"]) {
data["org.matrix.msc3765.topic"]?.["m.text"].forEach(d => {
data["org.matrix.msc3765.topic"].forEach(d => {
d.body = d.body.replace(regex, releaseTopic);
});
}
if (data["m.topic"]) {
data["m.topic"]?.["m.text"].forEach(d => {
data["m.topic"].forEach(d => {
d.body = d.body.replace(regex, releaseTopic);
});
}
@@ -102,18 +97,12 @@ jobs:
});
if (res.ok) {
const resJson = res.json();
if (resJson.errcode) {
core.setFailed(`Error updating ${roomId}: ${resJson.error}`);
} else {
console.log(roomId, "topic updated:", topic);
}
console.log(roomId, "topic updated:", topic);
} else {
const errText = await res.text();
core.setFailed(`Error updating ${roomId}: ${errText}`);
console.log(roomId, await res.text());
}
}
await updateReleaseInTopic(LOBBY_ROOM_ID);
await updateReleaseInTopic(PUBLIC_DISCUSSION_ROOM_ID);
await updateReleaseInTopic(PUBLIC_ROOM_ID);
await updateReleaseInTopic(ANNOUNCEMENT_ROOM_ID);

1
.gitignore vendored
View File

@@ -36,4 +36,3 @@ storybook-static
/packages/shared-components/node_modules
/packages/shared-components/dist
/packages/shared-components/src/i18nKeys.d.ts

View File

@@ -1 +1 @@
24
22

View File

@@ -2,6 +2,7 @@
/dist
/lib
/node_modules
/packages/
/webapp
/*.log
yarn.lock

View File

@@ -1,118 +1,3 @@
Changes in [1.12.6](https://github.com/element-hq/element-web/releases/tag/v1.12.6) (2025-12-03)
================================================================================================
This release fixes a bug where 1:1 calling was incorrectly not available if no Element Call focus was set.
## 🐛 Bug Fixes
* Add option to pick call options for voice calls. ([#31413](https://github.com/element-hq/element-web/pull/31413)).
Changes in [1.12.5](https://github.com/element-hq/element-web/releases/tag/v1.12.5) (2025-12-02)
================================================================================================
## ✨ Features
* Update Emojibase to v17 ([#31307](https://github.com/element-hq/element-web/pull/31307)). Contributed by @t3chguy.
* Adds tooltip for compose menu ([#31122](https://github.com/element-hq/element-web/pull/31122)). Contributed by @byteplow.
* Add option to hide pinned message banner in room view ([#31296](https://github.com/element-hq/element-web/pull/31296)). Contributed by @florianduros.
* update twemoji to not monochromise emoji with BLACK in their name ([#31281](https://github.com/element-hq/element-web/pull/31281)). Contributed by @ara4n.
* upgrade to twemoji 17.0.2 and fix #14695 ([#31267](https://github.com/element-hq/element-web/pull/31267)). Contributed by @ara4n.
* Add options to hide right panel in room view ([#31252](https://github.com/element-hq/element-web/pull/31252)). Contributed by @florianduros.
* Delayed event management: split endpoints, no auth ([#31183](https://github.com/element-hq/element-web/pull/31183)). Contributed by @AndrewFerr.
* Support using Element Call for voice calls in DMs ([#30817](https://github.com/element-hq/element-web/pull/30817)). Contributed by @Half-Shot.
* Improve screen reader accessibility of auth pages ([#31236](https://github.com/element-hq/element-web/pull/31236)). Contributed by @t3chguy.
* Add posthog tracking for key backup toasts ([#31195](https://github.com/element-hq/element-web/pull/31195)). Contributed by @Half-Shot.
## 🐛 Bug Fixes
* Return to using Fira Code as the default monospace font ([#31302](https://github.com/element-hq/element-web/pull/31302)). Contributed by @ara4n.
* Fix case of home screen being displayed erroneously ([#31301](https://github.com/element-hq/element-web/pull/31301)). Contributed by @langleyd.
* Fix message edition and reply when multiple rooms at displayed the same moment ([#31280](https://github.com/element-hq/element-web/pull/31280)). Contributed by @florianduros.
* Key storage out of sync: reset key backup when needed ([#31279](https://github.com/element-hq/element-web/pull/31279)). Contributed by @uhoreg.
* Fix invalid events crashing entire room rather than just their tile ([#31256](https://github.com/element-hq/element-web/pull/31256)). Contributed by @t3chguy.
* Fix expand button of space panel getting cut off at the edges ([#31259](https://github.com/element-hq/element-web/pull/31259)). Contributed by @MidhunSureshR.
* Fix pill buttons in dialogs ([#31246](https://github.com/element-hq/element-web/pull/31246)). Contributed by @dbkr.
* Fix blank sections at the top and bottom of the member list when scrolling ([#31198](https://github.com/element-hq/element-web/pull/31198)). Contributed by @langleyd.
* Fix emoji category selection with keyboard ([#31162](https://github.com/element-hq/element-web/pull/31162)). Contributed by @langleyd.
Changes in [1.12.4](https://github.com/element-hq/element-web/releases/tag/v1.12.4) (2025-11-18)
================================================================================================
## ✨ Features
* Apply aria-hidden to emoji in SAS verification ([#31204](https://github.com/element-hq/element-web/pull/31204)). Contributed by @t3chguy.
* Add options to hide header and composer of room view for the module api ([#31095](https://github.com/element-hq/element-web/pull/31095)). Contributed by @florianduros.
* Experimental Module API Additions ([#30863](https://github.com/element-hq/element-web/pull/30863)). Contributed by @dbkr.
* Change polls to use fieldset/legend markup ([#31160](https://github.com/element-hq/element-web/pull/31160)). Contributed by @langleyd.
* Use compound Button styles for Jitsi button ([#31159](https://github.com/element-hq/element-web/pull/31159)). Contributed by @Half-Shot.
* Add FocusLock to emoji picker ([#31146](https://github.com/element-hq/element-web/pull/31146)). Contributed by @langleyd.
* Move room name, avatar, and topic to IOpts. ([#30981](https://github.com/element-hq/element-web/pull/30981)). Contributed by @kaylendog.
* Add a devtool for looking at users and their devices ([#30983](https://github.com/element-hq/element-web/pull/30983)). Contributed by @uhoreg.
## 🐛 Bug Fixes
* Fix room list handling of membership changes ([#31197](https://github.com/element-hq/element-web/pull/31197)). Contributed by @t3chguy.
* Fix room list unable to be resized when displayed after a module ([#31186](https://github.com/element-hq/element-web/pull/31186)). Contributed by @florianduros.
* Inhibit keyboard highlights in dialogs when effector is not in focus ([#31181](https://github.com/element-hq/element-web/pull/31181)). Contributed by @t3chguy.
* Strip mentions from forwarded messages ([#30884](https://github.com/element-hq/element-web/pull/30884)). Contributed by @twassman.
* Don't allow pin or edit of messages with a send status ([#31158](https://github.com/element-hq/element-web/pull/31158)). Contributed by @langleyd.
* Hide room header buttons if the room hasn't been created yet. ([#31092](https://github.com/element-hq/element-web/pull/31092)). Contributed by @Half-Shot.
* Fix screen readers not indicating the emoji picker search field is focused. ([#31128](https://github.com/element-hq/element-web/pull/31128)). Contributed by @langleyd.
* Fix emoji picker highlight missing when not active element ([#31148](https://github.com/element-hq/element-web/pull/31148)). Contributed by @t3chguy.
* Add relevant aria attribute for selected emoji in the emoji picker ([#31125](https://github.com/element-hq/element-web/pull/31125)). Contributed by @t3chguy.
* Fix tooltips within context menu portals being unreliable ([#31129](https://github.com/element-hq/element-web/pull/31129)). Contributed by @t3chguy.
* Avoid excessive re-render of room list and member list ([#31131](https://github.com/element-hq/element-web/pull/31131)). Contributed by @florianduros.
* Make emoji picker height responsive. ([#31130](https://github.com/element-hq/element-web/pull/31130)). Contributed by @langleyd.
* Emoji Picker: Focused emoji does not move with the arrow keys ([#30893](https://github.com/element-hq/element-web/pull/30893)). Contributed by @langleyd.
* Fix audio player seek bar position ([#31127](https://github.com/element-hq/element-web/pull/31127)). Contributed by @florianduros.
* Add aria label to emoji picker search ([#31126](https://github.com/element-hq/element-web/pull/31126)). Contributed by @langleyd.
Changes in [1.12.3](https://github.com/element-hq/element-web/releases/tag/v1.12.3) (2025-11-04)
================================================================================================
## 🦖 Deprecations
* Remove allowVoipWithNoMedia feature flag ([#31087](https://github.com/element-hq/element-web/pull/31087)). Contributed by @Half-Shot.
## ✨ Features
* Change module API to be an instance getter ([#31025](https://github.com/element-hq/element-web/pull/31025)). Contributed by @dbkr.
## 🐛 Bug Fixes
* Show hover elements when keyboard focus is within an event tile ([#31078](https://github.com/element-hq/element-web/pull/31078)). Contributed by @t3chguy.
* Ensure toolbar navigation pattern works in MessageActionBar ([#31080](https://github.com/element-hq/element-web/pull/31080)). Contributed by @t3chguy.
* Ensure sent markers are hidden when showing thread summary. ([#31076](https://github.com/element-hq/element-web/pull/31076)). Contributed by @Half-Shot.
* Fix translation in dev mode ([#31045](https://github.com/element-hq/element-web/pull/31045)). Contributed by @florianduros.
* Fix sort order in space hierarchy ([#30975](https://github.com/element-hq/element-web/pull/30975)). Contributed by @t3chguy.
* New Room list: don't display message preview of thread ([#31043](https://github.com/element-hq/element-web/pull/31043)). Contributed by @florianduros.
* Revert "A11y: move focus to right panel when opened" ([#30999](https://github.com/element-hq/element-web/pull/30999)). Contributed by @florianduros.
* Fix highlights in messages (or search results) breaking links ([#30264](https://github.com/element-hq/element-web/pull/30264)). Contributed by @bojidar-bg.
* Add prepare script ([#31030](https://github.com/element-hq/element-web/pull/31030)). Contributed by @dbkr.
* Fix html exports by adding SDKContext ([#30987](https://github.com/element-hq/element-web/pull/30987)). Contributed by @t3chguy.
Changes in [1.12.2](https://github.com/element-hq/element-web/releases/tag/v1.12.2) (2025-10-21)
================================================================================================
## ✨ Features
* Room List: Extend the viewport to avoid so many black spots when scrolling the room list ([#30867](https://github.com/element-hq/element-web/pull/30867)). Contributed by @langleyd.
* Hide calling buttons in room header before a room is created ([#30816](https://github.com/element-hq/element-web/pull/30816)). Contributed by @Half-Shot.
* Improve invite dialog ui - Part 2 ([#30836](https://github.com/element-hq/element-web/pull/30836)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix platform settings race condition and make auto-launch tri-state ([#30977](https://github.com/element-hq/element-web/pull/30977)). Contributed by @t3chguy.
* Fix: member count in header and member list ([#30982](https://github.com/element-hq/element-web/pull/30982)). Contributed by @florianduros.
* Fix duration of voice message in timeline ([#30973](https://github.com/element-hq/element-web/pull/30973)). Contributed by @florianduros.
* Fix voice notes rendering at 00:00 when playback had not begun. ([#30961](https://github.com/element-hq/element-web/pull/30961)). Contributed by @Half-Shot.
* Improve handling of animated images, add support for AVIF animations ([#30932](https://github.com/element-hq/element-web/pull/30932)). Contributed by @t3chguy.
* Update key storage toggle when key storage status changes ([#30934](https://github.com/element-hq/element-web/pull/30934)). Contributed by @uhoreg.
* Fix jitsi widget popout ([#30908](https://github.com/element-hq/element-web/pull/30908)). Contributed by @dbkr.
* Improve keyboard navigation on invite dialog ([#30930](https://github.com/element-hq/element-web/pull/30930)). Contributed by @florianduros.
* Prefer UIA flows with supported UIA stages ([#30926](https://github.com/element-hq/element-web/pull/30926)). Contributed by @richvdh.
* Enhance accessibility of dropdown ([#30928](https://github.com/element-hq/element-web/pull/30928)). Contributed by @florianduros.
* Improve accessibility of the `\<AvatarSetting> component ([#30907](https://github.com/element-hq/element-web/pull/30907)). Contributed by @MidhunSureshR.
Changes in [1.12.1](https://github.com/element-hq/element-web/releases/tag/v1.12.1) (2025-10-07)
================================================================================================
## ✨ Features

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.20-labs@sha256:dbcde2ebc4abc8bb5c3c499b9c9a6876842bf5da243951cd2697f921a7aeb6a9
# syntax=docker.io/docker/dockerfile:1.19-labs@sha256:dce1c693ef318bca08c964ba3122ae6248e45a1b96d65c4563c8dc6fe80349a2
# Builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:b36a1eab6bdeb43cf4808370d18b6706452e810e3563b1ce669d2965af3c0464 AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:cd951bb228ddf85882951d4bf4acefc5314eb2eb66eee002256f4bb17c2293e3 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:304079937327a6490d5c89df47c8951d76f05b346d4c6e3b10cba2e266cd4904
# Need root user to install packages & manipulate the usr directory
USER root

View File

@@ -4,6 +4,7 @@
title = "Element Web & Desktop"
authors = ["New Vector Ltd.", "The Matrix.org Foundation C.I.C."]
language = "en"
multilingual = false
# The directory that documentation files are stored in
src = "docs"

View File

@@ -79,7 +79,7 @@ Unless otherwise specified, the following applies to all code:
11. If a variable is not receiving a value on declaration, its type must be defined.
```typescript
let errorMessage: string;
let errorMessage: Optional<string>;
```
12. Objects can use shorthand declarations, including mixing of types.

View File

@@ -407,7 +407,7 @@ The VoIP and Jitsi options are:
If you run your own rageshake server to collect bug reports, the following options may be of interest:
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
not present in the config, the app will disable all rageshake functionality. Set to `https://element.io/bugreports/submit` to submit
rageshakes to us, or use your own rageshake server.
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`

View File

@@ -11,7 +11,7 @@ There are some exceptions like when using localhost, which is considered a [secu
1. Download the latest version from <https://github.com/element-hq/element-web/releases>
1. Untar the tarball on your web server
1. Move (or symlink) the `element-x.x.x` directory to an appropriate name
1. Configure the correct caching headers in your webserver (see [README.md](../README.md#caching-requirements))
1. Configure the correct caching headers in your webserver (see below)
1. Configure the app by copying `config.sample.json` to `config.json` and
modifying it. See the [configuration docs](config.md) for details.
1. Enter the URL into your browser and log into Element!

View File

@@ -57,7 +57,7 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"defaultCountryCode": "GB",
"show_labs_settings": false,
"features": { },

View File

@@ -17,7 +17,7 @@
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"show_labs_settings": false,
"room_directory": {

View File

@@ -17,7 +17,7 @@
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"show_labs_settings": true,
"room_directory": {

View File

@@ -17,7 +17,7 @@ const config: Config = {
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
customExportConditions: ["browser", "node"],
},
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/packages/*/src/**/*.test.[t]s?(x)"],
globalSetup: "<rootDir>/test/globalSetup.ts",
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
@@ -40,7 +40,6 @@ const config: Config = {
"^!!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)).+$",

View File

@@ -5,7 +5,6 @@ export default {
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
"src/vector/localstorage-fix.ts",
"scripts/**",
"playwright/**",
"test/**",
@@ -42,8 +41,6 @@ export default {
"util",
// Embedded into webapp
"@element-hq/element-call-embedded",
// Transitive dep of jest
"jsdom",
// Used by matrix-js-sdk, which means we have to include them as a
// dependency so that // we can run `tsc` (since we import the typescript
@@ -51,13 +48,11 @@ export default {
// would with a normal library).
"@types/content-type",
"@types/sdp-transform",
// Used in EW but failed because of "link:"
"@element-hq/web-shared-components",
],
ignoreBinaries: [
// Used in scripts & workflows
"jq",
"wait-on",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;

View File

@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import * as YAML from "yaml";
import * as fs from "node:fs";
import * as fs from "fs";
export type BuildConfig = {
// Dev note: make everything here optional for user safety. Invalid

View File

@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as fs from "node:fs";
import * as childProcess from "node:child_process";
import * as fs from "fs";
import * as childProcess from "child_process";
import * as semver from "semver";
import { type BuildConfig } from "./BuildConfig.ts";
import { type BuildConfig } from "./BuildConfig";
// This expects to be run from ./scripts/install.ts

View File

@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { readBuildConfig } from "../BuildConfig.ts";
import { installer } from "../installer.ts";
import { readBuildConfig } from "../BuildConfig";
import { installer } from "../installer";
const buildConf = readBuildConfig();
installer(buildConf);

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.12.7-rc.1",
"version": "1.12.1",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -29,7 +29,7 @@
"UserFriendlyError"
],
"scripts": {
"i18n": "matrix-gen-i18n src res packages/shared-components/src && yarn i18n:sort && yarn i18n:lint",
"i18n": "matrix-gen-i18n src res packages/shared-components && 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: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",
@@ -38,16 +38,16 @@
"clean": "rimraf lib webapp",
"build": "yarn clean && yarn build:genfiles && yarn build:bundle",
"build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
"build:res": "node scripts/copy-res.ts",
"build:res": "ts-node scripts/copy-res.ts",
"build:genfiles": "yarn build:res && yarn build:module_system",
"build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
"build:bundle": "webpack --progress --mode production",
"build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json",
"build:module_system": "node module_system/scripts/install.ts",
"build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts",
"dist": "./scripts/package.sh",
"start": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n modules,res \"yarn build:module_system\" \"yarn build:res\" && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"",
"start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --server-type https\"",
"start:res": "node scripts/copy-res.ts -w",
"start:res": "ts-node scripts/copy-res.ts -w",
"start:js": "webpack serve --output-path webapp --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js --mode development",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows",
"lint:js": "eslint --max-warnings 0 src test playwright module_system && prettier --check .",
@@ -65,35 +65,34 @@
"coverage": "yarn test --coverage",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
"install": "yarn --cwd packages/shared-components install --frozen-lockfile",
"postinstall": "patch-package"
},
"resolutions": {
"**/pretty-format/react-is": "19.2.0",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"oidc-client-ts": "3.4.1",
"@playwright/test": "1.56.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.1",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001756",
"caniuse-lite": "1.0.30001750",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "1.9.0",
"@element-hq/web-shared-components": "link:packages/shared-components",
"@fontsource/fira-code": "^5",
"@element-hq/element-web-module-api": "1.4.1",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.30.0",
"@matrix-org/emojibase-bindings": "^1.5.0",
"@matrix-org/analytics-events": "^0.29.2",
"@matrix-org/emojibase-bindings": "^1.3.4",
"@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.0.0",
"@vector-im/compound-web": "^8.1.2",
"@vector-im/matrix-wysiwyg": "2.40.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
@@ -104,11 +103,12 @@
"browserslist": "^4.23.2",
"classnames": "^2.2.6",
"commonmark": "^0.31.0",
"counterpart": "^0.18.6",
"css-tree": "^3.0.0",
"diff-dom": "^5.0.0",
"diff-match-patch": "^1.0.5",
"domutils": "^3.2.2",
"emojibase-regex": "^17.0.0",
"emojibase-regex": "15.3.2",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "11.0.13",
@@ -118,26 +118,26 @@
"html-entities": "^2.0.0",
"html-react-parser": "^5.2.2",
"is-ip": "^3.1.0",
"js-xxhash": "^5.0.0",
"js-xxhash": "^4.0.0",
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-html": "4.3.2",
"linkify-react": "4.3.2",
"linkify-string": "4.3.2",
"linkifyjs": "4.3.2",
"lodash": "^4.17.21",
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-js-sdk": "39.4.0-rc.0",
"matrix-widget-api": "^1.14.0",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.10.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.275.1",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -179,14 +179,15 @@
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.4.0",
"@element-hq/element-call-embedded": "0.16.1",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.16.0",
"@element-hq/element-web-playwright-common": "^2.0.0",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "1.57.0",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@rrweb/types": "^2.0.0-alpha.18",
"@sentry/webpack-plugin": "^4.0.0",
"@storybook/react-vite": "^10.0.7",
"@storybook/react-vite": "^9.1.10",
"@stylistic/eslint-plugin": "^5.0.0",
"@svgr/webpack": "^8.0.0",
"@testing-library/dom": "^10.4.0",
@@ -202,7 +203,7 @@
"@types/express": "^5.0.0",
"@types/file-saver": "^2.0.3",
"@types/glob-to-regexp": "^0.4.1",
"@types/jest": "30.0.0",
"@types/jest": "29.5.12",
"@types/jitsi-meet": "^2.0.2",
"@types/jsrsasign": "^10.5.4",
"@types/katex": "^0.16.0",
@@ -213,9 +214,9 @@
"@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.2",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.2.3",
"@types/react-dom": "19.2.1",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
"@types/sdp-transform": "^2.4.10",
@@ -224,7 +225,7 @@
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"babel-jest": "^30.0.0",
"babel-jest": "^29.0.0",
"babel-loader": "^10.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"blob-polyfill": "^9.0.0",
@@ -256,10 +257,10 @@
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.0.0",
"jest": "^29.6.2",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^30.0.0",
"jest-mock": "^30.0.0",
"jest-environment-jsdom": "^29.7.0",
"jest-mock": "^29.6.2",
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"knip": "^5.36.2",
@@ -287,18 +288,19 @@
"rimraf": "^6.0.0",
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"storybook": "^10.0.7",
"storybook": "^9.1.10",
"stylelint": "^16.23.0",
"stylelint-config-standard": "^39.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^11.0.0",
"ts-node": "^10.9.1",
"typescript": "5.8.3",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^5.0.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^6.0.0",
"webpack-dev-server": "^5.0.0",
"webpack-retry-chunk-load-plugin": "^3.1.1",
@@ -311,7 +313,7 @@
"relativePaths": true
},
"engines": {
"node": ">=22.18"
"node": ">=20.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -1,12 +1,4 @@
/*
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.
*/
module.exports = {
root: true,
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: [
"plugin:matrix-org/babel",
@@ -16,7 +8,6 @@ module.exports = {
],
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
},
env: {
browser: true,

View File

@@ -1,2 +1 @@
dist/
i18n/i18nKeys.d.ts

View File

@@ -6,7 +6,6 @@ import React, { useLayoutEffect } from "react";
import { setLanguage } from "../src/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
import { StoryContext } from "storybook/internal/csf";
import { I18nApi, I18nContext } from "../src";
export const globalTypes = {
theme: {
@@ -71,17 +70,9 @@ const withTooltipProvider: Decorator = (Story) => {
);
};
const withI18nProvider: Decorator = (Story) => {
return (
<I18nContext.Provider value={new I18nApi()}>
<Story />
</I18nContext.Provider>
);
};
const preview: Preview = {
tags: ["autodocs"],
decorators: [withThemeProvider, withTooltipProvider, withI18nProvider],
decorators: [withThemeProvider, withTooltipProvider],
parameters: {
options: {
storySort: {

View File

@@ -5,14 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { waitForPageReady, TestRunnerConfig } from "@storybook/test-runner";
import { waitForPageReady } from "@storybook/test-runner";
import { toMatchImageSnapshot } from "jest-image-snapshot";
const customSnapshotsDir = `${process.cwd()}/playwright/snapshots/`;
const customReceivedDir = `${process.cwd()}/playwright/received/`;
const config: TestRunnerConfig = {
setup() {
/**
* @type {import('@storybook/test-runner').TestRunnerConfig}
*/
const config = {
setup(page) {
expect.extend({ toMatchImageSnapshot });
},
async postVisit(page, context) {

View File

@@ -1,21 +0,0 @@
module.exports = {
sourceMaps: true,
presets: [
[
"@babel/preset-env",
{
include: ["@babel/plugin-transform-class-properties"],
},
],
["@babel/preset-typescript", { allowDeclareFields: true }],
"@babel/preset-react",
],
plugins: [
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-transform-numeric-separator",
"@babel/plugin-transform-object-rest-spread",
"@babel/plugin-transform-optional-chaining",
"@babel/plugin-transform-nullish-coalescing-operator",
"@babel/plugin-transform-runtime",
],
};

View File

@@ -1,58 +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.
*/
import { env } from "process";
import type { Config } from "jest";
const config: Config = {
testEnvironment: "jsdom",
testEnvironmentOptions: {
url: "http://localhost/",
},
testMatch: ["<rootDir>/src/**/*.test.[tj]s?(x)"],
setupFilesAfterEnv: ["<rootDir>/src/test/setupTests.ts"],
moduleNameMapper: {
// Support CSS module
"\\.(module.css)$": "identity-obj-proxy",
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/__mocks__/svg.js",
"\\$webapp/i18n/languages.json": "<rootDir>/../../__mocks__/languages.json",
"^react$": "<rootDir>/node_modules/react",
"^react-dom$": "<rootDir>/node_modules/react-dom",
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
},
transformIgnorePatterns: [
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|@storybook|storybook)).+$",
],
collectCoverageFrom: [
"<rootDir>/src/**/*.{js,ts,tsx}",
"<rootDir>/packages/**/*.{js,ts,tsx}",
// Coverage chokes on type definition files
"!<rootDir>/src/**/*.d.ts",
],
coverageReporters: ["text-summary", "lcov"],
testResultsProcessor: "@casualbot/jest-sonar-reporter",
prettierPath: null,
moduleDirectories: ["node_modules", "./src/test/utils"],
};
// if we're running under GHA, enable the GHA reporter
if (env["GITHUB_ACTIONS"] !== undefined) {
const reporters: Config["reporters"] = [["github-actions", { silent: false }], "summary"];
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
reporters.push("<rootDir>/../../test/slowReporter.cjs");
}
config.reporters = reporters;
}
export default config;

View File

@@ -1,6 +1,6 @@
{
"name": "@element-hq/web-shared-components",
"version": "0.0.0-test.11",
"version": "0.0.0-test.5",
"description": "Shared components for Element",
"author": "New Vector Ltd.",
"repository": {
@@ -19,10 +19,6 @@
"types": "./dist/element-web-shared-components.d.ts",
"default": "./dist/element-web-shared-components.mjs"
}
},
"./dist/element-web-shared-components.css": {
"require": "./dist/element-web-shared-components.css",
"import": "./dist/element-web-shared-components.css"
}
},
"types": "dist/element-web-shared-components.d.ts",
@@ -34,8 +30,8 @@
"package.json"
],
"scripts": {
"test": "jest",
"prepare": "patch-package && yarn --cwd ../.. build:res && node scripts/gatherTranslationKeys.ts && vite build",
"postinstall": "patch-package",
"prepare": "vite build",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"lint": "yarn lint:types && yarn lint:js",
@@ -45,44 +41,25 @@
"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"
},
"resolutions": {
"playwright": "1.57.0"
},
"dependencies": {
"@element-hq/element-web-module-api": "^1.8.0",
"@vector-im/compound-design-tokens": "^6.3.0",
"classnames": "^2.5.1",
"counterpart": "^0.18.6",
"lodash": "^4.17.21",
"matrix-web-i18n": "^3.4.0",
"patch-package": "^8.0.1",
"react-merge-refs": "^3.0.2",
"temporal-polyfill": "^0.3.0"
"counterpart": "^0.18.6"
},
"devDependencies": {
"@element-hq/element-web-playwright-common": "^2.0.0",
"@playwright/test": "1.57.0",
"@storybook/addon-a11y": "^10.0.7",
"@storybook/addon-designs": "^11.0.1",
"@storybook/addon-docs": "^10.0.7",
"@storybook/icons": "^2.0.0",
"@storybook/react-vite": "^10.0.7",
"@storybook/test-runner": "^0.24.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/counterpart": "^0.18.4",
"@types/jest-image-snapshot": "^6.4.0",
"@types/lodash": "^4.17.20",
"@types/react": "^19.2.2",
"@storybook/addon-a11y": "^9.1.10",
"@storybook/addon-designs": "^10.0.2",
"@storybook/addon-docs": "^9.1.10",
"@storybook/icons": "^1.6.0",
"@storybook/react-vite": "^9.1.10",
"@storybook/test-runner": "^0.23.0",
"concurrently": "^9.2.1",
"eslint": "8",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-storybook": "^10.0.7",
"jest": "^30.2.0",
"eslint-plugin-storybook": "^9.1.10",
"jest-image-snapshot": "^6.5.1",
"patch-package": "^8.0.1",
"prettier": "^3.6.2",
"storybook": "^10.0.7",
"storybook": "^9.1.10",
"typescript": "^5.9.3",
"vite": "^7.1.9",
"vite-plugin-dts": "^4.5.4",
@@ -91,8 +68,5 @@
"engines": {
"node": ">=20.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"peerDependencies": {
"@vector-im/compound-web": "^8.2.5"
}
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -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();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, useMemo, type ComponentType } from "react";
import { omitBy, pickBy } from "lodash";
import { MockViewModel } from "./viewmodel/MockViewModel";
import { type ViewModel } from "./viewmodel/ViewModel";
interface ViewWrapperProps<V> {
/**
* The component to render, which should accept a `vm` prop of type `V`.
*/
Component: ComponentType<{ vm: V }>;
/**
* The props to pass to the component, which can include both snapshot data and actions.
*/
props: Record<string, any>;
}
/**
* A wrapper component that creates a view model instance and passes it to the specified component.
* This is useful for testing components in isolation with a mocked view model and allows to use primitive types in stories.
*
* Props is parsed and split into snapshot and actions. Where values that are functions (`typeof Function`) are considered actions and the rest is considered the snapshot.
*
* @example
* ```tsx
* <ViewWrapper<SnapshotType, ViewModelType> props={Snapshot&Actions} Component={MyComponent} />
* ```
*/
export function ViewWrapper<T, V extends ViewModel<T>>({
props,
Component,
}: Readonly<ViewWrapperProps<V>>): JSX.Element {
const vm = useMemo(() => {
const isFunction = (value: any): value is typeof Function => typeof value === typeof Function;
const snapshot = omitBy(props, isFunction) as T;
const actions = pickBy(props, isFunction);
const vm = new MockViewModel<T>(snapshot);
Object.assign(vm, actions);
return vm as unknown as V;
}, [props]);
return <Component vm={vm} />;
}

View File

@@ -6,7 +6,7 @@
*/
.audioPlayer {
padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) !important;
padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x);
}
.mediaInfo {

View File

@@ -9,18 +9,18 @@ import React, { type JSX } from "react";
import { fn } from "storybook/test";
import type { Meta, StoryFn } from "@storybook/react-vite";
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
import { useMockedViewModel } from "../../useMockedViewModel";
import {
AudioPlayerView,
type AudioPlayerViewActions,
type AudioPlayerViewSnapshot,
type AudioPlayerViewModel,
} from "./AudioPlayerView";
import { ViewWrapper } from "../../ViewWrapper";
type AudioPlayerProps = AudioPlayerViewSnapshot & AudioPlayerViewActions;
const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...rest }: AudioPlayerProps): JSX.Element => {
const vm = useMockedViewModel(rest, {
togglePlay,
onKeyDown,
onSeekbarChange,
});
return <AudioPlayerView vm={vm} />;
};
const AudioPlayerViewWrapper = (props: AudioPlayerProps): JSX.Element => (
<ViewWrapper<AudioPlayerViewSnapshot, AudioPlayerViewModel> Component={AudioPlayerView} props={props} />
);
export default {
title: "Audio/AudioPlayerView",

View File

@@ -14,8 +14,6 @@ import { fireEvent } from "@testing-library/dom";
import * as stories from "./AudioPlayerView.stories.tsx";
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
import { I18nContext } from "../../utils/i18nContext.ts";
import { I18nApi } from "../../index.ts";
const { Default, NoMediaName, NoSize, HasError } = composeStories(stories);
@@ -66,9 +64,7 @@ describe("AudioPlayerView", () => {
error: false,
});
render(<AudioPlayerView vm={vm} />, {
wrapper: ({ children }) => <I18nContext.Provider value={new I18nApi()}>{children}</I18nContext.Provider>,
});
render(<AudioPlayerView vm={vm} />);
await user.click(screen.getByRole("button", { name: "Play" }));
expect(togglePlay).toHaveBeenCalled();

View File

@@ -14,7 +14,7 @@ import { Flex } from "../../utils/Flex";
import styles from "./AudioPlayerView.module.css";
import { PlayPauseButton } from "../PlayPauseButton";
import { type PlaybackState } from "../playback";
import { useI18n } from "../../utils/i18nContext";
import { _t } from "../../utils/i18n";
import { formatBytes } from "../../utils/FormattingUtils";
import { Clock } from "../Clock";
import { SeekBar } from "../SeekBar";
@@ -90,8 +90,6 @@ interface AudioPlayerViewProps {
* ```
*/
export function AudioPlayerView({ vm }: Readonly<AudioPlayerViewProps>): JSX.Element {
const { translate: _t } = useI18n();
const {
playbackState,
mediaName = _t("timeline|m.audio|unnamed_audio"),

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AudioPlayerView renders the audio player in default state 1`] = `
<div>
@@ -23,7 +23,7 @@ exports[`AudioPlayerView renders the audio player in default state 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_147l5_17"
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -114,7 +114,7 @@ exports[`AudioPlayerView renders the audio player in error state 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_147l5_17"
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -210,7 +210,7 @@ exports[`AudioPlayerView renders the audio player without media name 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_147l5_17"
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -301,7 +301,7 @@ exports[`AudioPlayerView renders the audio player without size 1`] = `
tabindex="-1"
>
<div
class="_indicator-icon_147l5_17"
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Clock renders the clock 1`] = `
<div>

View File

@@ -6,6 +6,6 @@
*/
.button {
border-radius: 32px !important;
background-color: var(--cpd-color-bg-subtle-primary) !important;
border-radius: 32px;
background-color: var(--cpd-color-bg-subtle-primary);
}

View File

@@ -11,7 +11,7 @@ import Play from "@vector-im/compound-design-tokens/assets/web/icons/play-solid"
import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
import styles from "./PlayPauseButton.module.css";
import { useI18n } from "../../utils/i18nContext";
import { _t } from "../../utils/i18n";
export interface PlayPauseButtonProps extends HTMLAttributes<HTMLButtonElement> {
/**
@@ -46,8 +46,6 @@ export function PlayPauseButton({
togglePlay,
...rest
}: Readonly<PlayPauseButtonProps>): JSX.Element {
const { translate: _t } = useI18n();
const label = playing ? _t("action|pause") : _t("action|play");
return (

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PlayPauseButton renders the button in default state 1`] = `
<div>
@@ -13,7 +13,7 @@ exports[`PlayPauseButton renders the button in default state 1`] = `
tabindex="0"
>
<div
class="_indicator-icon_147l5_17"
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
@@ -45,7 +45,7 @@ exports[`PlayPauseButton renders the button in playing state 1`] = `
tabindex="0"
>
<div
class="_indicator-icon_147l5_17"
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg

View File

@@ -10,7 +10,7 @@ import { throttle } from "lodash";
import classNames from "classnames";
import style from "./SeekBar.module.css";
import { useI18n } from "../../utils/i18nContext";
import { _t } from "../../utils/i18n";
export interface SeekBarProps extends React.InputHTMLAttributes<HTMLInputElement> {
/**
@@ -33,8 +33,6 @@ interface ISeekCSS extends CSSProperties {
* ```
*/
export function SeekBar({ value = 0, className, ...rest }: Readonly<SeekBarProps>): JSX.Element {
const { translate: _t } = useI18n();
const [newValue, setNewValue] = useState(value);
// Throttle the value setting to avoid excessive re-renders
const setThrottledValue = useMemo(() => throttle(setNewValue, 10), []);

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Seekbar renders the clock 1`] = `
<div>

View File

@@ -6,7 +6,7 @@
*/
import React from "react";
import { type Meta, type StoryObj } from "@storybook/react-vite";
import { type Meta, type StoryObj } from "@storybook/react-vite/*";
import { AvatarWithDetails } from "./AvatarWithDetails";

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AvatarWithDetails renders a textual event 1`] = `
<div>

View File

@@ -1,93 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
:root {
--cpd-color-gradient-critical-linear: linear-gradient(
180deg,
var(--cpd-color-alpha-red-500) 0%,
var(--cpd-color-alpha-red-400) 20%,
var(--cpd-color-alpha-red-300) 40%,
var(--cpd-color-alpha-red-200) 60%,
var(--cpd-color-alpha-red-100) 80%,
var(--cpd-color-transparent) 100%
);
}
.banner {
container-type: inline-size;
container-name: banner;
display: flex;
align-items: center;
justify-content: start;
gap: var(--cpd-space-3x);
padding: var(--cpd-space-4x);
border-top: 1px solid var(--cpd-color-gray-400);
white-space: nowrap;
}
.banner[data-type="success"] {
background: var(--cpd-color-gradient-subtle-linear);
border-color: var(--cpd-color-green-900);
}
.banner[data-type="critical"] {
background: var(--cpd-color-gradient-critical-linear);
border-color: var(--cpd-color-border-critical-primary);
}
.banner[data-type="info"] {
background: var(--cpd-color-gradient-info-linear);
border-color: var(--cpd-color-blue-900);
}
.banner[data-type="info"] :is(svg) {
color: var(--cpd-color-blue-900);
}
.banner[data-type="success"] :is(.content, svg) {
color: var(--cpd-color-green-900);
}
.banner[data-type="critical"] :is(.content, svg) {
color: var(--cpd-color-red-900);
}
.banner p {
margin: 0;
}
.icon {
/* lock icon dimensions */
min-width: 32px;
min-height: 32px;
max-width: 32px;
max-height: 32px;
margin: 4px;
/* centre svg icons, as they are not full width */
flex: 0;
display: flex;
align-items: center;
justify-content: center;
}
.icon img {
border-radius: 50%;
}
.actions {
margin-left: auto;
flex: 0;
display: flex;
flex-direction: row;
gap: var(--cpd-space-1x);
align-self: center;
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fn } from "storybook/test";
import { type Meta, type StoryObj } from "@storybook/react-vite";
import { Button } from "@vector-im/compound-web";
import { Banner } from "./Banner";
import { _t } from "../../utils/i18n";
const meta = {
title: "room/Banner",
component: Banner,
tags: ["autodocs"],
args: {
children: <p>Hello! This is a status banner.</p>,
onClose: fn(),
},
} satisfies Meta<typeof Banner>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Info: Story = {
args: {
type: "info",
},
};
export const Success: Story = {
args: {
type: "success",
},
};
export const Critical: Story = {
args: {
type: "critical",
},
};
export const WithAction: Story = {
args: {
children: (
<p>
{_t(
"encryption|pinned_identity_changed",
{ displayName: "Alice", userId: "@alice:example.org" },
{
a: (sub) => <a href="https://example.org">{sub}</a>,
b: (sub) => <b>{sub}</b>,
},
)}
</p>
),
actions: <Button kind="primary">{_t("encryption|withdraw_verification_action")}</Button>,
},
};
export const WithAvatarImage: Story = {
args: {
avatar: <img alt="Example" src="https://picsum.photos/32/32" />,
},
};
export const WithoutClose: Story = {
args: {
onClose: undefined,
},
};

View File

@@ -1,41 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { composeStories } from "@storybook/react-vite";
import * as stories from "./Banner.stories.tsx";
const { Default, Info, Success, WithAction, WithAvatarImage, Critical } = composeStories(stories);
describe("AvatarWithDetails", () => {
it("renders a default banner", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders a info banner", () => {
const { container } = render(<Info />);
expect(container).toMatchSnapshot();
});
it("renders a success banner", () => {
const { container } = render(<Success />);
expect(container).toMatchSnapshot();
});
it("renders a critical banner", () => {
const { container } = render(<Critical />);
expect(container).toMatchSnapshot();
});
it("renders a banner with an action", () => {
const { container } = render(<WithAction />);
expect(container).toMatchSnapshot();
});
it("renders a banner with an avatar iamge", () => {
const { container } = render(<WithAvatarImage />);
expect(container).toMatchSnapshot();
});
});

View File

@@ -1,93 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, {
type MouseEventHandler,
type ReactElement,
type ReactNode,
type PropsWithChildren,
useMemo,
} from "react";
import { Button } from "@vector-im/compound-web";
import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle";
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info";
import styles from "./Banner.module.css";
import { _t } from "../../utils/i18n";
interface BannerProps {
/**
* The type of the status banner.
*/
type?: "success" | "info" | "critical";
/**
* The banner avatar.
*/
avatar?: React.ReactNode;
className?: string;
/**
* Actions presented to the user in the right-hand side of the banner alongside the dismiss button.
*/
actions?: ReactNode;
/**
* Called when the user presses the "dismiss" button.
*/
onClose?: MouseEventHandler<HTMLButtonElement>;
}
/**
* A banner component used for displaying user-facing information above the message composer.
*
* @example
* ```tsx
* <Banner onClose={onCloseHandler} />
* ```
*/
export function Banner({
type,
children,
avatar,
className,
actions,
onClose,
...props
}: PropsWithChildren<BannerProps>): ReactElement {
const classes = classNames(styles.banner, className);
const icon = useMemo(() => {
switch (type) {
case "critical":
return <ErrorIcon fontSize={24} {...props} />;
case "info":
return <InfoIcon fontSize={24} {...props} />;
case "success":
return <CheckCircleIcon fontSize={24} {...props} />;
default:
return <InfoIcon fontSize={24} {...props} />;
}
}, [type, props]);
return (
<div {...props} className={classes} data-type={type}>
<div className={styles.icon}>{avatar ?? icon}</div>
<span className={styles.content}>{children}</span>
<div className={styles.actions}>
{actions}
{onClose && (
<Button kind="secondary" size="sm" onClick={onClose}>
{_t("action|dismiss")}
</Button>
)}
</div>
</div>
);
}

View File

@@ -1,290 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`AvatarWithDetails renders a banner with an action 1`] = `
<div>
<div
class="banner"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
<span
class="content"
>
<p>
encryption|pinned_identity_changed
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
encryption|withdraw_verification_action
</button>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a banner with an avatar iamge 1`] = `
<div>
<div
class="banner"
>
<div
class="icon"
>
<img
alt="Example"
src="https://picsum.photos/32/32"
/>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a critical banner 1`] = `
<div>
<div
class="banner"
data-type="critical"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a default banner 1`] = `
<div>
<div
class="banner"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a info banner 1`] = `
<div>
<div
class="banner"
data-type="info"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
exports[`AvatarWithDetails renders a success banner 1`] = `
<div>
<div
class="banner"
data-type="success"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m10.6 13.8-2.15-2.15a.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275.95.95 0 0 0-.275.7q0 .425.275.7L9.9 15.9q.3.3.7.3t.7-.3l5.65-5.65a.95.95 0 0 0 .275-.7.95.95 0 0 0-.275-.7.95.95 0 0 0-.7-.275.95.95 0 0 0-.7.275zM12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
/>
</svg>
</div>
<span
class="content"
>
<p>
Hello! This is a status banner.
</p>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;

View File

@@ -1,8 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export * from "./Banner";

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TextualEventView renders a textual event 1`] = `
<div>

View File

@@ -5,4 +5,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
export { TextualEventView, type TextualEventViewSnapshot } from "./TextualEventView";
export { TextualEventView } from "./TextualEventView";

View File

@@ -11,7 +11,6 @@ export * from "./audio/Clock";
export * from "./audio/PlayPauseButton";
export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails";
export * from "./composer/Banner";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./pill-input/Pill";
@@ -23,18 +22,11 @@ export * from "./utils/Flex";
// Utils
export * from "./utils/i18n";
export * from "./utils/i18nContext";
export * from "./utils/humanize";
export * from "./utils/DateUtils";
export * from "./utils/numbers";
export * from "./utils/FormattingUtils";
export * from "./utils/I18nApi";
// MVVM
export * from "./viewmodel";
export * from "./useMockedViewModel";
export * from "./ViewWrapper";
export * from "./useViewModel";
// i18n (we must export this directly in order to not confuse the type bundler, it seems,
// otherwise it will leave it as a relative import rather than bundling it)
export type * from "./i18nKeys.d.ts";

View File

@@ -7,7 +7,7 @@
.mediaBody {
background-color: var(--cpd-color-bg-subtle-secondary);
border-radius: var(--cpd-space-2x) !important;
border-radius: var(--cpd-space-2x);
max-width: 243px; /* use max-width instead of width so it fits within right panels */
font: var(--cpd-font-body-md-regular);

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MediaBody renders the media body 1`] = `
<div>

View File

@@ -12,7 +12,7 @@ import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"
import { Flex } from "../../utils/Flex";
import styles from "./Pill.module.css";
import { useI18n } from "../../utils/i18nContext";
import { _t } from "../../utils/i18n";
export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
/**
@@ -39,7 +39,6 @@ export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick
*/
export function Pill({ className, children, label, onClick, ...props }: PropsWithChildren<PillProps>): JSX.Element {
const id = useId();
const { translate: _t } = useI18n();
return (
<Flex
@@ -54,13 +53,7 @@ export function Pill({ className, children, label, onClick, ...props }: PropsWit
{label}
</span>
{onClick && (
<IconButton
aria-describedby={id}
size="16px"
onClick={onClick}
aria-label={_t("action|delete")}
className="mx_Dialog_nonDialogButton"
>
<IconButton aria-describedby={id} size="16px" onClick={onClick} aria-label={_t("action|delete")}>
<CloseIcon color="var(--cpd-color-icon-tertiary)" />
</IconButton>
)}

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Pill renders the pill 1`] = `
<div>
@@ -18,14 +18,14 @@ exports[`Pill renders the pill 1`] = `
<button
aria-describedby="_r_0_"
aria-label="Delete"
class="_icon-button_1pz9o_8 mx_Dialog_nonDialogButton"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 16px;"
tabindex="0"
>
<div
class="_indicator-icon_147l5_17"
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PillInput renders only the input without children 1`] = `
<div>

View File

@@ -8,8 +8,8 @@
import React from "react";
import { fn } from "storybook/test";
import type { Meta, StoryFn } from "@storybook/react-vite";
import { RichItem } from "./RichItem";
import type { Meta, StoryFn } from "@storybook/react-vite";
const currentTimestamp = new Date("2025-03-09T12:00:00Z").getTime();

View File

@@ -9,8 +9,8 @@ import React, { type HTMLAttributes, type JSX, memo } from "react";
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
import styles from "./RichItem.module.css";
import { humanizeTime } from "../../utils/humanize";
import { Flex } from "../../utils/Flex";
import { useI18n } from "../../utils/i18nContext";
export interface RichItemProps extends HTMLAttributes<HTMLLIElement> {
/**
@@ -63,8 +63,6 @@ export const RichItem = memo(function RichItem({
selected,
...props
}: RichItemProps): JSX.Element {
const i18n = useI18n();
return (
<li
className={styles.richItem}
@@ -79,7 +77,7 @@ export const RichItem = memo(function RichItem({
<span className={styles.description}>{description}</span>
{timestamp && (
<span role="timer" className={styles.timestamp}>
{i18n.humanizeTime(timestamp)}
{humanizeTime(timestamp)}
</span>
)}
</li>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RichItem renders the item in default state 1`] = `
<div>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RichItem renders the list 1`] = `
<div>

View File

@@ -1,22 +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.
*/
import fetchMock from "fetch-mock-jest";
import { setLanguage } from "../../src/utils/i18n";
import en from "../../../../src/i18n/strings/en_EN.json";
export function setupLanguageMock(): void {
fetchMock
.get("/i18n/languages.json", {
en: "en_EN.json",
})
.get("end:en_EN.json", en);
}
setupLanguageMock();
setLanguage("en");

View File

@@ -1,55 +0,0 @@
/*
Copyright 2025 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// Copied from element-web/test/test-utils because, seemingly, if we
// set that as the modules directory to use it directly, it fails to
// actually put the right thing in the context somehow.
import React, { type ReactElement } from "react";
// eslint-disable-next-line no-restricted-imports
import { render, type RenderOptions } from "@testing-library/react";
import { TooltipProvider } from "@vector-im/compound-web";
import { I18nApi, I18nContext } from "../..";
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
return ({ children }: { children: React.ReactNode }) => {
if (Wrapper) {
return (
<I18nContext.Provider value={new I18nApi()}>
<Wrapper>
<TooltipProvider>{children}</TooltipProvider>
</Wrapper>
</I18nContext.Provider>
);
} else {
return (
<I18nContext.Provider value={new I18nApi()}>
<TooltipProvider>{children}</TooltipProvider>
</I18nContext.Provider>
);
}
};
};
const customRender = (ui: ReactElement, options: RenderOptions = {}): ReturnType<typeof render> => {
return render(ui, {
...options,
wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"],
}) as ReturnType<typeof render>;
};
// eslint-disable-next-line no-restricted-imports
export * from "@testing-library/react";
/**
* This custom render function wraps your component with a TooltipProvider.
* See https://testing-library.com/docs/react-testing-library/setup/#custom-render
*/
export { customRender as render };

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { useMemo } from "react";
import { MockViewModel, type ViewModel } from "./viewmodel";
/**
* Hook helper to return a mocked view model created with the given snapshot and actions.
* This is useful for testing components in isolation with a mocked view model and allows to use primitive types in stories.
*
* @param snapshot
* @param actions
*/
export function useMockedViewModel<S, A>(snapshot: S, actions: A): ViewModel<S> & A {
return useMemo(() => {
const vm = new MockViewModel<S>(snapshot);
Object.assign(vm, actions);
return vm as unknown as ViewModel<S> & A;
}, [snapshot, actions]);
}

View File

@@ -1,22 +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.
*/
import { type TranslationKey } from "../i18nKeys";
import { I18nApi } from "./I18nApi";
describe("I18nApi", () => {
it("can register a translation and use it", () => {
const i18n = new I18nApi();
i18n.register({
"hello.world": {
en: "Hello, World!",
},
});
expect(i18n.translate("hello.world" as TranslationKey)).toBe("Hello, World!");
});
});

View File

@@ -6,9 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type I18nApi } from "@element-hq/element-web-module-api";
import { _t as _tFromModule } from "./i18n";
import { _t } from "./i18n";
// These are the constants we use for when to break the text
const MILLISECONDS_RECENT = 15000;
@@ -23,15 +21,13 @@ const HOURS_1_DAY = 26;
* @param {number} timeMillis The time in millis to compare against.
* @returns {string} The humanized time.
*/
export function humanizeTime(timeMillis: number, i18nApi?: I18nApi): string {
export function humanizeTime(timeMillis: number): string {
const now = Date.now();
let msAgo = now - timeMillis;
const minutes = Math.abs(Math.ceil(msAgo / 60000));
const hours = Math.ceil(minutes / 60);
const days = Math.ceil(hours / 24);
const _t = i18nApi?.translate ?? _tFromModule;
if (msAgo >= 0) {
// Past
if (msAgo <= MILLISECONDS_RECENT) return _t("time|few_seconds_ago");

View File

@@ -1,46 +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.
*/
import counterpart from "counterpart";
import { registerTranslations, setMissingEntryGenerator, getLocale, setLocale } from "./i18n";
describe("i18n utils", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("should wrap registerTranslations", () => {
jest.spyOn(counterpart, "registerTranslations");
registerTranslations("en", { test: "This is a test" });
expect(counterpart.registerTranslations).toHaveBeenCalledWith("en", { test: "This is a test" });
});
it("should wrap setMissingEntryGenerator", () => {
jest.spyOn(counterpart, "setMissingEntryGenerator");
const dummyFn = jest.fn();
setMissingEntryGenerator(dummyFn);
expect(counterpart.setMissingEntryGenerator).toHaveBeenCalledWith(dummyFn);
});
it("should wrap getLocale", () => {
jest.spyOn(counterpart, "getLocale");
getLocale();
expect(counterpart.getLocale).toHaveBeenCalled();
});
it("should wrap setLocale", () => {
jest.spyOn(counterpart, "setLocale");
setLocale("en");
expect(counterpart.setLocale).toHaveBeenCalledWith("en");
});
});

View File

@@ -22,10 +22,10 @@
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
import React from "react";
import { KEY_SEPARATOR } from "matrix-web-i18n";
import { type TranslationKey as _TranslationKey, KEY_SEPARATOR } from "matrix-web-i18n";
import counterpart from "counterpart";
import type { TranslationKey } from "../index";
import type Translations from "../../../../src/i18n/strings/en_EN.json";
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
@@ -45,23 +45,16 @@ counterpart.setSeparator(KEY_SEPARATOR);
const FALLBACK_LOCALE = "en";
counterpart.setFallbackLocale(FALLBACK_LOCALE);
// export wrappers around these functions because if we used counterpart directly from
// element-web, it operates on a different instance of counterpart
export function registerTranslations(locale: string, data: object): void {
counterpart.registerTranslations(locale, data);
}
export function setMissingEntryGenerator(callback: (value: string) => void): void {
counterpart.setMissingEntryGenerator(callback);
}
export function getLocale(): string {
return counterpart.getLocale();
}
export function setLocale(value: string): string {
return counterpart.setLocale(value);
}
/**
* A type representing the union of possible keys into the translation file using `|` delimiter to access nested fields.
* @example `common|error` to access `error` within the `common` sub-object.
* {
* "common": {
* "error": "Error"
* }
* }
*/
export type TranslationKey = _TranslationKey<typeof Translations>;
// Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings

View File

@@ -1,27 +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.
*/
import { createContext, useContext } from "react";
import { type I18nApi } from "@element-hq/element-web-module-api";
export const I18nContext = createContext<I18nApi | null>(null);
I18nContext.displayName = "I18nContext";
/**
* A hook to get the i18n API from the context. Will throw if no i18n context is found.
* @throws If no i18n context is found
* @returns The i18n API from the context
*/
export function useI18n(): I18nApi {
const i18n = useContext(I18nContext);
if (!i18n) {
throw new Error("useI18n must be used within an I18nContext.Provider");
}
return i18n;
}

View File

@@ -1,47 +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.
*/
import { renderHook } from "jest-matrix-react";
import { BaseViewModel } from "../BaseViewModel";
import { useCreateAutoDisposedViewModel } from "../useCreateAutoDisposedViewModel";
class TestViewModel extends BaseViewModel<{ count: number }, { initial: number }> {
public constructor(props: { initial: number }) {
super(props, { count: props.initial });
}
public increment(): void {
const newCount = this.getSnapshot().count + 1;
this.snapshot.set({ count: newCount });
}
}
describe("useAutoDisposedViewModel", () => {
it("should return view-model", () => {
const vmCreator = (): TestViewModel => new TestViewModel({ initial: 0 });
const { result } = renderHook(() => useCreateAutoDisposedViewModel(vmCreator));
const vm = result.current;
expect(vm).toBeInstanceOf(TestViewModel);
expect(vm.isDisposed).toStrictEqual(false);
});
it("should dispose view-model on unmount", () => {
const vmCreator = (): TestViewModel => new TestViewModel({ initial: 0 });
const { result, unmount } = renderHook(() => useCreateAutoDisposedViewModel(vmCreator));
const vm = result.current;
vm.increment();
unmount();
expect(vm.isDisposed).toStrictEqual(true);
});
it("should recreate view-model on react strict mode", async () => {
const vmCreator = (): TestViewModel => new TestViewModel({ initial: 0 });
const output = renderHook(() => useCreateAutoDisposedViewModel(vmCreator), { reactStrictMode: true });
const vm = output.result.current;
expect(vm.isDisposed).toStrictEqual(false);
});
});

View File

@@ -1,68 +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.
*/
import { useEffect, useState } from "react";
import type { BaseViewModel } from "./BaseViewModel";
type VmCreator<B extends BaseViewModel<unknown, unknown>> = () => B;
/**
* Instantiate a view-model that gets disposed when the calling react component unmounts.
* In other words, this hook ties the lifecycle of a view-model to the lifecycle of a
* react component.
*
* @param vmCreator A function that returns a view-model instance
* @returns view-model instance from vmCreator
* @example
* const vm = useCreateAutoDisposedViewModel(() => new FooViewModel({prop1, prop2, ...});
*/
export function useCreateAutoDisposedViewModel<B extends BaseViewModel<unknown, unknown>>(vmCreator: VmCreator<B>): B {
/**
* The view-model instance may be replaced by a different instance in some scenarios.
* We want to be sure that whichever react component called this hook gets re-rendered
* when this happens, hence the state.
*/
const [viewModel, setViewModel] = useState<B>(vmCreator);
/**
* Our intention here is to ensure that the dispose method of the view-model gets called
* when the component that uses this hook unmounts.
* We can do that by combining a useEffect cleanup with an empty dependency array.
*/
useEffect(() => {
let toDispose = viewModel;
/**
* Because we use react strict mode, react will run our effects twice in dev mode to make
* sure that they are pure.
* This presents a complication - the vm instance that we created in our state initializer
* will get disposed on the first cleanup.
* So we'll recreate the view-model if it's already disposed.
*/
if (viewModel.isDisposed) {
const newViewModel = vmCreator();
// Change toDispose so that we don't end up disposing the already disposed vm.
toDispose = newViewModel;
setViewModel(newViewModel);
}
return () => {
// Dispose the view-model when this component unmounts
toDispose.dispose();
};
/**
* We explicitly provide an empty dependency array as we don't expect the viewModel/viewCreator to
* change.
* Or to put it in another way, the only reason to use this hook is to create/dispose the view-model
* and that is something that should only happen at the start/end of the lifecycle of this component.
*/
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return viewModel;
}

View File

@@ -7,7 +7,7 @@
"esModuleInterop": true,
"useDefineForClassFields": true,
"module": "es2022",
"moduleResolution": "bundler",
"moduleResolution": "node",
"target": "es2022",
"noUnusedLocals": true,
"sourceMap": false,
@@ -17,9 +17,15 @@
"lib": ["es2022", "es2024.promise", "dom", "dom.iterable"],
"strict": true,
"paths": {
"jest-matrix-react": ["./src/test/utils/jest-matrix-react"],
"jest-matrix-react": ["../../test/test-utils/jest-matrix-react"],
"rollup/parseAst": ["./node_modules/rollup/dist/parseAst"]
}
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"ts-node": {
"files": true,
"moduleTypes": {
"*": "cjs"
}
}
}

View File

@@ -26,7 +26,7 @@ export default defineConfig({
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ["react", "react-dom", "@vector-im/compound-design-tokens", "@vector-im/compound-web"],
external: ["react", "react-dom"],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
@@ -43,12 +43,5 @@ export default defineConfig({
$webapp: resolve(__dirname, "..", "..", "webapp"),
},
},
plugins: [
dts({
rollupTypes: true,
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.test.{ts,tsx}"],
copyDtsFiles: true,
}),
],
plugins: [dts({ rollupTypes: true, include: ["src/**/*.{ts,tsx}"], copyDtsFiles: true })],
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
diff --git a/node_modules/jsdom/lib/jsdom/browser/Window.js b/node_modules/jsdom/lib/jsdom/browser/Window.js
index 52d011c..f62f6d6 100644
--- a/node_modules/jsdom/lib/jsdom/browser/Window.js
+++ b/node_modules/jsdom/lib/jsdom/browser/Window.js
@@ -505,10 +505,10 @@ function installOwnProperties(window, options) {
event: makeReplaceablePropertyDescriptor("event", window),
// [LegacyUnforgeable]:
- window: { configurable: false },
- document: { configurable: false },
- location: { configurable: false },
- top: { configurable: false }
+ window: { configurable: true },
+ document: { configurable: true },
+ location: { configurable: true },
+ top: { configurable: true }
});
diff --git a/node_modules/jsdom/lib/jsdom/living/generated/Location.js b/node_modules/jsdom/lib/jsdom/living/generated/Location.js
index fc4d1dd..c855bd5 100644
--- a/node_modules/jsdom/lib/jsdom/living/generated/Location.js
+++ b/node_modules/jsdom/lib/jsdom/living/generated/Location.js
@@ -322,19 +322,19 @@ function getUnforgeables(globalObject) {
}
});
Object.defineProperties(unforgeables, {
- assign: { configurable: false, writable: false },
- replace: { configurable: false, writable: false },
- reload: { configurable: false, writable: false },
- href: { configurable: false },
- toString: { configurable: false, writable: false },
- origin: { configurable: false },
- protocol: { configurable: false },
- host: { configurable: false },
- hostname: { configurable: false },
- port: { configurable: false },
- pathname: { configurable: false },
- search: { configurable: false },
- hash: { configurable: false }
+ assign: { configurable: true, writable: false },
+ replace: { configurable: true, writable: false },
+ reload: { configurable: true, writable: false },
+ href: { configurable: true },
+ toString: { configurable: true, writable: false },
+ origin: { configurable: true },
+ protocol: { configurable: true },
+ host: { configurable: true },
+ hostname: { configurable: true },
+ port: { configurable: true },
+ pathname: { configurable: true },
+ search: { configurable: true },
+ hash: { configurable: true }
});
unforgeablesMap.set(globalObject, unforgeables);
}

Some files were not shown because too many files have changed in this diff Show More