Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4c22d43f3 | |||
| 926bb56723 | |||
| 1dfd92c1eb | |||
| 68e6b0f845 | |||
| d426063a22 | |||
| 09c3afd311 | |||
| f6731ec318 | |||
| 7e04998a58 | |||
| dae5b6b43b | |||
| 8376e43a03 | |||
| e528fefd4c | |||
| 5e87f20cd8 | |||
| 7d9981e5dd | |||
| 1d20a4c5ee | |||
| bc97560532 | |||
| a6316d99d5 | |||
| bea3574b30 | |||
| 0e6bacffed | |||
| 9be323dfd0 | |||
| d6e50598cd | |||
| 41e33a0755 | |||
| 55a7bc5b13 | |||
| b1fdf03236 | |||
| 39464f64ab | |||
| 87fd279079 | |||
| 77c41d6789 | |||
| 9e1d0367d0 | |||
| 8fd6e29f2d | |||
| cbcceee970 | |||
| d0a8879971 | |||
| e6e6f87d01 | |||
| cf51b256ce | |||
| 9d973c88f9 | |||
| e33894bed4 | |||
| 0468876aa0 | |||
| c8f1c19517 | |||
| 2598e4ea22 | |||
| ac96ab0d46 | |||
| 949d0dc8a9 | |||
| 0e0e928040 | |||
| c519438dad | |||
| 146e4772ac | |||
| 6cfe197a38 | |||
| a4a44a0c1c | |||
| 2ce2218549 | |||
| a7ddf1c88a | |||
| 1b0863483a | |||
| 8bf2049f8a | |||
| abafde4b64 | |||
| 756e1769ba | |||
| 1e28cd2b49 | |||
| 252546f085 | |||
| a31dcaf157 | |||
| 735d2bd981 | |||
| ccd7f923d0 | |||
| 8f3c1ce05e | |||
| 124afe3bf3 | |||
| 02246020a2 | |||
| b0ee3c11ff | |||
| b7dd05bc97 | |||
| 56f6c1ef46 | |||
| b4396f5943 |
@@ -44,7 +44,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
# Disable cache on Windows as it is slower than not caching
|
||||
# https://github.com/actions/setup-node/issues/975
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
|
||||
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5
|
||||
if: github.event_name != 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
repository: vectorim/element-web
|
||||
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
repository: element-hq/element-web-pro
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
path: matrix-js-sdk
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: element-web/yarn.lock
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
name: webapp
|
||||
path: webapp
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: yarn.lock
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
if: inputs.skip != true
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
name: Publish shared component npm package
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-bump:
|
||||
description: The scale of the version bump required for semver compatibility
|
||||
required: true
|
||||
default: patch
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency: release
|
||||
jobs:
|
||||
publish:
|
||||
name: "Release & Publish"
|
||||
name: "Publish"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -25,25 +16,19 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: 🔧 Set up node environment
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: ".node-version"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.ELEMENT_NPM_TOKEN }}
|
||||
|
||||
- name: 🛠️ Setup
|
||||
# When running `install` it also calls the `prepare` step which generates
|
||||
# a build
|
||||
run: yarn --cwd packages/shared-components install --pure-lockfile
|
||||
|
||||
- name: 👊 Bump version
|
||||
run: |
|
||||
cd packages/shared-components
|
||||
yarn version --no-git-tag-version --${{ github.event.inputs.version-bump }}
|
||||
git config --global user.name 'ElementRobot'
|
||||
git config --global user.email 'releases@riot.im'
|
||||
git commit -am "Shared components: ${{ github.event.inputs.version-bump }} version bump"
|
||||
git push
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
working-directory: packages/shared-components
|
||||
run: npm publish --access public --provenance
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "yarn"
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10
|
||||
with:
|
||||
operations-per-run: 100
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.18-labs@sha256:79cdc14e1c220efb546ad14a8ebc816e3277cd72d27195ced5bebdd226dd1025
|
||||
# syntax=docker.io/docker/dockerfile:1.19-labs@sha256:dce1c693ef318bca08c964ba3122ae6248e45a1b96d65c4563c8dc6fe80349a2
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:5e638ea282ab9f0224949e8cfc7bb4621710e5d21b19fc3cf6e8884fcb5839f0 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:13d1e0acc26b7ce8a40d155473759387e04d1433e73726d4bb49c67bdb197fe5
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:304079937327a6490d5c89df47c8951d76f05b346d4c6e3b10cba2e266cd4904
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -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 below)
|
||||
1. Configure the correct caching headers in your webserver (see [README.md](../README.md#caching-requirements))
|
||||
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!
|
||||
|
||||
@@ -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)", "<rootDir>/src/shared-components/**/*.test.[t]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"],
|
||||
@@ -46,6 +46,7 @@ const config: Config = {
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||
"<rootDir>/packages/**/*.{js,ts,tsx}",
|
||||
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
|
||||
// not available in that contest. So, turn off coverage instrumentation for it.
|
||||
"!<rootDir>/src/utils/SessionLock.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.12.2",
|
||||
"version": "1.12.3",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -68,13 +68,13 @@
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.1",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/react": "19.1.14",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"**/pretty-format/react-is": "19.2.0",
|
||||
"@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.30001745",
|
||||
"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"
|
||||
@@ -122,6 +122,7 @@
|
||||
"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",
|
||||
@@ -129,7 +130,7 @@
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "39.0.0",
|
||||
"matrix-js-sdk": "39.1.2",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
@@ -137,7 +138,7 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.268.6",
|
||||
"posthog-js": "1.275.1",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
@@ -187,6 +188,7 @@
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@rrweb/types": "^2.0.0-alpha.18",
|
||||
"@sentry/webpack-plugin": "^4.0.0",
|
||||
"@storybook/react-vite": "^9.1.10",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@svgr/webpack": "^8.0.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
@@ -213,9 +215,9 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.1.14",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@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",
|
||||
@@ -239,14 +241,14 @@
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-deprecate": "0.8.5",
|
||||
"eslint-plugin-deprecate": "0.8.7",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jest": "^29.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "^2.0.2",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"express": "^5.0.0",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
@@ -287,6 +289,7 @@
|
||||
"rimraf": "^6.0.0",
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"storybook": "^9.1.10",
|
||||
"stylelint": "^16.23.0",
|
||||
"stylelint-config-standard": "^39.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@element-hq/web-shared-components",
|
||||
"version": "0.0.0-test.5",
|
||||
"version": "0.0.0-test.6",
|
||||
"description": "Shared components for Element",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -31,6 +31,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"prepare": "vite build",
|
||||
"storybook": "storybook dev -p 6007",
|
||||
"build-storybook": "storybook build",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
@@ -42,7 +43,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"matrix-web-i18n": "^3.4.0",
|
||||
"patch-package": "^8.0.1"
|
||||
"patch-package": "^8.0.1",
|
||||
"counterpart": "^0.18.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-a11y": "^9.1.10",
|
||||
@@ -53,7 +55,6 @@
|
||||
"@storybook/test-runner": "^0.23.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "8",
|
||||
"jest-image-snapshot": "^6.5.1",
|
||||
"eslint-plugin-storybook": "^9.1.10",
|
||||
"jest-image-snapshot": "^6.5.1",
|
||||
"patch-package": "^8.0.1",
|
||||
|
||||
@@ -1,52 +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 React, { type JSX, useMemo, type ComponentType } from "react";
|
||||
import { omitBy, pickBy } from "lodash";
|
||||
|
||||
import { MockViewModel } from "./MockViewModel";
|
||||
import { type ViewModel } from "./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} />;
|
||||
}
|
||||
@@ -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,
|
||||
type AudioPlayerViewModel,
|
||||
} from "./AudioPlayerView";
|
||||
import { ViewWrapper } from "../../ViewWrapper";
|
||||
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
|
||||
import { useMockedViewModel } from "../../useMockedViewModel";
|
||||
|
||||
type AudioPlayerProps = AudioPlayerViewSnapshot & AudioPlayerViewActions;
|
||||
const AudioPlayerViewWrapper = (props: AudioPlayerProps): JSX.Element => (
|
||||
<ViewWrapper<AudioPlayerViewSnapshot, AudioPlayerViewModel> Component={AudioPlayerView} props={props} />
|
||||
);
|
||||
const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...rest }: AudioPlayerProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(rest, {
|
||||
togglePlay,
|
||||
onKeyDown,
|
||||
onSeekbarChange,
|
||||
});
|
||||
return <AudioPlayerView vm={vm} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Audio/AudioPlayerView",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { fireEvent } from "@testing-library/dom";
|
||||
|
||||
import * as stories from "./AudioPlayerView.stories.tsx";
|
||||
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
|
||||
import { MockViewModel } from "../../MockViewModel";
|
||||
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
|
||||
|
||||
const { Default, NoMediaName, NoSize, HasError } = composeStories(stories);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import React, { type ChangeEventHandler, type JSX, type KeyboardEventHandler, type MouseEventHandler } from "react";
|
||||
|
||||
import { type ViewModel } from "../../ViewModel";
|
||||
import { type ViewModel } from "../../viewmodel/ViewModel";
|
||||
import { useViewModel } from "../../useViewModel";
|
||||
import { MediaBody } from "../../message-body/MediaBody";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
|
||||
@@ -15,7 +15,7 @@ exports[`AudioPlayerView renders the audio player in default state 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="«r0»"
|
||||
aria-labelledby="_r_0_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -106,7 +106,7 @@ exports[`AudioPlayerView renders the audio player in error state 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="«ri»"
|
||||
aria-labelledby="_r_i_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -202,7 +202,7 @@ exports[`AudioPlayerView renders the audio player without media name 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="«r6»"
|
||||
aria-labelledby="_r_6_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -293,7 +293,7 @@ exports[`AudioPlayerView renders the audio player without size 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="«rc»"
|
||||
aria-labelledby="_r_c_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`PlayPauseButton renders the button in default state 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Play"
|
||||
aria-labelledby="«r0»"
|
||||
aria-labelledby="_r_0_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -37,7 +37,7 @@ exports[`PlayPauseButton renders the button in playing state 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Pause"
|
||||
aria-labelledby="«r6»"
|
||||
aria-labelledby="_r_6_"
|
||||
class="_icon-button_1pz9o_8 button"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
|
||||
@@ -9,7 +9,7 @@ import React from "react";
|
||||
import { type Meta, type StoryFn } from "@storybook/react-vite";
|
||||
|
||||
import { TextualEventView as TextualEventComponent } from "./TextualEventView";
|
||||
import { MockViewModel } from "../../MockViewModel";
|
||||
import { MockViewModel } from "../../viewmodel/MockViewModel";
|
||||
|
||||
export default {
|
||||
title: "Event/TextualEvent",
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React, { type ReactNode, type JSX } from "react";
|
||||
|
||||
import { type ViewModel } from "../../ViewModel";
|
||||
import { type ViewModel } from "../../viewmodel/ViewModel";
|
||||
import { useViewModel } from "../../useViewModel";
|
||||
|
||||
export type TextualEventViewSnapshot = {
|
||||
|
||||
@@ -21,13 +21,12 @@ export * from "./utils/Box";
|
||||
export * from "./utils/Flex";
|
||||
|
||||
// Utils
|
||||
export * from "./utils/i18n";
|
||||
export { setLanguage } from "./utils/i18n";
|
||||
export * from "./utils/humanize";
|
||||
export * from "./utils/DateUtils";
|
||||
export * from "./utils/numbers";
|
||||
|
||||
// MVVM
|
||||
export * from "./ViewWrapper";
|
||||
export type * from "./ViewModel";
|
||||
export * from "./viewmodel";
|
||||
export * from "./useMockedViewModel";
|
||||
export * from "./useViewModel";
|
||||
export * from "./MockViewModel";
|
||||
|
||||
@@ -11,12 +11,12 @@ exports[`Pill renders the pill 1`] = `
|
||||
/>
|
||||
<span
|
||||
class="label"
|
||||
id="«r0»"
|
||||
id="_r_0_"
|
||||
>
|
||||
Pill
|
||||
</span>
|
||||
<button
|
||||
aria-describedby="«r0»"
|
||||
aria-describedby="_r_0_"
|
||||
aria-label="Delete"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
@@ -57,7 +57,7 @@ exports[`Pill renders the pill without close button 1`] = `
|
||||
/>
|
||||
<span
|
||||
class="label"
|
||||
id="«r1»"
|
||||
id="_r_1_"
|
||||
>
|
||||
Pill
|
||||
</span>
|
||||
|
||||
@@ -11,12 +11,12 @@ exports[`RichItem renders the list 1`] = `
|
||||
>
|
||||
<span
|
||||
class="title"
|
||||
id="«r0»"
|
||||
id="_r_0_"
|
||||
>
|
||||
Rich List Title
|
||||
</span>
|
||||
<ul
|
||||
aria-labelledby="«r0»"
|
||||
aria-labelledby="_r_0_"
|
||||
class="content"
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
@@ -174,7 +174,7 @@ exports[`RichItem renders the list with isEmpty=true 1`] = `
|
||||
>
|
||||
<span
|
||||
class="title"
|
||||
id="«r1»"
|
||||
id="_r_1_"
|
||||
>
|
||||
Rich List Title
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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]);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import { type ViewModel } from "./ViewModel";
|
||||
import { type ViewModel } from "./viewmodel/ViewModel";
|
||||
|
||||
/**
|
||||
* A small wrapper around useSyncExternalStore to use a view model in a shared component view
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ViewModel } from "../../../packages/shared-components/src/ViewModel";
|
||||
import { type ViewModel } from "./ViewModel";
|
||||
import { Disposables } from "./Disposables";
|
||||
import { Snapshot } from "./Snapshot";
|
||||
import { ViewModelSubscriptions } from "./ViewModelSubscriptions";
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export * from "./BaseViewModel";
|
||||
export * from "./Disposables";
|
||||
export * from "./Snapshot";
|
||||
export * from "./ViewModelSubscriptions";
|
||||
export type * from "./ViewModel";
|
||||
export * from "./MockViewModel";
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { Disposables } from "../../../src/viewmodels/base/Disposables";
|
||||
import { Disposables } from "..";
|
||||
|
||||
describe("Disposable", () => {
|
||||
it("isDisposed is true after dispose() is called", () => {
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Snapshot } from "../../../src/viewmodels/base/Snapshot";
|
||||
import { Snapshot } from "..";
|
||||
|
||||
interface TestSnapshot {
|
||||
key1: string;
|
||||
@@ -2333,6 +2333,17 @@ core-util-is@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
counterpart@^0.18.6:
|
||||
version "0.18.6"
|
||||
resolved "https://registry.yarnpkg.com/counterpart/-/counterpart-0.18.6.tgz#cf6b60d8ef99a4b44b8bf6445fa99b4bd1b2f9dd"
|
||||
integrity sha512-cAIDAYbC3x8S2DDbvFEJ4TzPtPYXma25/kfAkfmprNLlkPWeX4SdUp1c2xklfphqCU3HnDaivR4R3BrAYf5OMA==
|
||||
dependencies:
|
||||
date-names "^0.1.11"
|
||||
except "^0.1.3"
|
||||
extend "^3.0.0"
|
||||
pluralizers "^0.1.7"
|
||||
sprintf-js "^1.0.3"
|
||||
|
||||
create-ecdh@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
|
||||
@@ -2422,6 +2433,11 @@ cwd@^0.10.0:
|
||||
find-pkg "^0.1.2"
|
||||
fs-exists-sync "^0.1.0"
|
||||
|
||||
date-names@^0.1.11:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/date-names/-/date-names-0.1.13.tgz#c4358f6f77c8056e2f5ea68fdbb05f0bf1e53bd0"
|
||||
integrity sha512-IxxoeD9tdx8pXVcmqaRlPvrXIsSrSrIZzfzlOkm9u+hyzKp5Wk/odt9O/gd7Ockzy8n/WHeEpTVJ2bF3mMV4LA==
|
||||
|
||||
de-indent@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||
@@ -2870,6 +2886,13 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
|
||||
md5.js "^1.3.4"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
except@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/except/-/except-0.1.3.tgz#98261c91958551536b44482238e9783fb73d292a"
|
||||
integrity sha512-ouwgJavvMOTOfy0RE8NGQFAIoWh8ehJhkuxDyXxngMVTxTq7HGE7gZopZhqKFnu5lZLI+qQdtvJ8n03ehp7RJg==
|
||||
dependencies:
|
||||
indexof "0.0.1"
|
||||
|
||||
execa@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||
@@ -2918,6 +2941,11 @@ exsolve@^1.0.7:
|
||||
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e"
|
||||
integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==
|
||||
|
||||
extend@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
@@ -3449,6 +3477,11 @@ indent-string@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
|
||||
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
|
||||
|
||||
indexof@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
||||
integrity sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
@@ -4994,6 +5027,11 @@ playwright@^1.14.0:
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
pluralizers@^0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/pluralizers/-/pluralizers-0.1.7.tgz#8d38dd0a1b660e739b10ab2eab10b684c9d50142"
|
||||
integrity sha512-mw6AejUiCaMQ6uPN9ObjJDTnR5AnBSmnHHy3uVTbxrSFSxO5scfwpTs8Dxyb6T2v7GSulhvOq+pm9y+hXUvtOA==
|
||||
|
||||
pngjs@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
||||
@@ -5535,6 +5573,11 @@ spawnd@^5.0.0:
|
||||
tree-kill "^1.2.2"
|
||||
wait-port "^0.2.9"
|
||||
|
||||
sprintf-js@^1.0.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
|
||||
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
|
||||
@@ -92,7 +92,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
css: `
|
||||
/* The timestamp is of inconsistent width depending on the time the test runs at */
|
||||
.mx_MessageTimestamp {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
}
|
||||
/* The MAB showing up on hover is not needed for the test */
|
||||
.mx_MessageActionBar {
|
||||
|
||||
@@ -124,11 +124,12 @@ test.describe("HTML Export", () => {
|
||||
const zip = await extractZipFileToPath(zipPath, dirPath);
|
||||
await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`);
|
||||
await expect(page).toMatchScreenshot("html-export.png", {
|
||||
mask: [
|
||||
// We need to mask the whole thing because the width of the time part changes
|
||||
page.locator(".mx_TimelineSeparator"),
|
||||
page.locator(".mx_MessageTimestamp"),
|
||||
],
|
||||
mask: [page.locator(".mx_TimelineSeparator")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -138,7 +138,11 @@ test.describe("Editing", () => {
|
||||
|
||||
// Take a snapshot of the dialog
|
||||
await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
@@ -59,9 +59,11 @@ async function editMessage(page: Page, message: Locator, newMsg: string): Promis
|
||||
}
|
||||
|
||||
const screenshotOptions = (page?: Page) => ({
|
||||
mask: page ? [page.locator(".mx_MessageTimestamp")] : undefined,
|
||||
// Hide the jump to bottom button in the timeline to avoid flakiness
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_JumpToBottomButton {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ import fs from "node:fs";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
const screenshotOptions = (page: Page) => ({
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
// Hide the jump to bottom button in the timeline to avoid flakiness
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_JumpToBottomButton {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -93,10 +93,12 @@ test.describe("permalinks", () => {
|
||||
getPill(timeline, danielleId);
|
||||
|
||||
await expect(timeline).toMatchScreenshot("permalink-rendering.png", {
|
||||
mask: [
|
||||
// Exclude timestamps from the snapshot, for consistency.
|
||||
page.locator(".mx_MessageTimestamp"),
|
||||
],
|
||||
// Exclude timestamps from the snapshot, for consistency.
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,7 +121,11 @@ test.describe("Polls", () => {
|
||||
.filter({ hasText: pollParams.title })
|
||||
.getAttribute("data-scroll-tokens");
|
||||
await expect(getPollTile(page, pollId)).toMatchScreenshot("Polls_Timeline_tile_no_votes.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
// Bot votes 'Maybe' in the poll
|
||||
@@ -293,7 +297,11 @@ test.describe("Polls", () => {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_a_poll_on_bubble_layout.png",
|
||||
{
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -303,7 +311,11 @@ test.describe("Polls", () => {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_a_poll_on_group_layout.png",
|
||||
{
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -126,7 +126,12 @@ test.describe("FilePanel", () => {
|
||||
// Take a snapshot of file tiles list on FilePanel
|
||||
await expect(filePanelMessageList).toMatchScreenshot("file-tiles-list.png", {
|
||||
// Exclude timestamps & flaky seek bar from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp"), page.getByTestId("audio-player-seek")],
|
||||
mask: [page.getByTestId("audio-player-seek")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -31,8 +31,13 @@ test.describe("Preferences user settings tab", () => {
|
||||
// Assert that the top heading is rendered
|
||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", {
|
||||
// masked due to daylight saving time
|
||||
// masked with fixed-width due to daylight saving time making the text content vary
|
||||
mask: [tab.locator("#mx_dropdownUserTimezone_value")],
|
||||
css: `
|
||||
#mx_dropdownUserTimezone_value {
|
||||
width: 200px;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -361,7 +361,9 @@ test.describe("Sliding Sync", () => {
|
||||
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
|
||||
|
||||
// now click on the permalink for Permalink me
|
||||
await page.locator(".mx_EventTile").filter({ hasText: "Permalink me" }).locator("a").dispatchEvent("click");
|
||||
const tile = page.locator(".mx_EventTile").filter({ hasText: "Permalink me" });
|
||||
await tile.hover();
|
||||
await tile.locator("a").dispatchEvent("click");
|
||||
|
||||
// make sure it is now selected with the little green |
|
||||
await expect(page.locator(".mx_EventTile_selected").filter({ hasText: "Permalink me" })).toBeVisible();
|
||||
|
||||
@@ -39,7 +39,12 @@ test.describe("Threads", () => {
|
||||
|
||||
const ThreadViewGroupSpacingStart = "56px"; // --ThreadView_group_spacing-start
|
||||
// Exclude timestamp and read marker from snapshots
|
||||
const mask = [page.locator(".mx_MessageTimestamp"), page.locator(".mx_MessagePanel_myReadMarker")];
|
||||
const mask = [page.locator(".mx_MessagePanel_myReadMarker")];
|
||||
const css = `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const roomViewLocator = page.locator(".mx_RoomView_body");
|
||||
// User sends message
|
||||
@@ -74,13 +79,15 @@ test.describe("Threads", () => {
|
||||
|
||||
// Take snapshots in group layout and bubble layout (IRC layout is not available on ThreadView)
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Initial_ThreadView_on_group_layout.png", {
|
||||
mask: mask,
|
||||
mask,
|
||||
css,
|
||||
});
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toHaveCount(2);
|
||||
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Initial_ThreadView_on_bubble_layout.png", {
|
||||
mask: mask,
|
||||
mask,
|
||||
css,
|
||||
});
|
||||
|
||||
// Set the group layout
|
||||
@@ -154,7 +161,8 @@ test.describe("Threads", () => {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png",
|
||||
{
|
||||
mask: mask,
|
||||
mask,
|
||||
css,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -178,7 +186,8 @@ test.describe("Threads", () => {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png",
|
||||
{
|
||||
mask: mask,
|
||||
mask,
|
||||
css,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -214,7 +223,8 @@ test.describe("Threads", () => {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_redacted_messages_on_group_layout.png",
|
||||
{
|
||||
mask: mask,
|
||||
mask,
|
||||
css,
|
||||
},
|
||||
);
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
@@ -222,7 +232,8 @@ test.describe("Threads", () => {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_redacted_messages_on_bubble_layout.png",
|
||||
{
|
||||
mask: mask,
|
||||
mask,
|
||||
css,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -445,7 +456,7 @@ test.describe("Threads", () => {
|
||||
await expect(locator.locator(".mx_EventTile").last().getByText("Hello Mr. User")).toBeAttached();
|
||||
});
|
||||
|
||||
test("navigate through right panel", async ({ page, app, user }) => {
|
||||
test("navigate through right panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
// Create room
|
||||
const roomId = await app.client.createRoom({});
|
||||
await page.goto("/#/room/" + roomId);
|
||||
@@ -497,6 +508,9 @@ test.describe("Threads", () => {
|
||||
await expect(
|
||||
threadPanel.locator(".mx_EventTile_last").getByText("Hello again Mr. User in a thread"),
|
||||
).toBeVisible();
|
||||
await expect(threadPanel).toMatchScreenshot("thread-panel.png", {
|
||||
css: ".mx_MessageTimestamp { visibility: hidden !important; }",
|
||||
});
|
||||
|
||||
const rightPanel = page.locator(".mx_RightPanel");
|
||||
// Check that the threads are listed
|
||||
|
||||
@@ -181,8 +181,10 @@ test.describe("Timeline", () => {
|
||||
await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible();
|
||||
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -215,8 +217,10 @@ test.describe("Timeline", () => {
|
||||
await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible();
|
||||
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -255,7 +259,11 @@ test.describe("Timeline", () => {
|
||||
// Save snapshot of expanded generic event list summary on bubble layout
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", {
|
||||
// Exclude timestamp from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
// Click "collapse" link button on the first hovered info event line
|
||||
@@ -271,7 +279,11 @@ test.describe("Timeline", () => {
|
||||
|
||||
// Save snapshot of collapsed generic event list summary on bubble layout
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -312,12 +324,14 @@ test.describe("Timeline", () => {
|
||||
"event-line-inline-start-margin-irc-layout.png",
|
||||
{
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
},
|
||||
);
|
||||
await expect(axe).toHaveNoViolations();
|
||||
@@ -409,7 +423,11 @@ test.describe("Timeline", () => {
|
||||
"collapsed-gels-and-messages-irc-layout.png",
|
||||
{
|
||||
// Exclude timestamp from snapshot of mx_MainSplit
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -428,7 +446,11 @@ test.describe("Timeline", () => {
|
||||
"expanded-gels-and-messages-irc-layout.png",
|
||||
{
|
||||
// Exclude timestamp from snapshot of mx_MainSplit
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -453,7 +475,11 @@ test.describe("Timeline", () => {
|
||||
"expanded-gels-redaction-placeholder.png",
|
||||
{
|
||||
// Exclude timestamp from snapshot of mx_MainSplit
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -481,7 +507,11 @@ test.describe("Timeline", () => {
|
||||
// Record alignment of expanded GELS, placeholder of deleted message, and emote
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", {
|
||||
// Exclude timestamp from snapshot of mx_MainSplit
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -492,12 +522,14 @@ test.describe("Timeline", () => {
|
||||
async ({ page, app, room }) => {
|
||||
const screenshotOptions = {
|
||||
// Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
await sendEvent(app.client, room.roomId);
|
||||
@@ -605,12 +637,10 @@ test.describe("Timeline", () => {
|
||||
await messageEdit(page);
|
||||
|
||||
// Click timestamp to highlight hidden event line
|
||||
const timestamp = page.locator(".mx_RoomView_body .mx_EventTile_info a", {
|
||||
has: page.locator(".mx_MessageTimestamp"),
|
||||
});
|
||||
const timestamp = page.locator(".mx_RoomView_body .mx_EventTile_info a.mx_MessageTimestamp");
|
||||
// wait for the remote echo otherwise we get an error modal due to a 404 on the /event/ API
|
||||
await expect(timestamp).not.toHaveAttribute("href", /~!/);
|
||||
await timestamp.locator(".mx_MessageTimestamp").click();
|
||||
await timestamp.click();
|
||||
|
||||
// should not add inline start padding to a hidden event line on IRC layout
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
@@ -620,12 +650,14 @@ test.describe("Timeline", () => {
|
||||
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
const screenshotOptions = {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
|
||||
@@ -654,7 +686,11 @@ test.describe("Timeline", () => {
|
||||
|
||||
// Exclude timestamp from snapshot
|
||||
const screenshotOptions = {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
await sendEvent(app.client, room.roomId);
|
||||
@@ -795,8 +831,10 @@ test.describe("Timeline", () => {
|
||||
await app.timeline.scrollToBottom();
|
||||
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -892,7 +930,13 @@ test.describe("Timeline", () => {
|
||||
|
||||
const tile = page.locator(".mx_EventTile");
|
||||
await expect(tile).toBeVisible();
|
||||
await expect(tile).toMatchScreenshot("code-block.png", { mask: [page.locator(".mx_MessageTimestamp")] });
|
||||
await expect(tile).toMatchScreenshot("code-block.png", {
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
// Edit a code block and assert the edited code block has been correctly rendered
|
||||
await tile.hover();
|
||||
@@ -904,7 +948,11 @@ test.describe("Timeline", () => {
|
||||
|
||||
const newTile = page.locator(".mx_EventTile");
|
||||
await expect(newTile).toMatchScreenshot("edited-code-block.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1107,8 +1155,10 @@ test.describe("Timeline", () => {
|
||||
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
const screenshotOptions = {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -1233,12 +1283,14 @@ test.describe("Timeline", () => {
|
||||
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
const screenshotOptions = {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
// Make sure the strings do not overflow on IRC layout
|
||||
@@ -1297,8 +1349,10 @@ test.describe("Timeline", () => {
|
||||
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
const screenshotOptions = {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
visibility: hidden;
|
||||
}
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ export class ElementAppPage {
|
||||
*/
|
||||
public async inviteUserToCurrentRoom(userId: string): Promise<void> {
|
||||
await this.toggleRoomInfoPanel(); // TODO skip this if the room info panel is already open
|
||||
await this.page.getByLabel("Right panel").getByRole("menuitem", { name: "Invite" }).click();
|
||||
await this.page.getByTestId("right-panel").getByRole("menuitem", { name: "Invite" }).click();
|
||||
|
||||
const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input");
|
||||
await input.fill(userId);
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |