Merge branch 'develop' into midhun/fix-spotlight-1

pull/28452/head
Michael Telatynski 2024-11-13 15:38:56 +00:00 committed by GitHub
commit ec96d33ed7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
215 changed files with 3574 additions and 2704 deletions

View File

@ -1,60 +0,0 @@
module.exports = {
plugins: ["matrix-org"],
extends: ["./.eslintrc.js"],
parserOptions: {
project: ["./tsconfig.module_system.json"],
},
overrides: [
{
files: ["module_system/**/*.{ts,tsx}"],
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell.
"no-restricted-imports": [
"error",
{
paths: [
{
name: "matrix-js-sdk",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src/",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src/index",
message: "Please use matrix-js-sdk/src/matrix instead",
},
],
patterns: [
{
group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"],
message: "Please use matrix-js-sdk/src/* instead",
},
],
},
],
},
},
],
};

View File

@ -270,6 +270,60 @@ module.exports = {
"react-hooks/rules-of-hooks": ["off"], "react-hooks/rules-of-hooks": ["off"],
}, },
}, },
{
files: ["module_system/**/*.{ts,tsx}"],
parserOptions: {
project: ["./tsconfig.module_system.json"],
},
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell.
"no-restricted-imports": [
"error",
{
paths: [
{
name: "matrix-js-sdk",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src/",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src/index",
message: "Please use matrix-js-sdk/src/matrix instead",
},
],
patterns: [
{
group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"],
message: "Please use matrix-js-sdk/src/* instead",
},
],
},
],
},
},
], ],
settings: { settings: {
react: { react: {

View File

@ -21,13 +21,13 @@ jobs:
fetch-depth: 0 # needed for docker-package to be able to calculate the version fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3 uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
with: with:
install: true install: true

View File

@ -15,7 +15,7 @@ jobs:
report: report:
if: github.event.workflow_run.conclusion != 'cancelled' if: github.event.workflow_run.conclusion != 'cancelled'
name: Report results name: Report results
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
environment: Netlify environment: Netlify
permissions: permissions:
statuses: write statuses: write

View File

@ -36,7 +36,7 @@ env:
jobs: jobs:
build: build:
name: "Build Element-Web" name: "Build Element-Web"
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: inputs.skip != true if: inputs.skip != true
steps: steps:
- name: Checkout code - name: Checkout code
@ -144,7 +144,7 @@ jobs:
name: end-to-end-tests name: end-to-end-tests
needs: playwright needs: playwright
if: always() if: always()
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
if: inputs.skip != true if: inputs.skip != true

View File

@ -9,7 +9,7 @@ on:
jobs: jobs:
deploy: deploy:
if: github.event.workflow_run.conclusion != 'cancelled' && github.event.workflow_run.event == 'pull_request' if: github.event.workflow_run.conclusion != 'cancelled' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
environment: Netlify environment: Netlify
steps: steps:
- name: 📝 Create Deployment - name: 📝 Create Deployment

View File

@ -5,7 +5,7 @@ on:
- cron: "0 6 * * *" # Every day at 6am UTC - cron: "0 6 * * *" # Every day at 6am UTC
jobs: jobs:
update: update:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@ -5,7 +5,7 @@ on:
jobs: jobs:
check_base_branch: check_base_branch:
name: Check PR base branch name: Check PR base branch
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/github-script@v7 - uses: actions/github-script@v7
with: with:

View File

@ -50,7 +50,7 @@ jobs:
rethemendex_lint: rethemendex_lint:
name: "Rethemendex Check" name: "Rethemendex Check"
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -123,6 +123,12 @@ jobs:
cache: "yarn" cache: "yarn"
node-version: "lts/*" node-version: "lts/*"
- name: Install Deps
run: "yarn install --frozen-lockfile"
- name: Run linter
run: "yarn run lint:knip"
- name: Install Deps - name: Install Deps
run: "scripts/layered.sh" run: "scripts/layered.sh"

View File

@ -29,7 +29,7 @@ env:
jobs: jobs:
jest: jest:
name: Jest name: Jest
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -93,7 +93,7 @@ jobs:
name: jest-tests name: jest-tests
needs: jest needs: jest
if: always() if: always()
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success' - if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1 run: exit 1

View File

@ -2,6 +2,6 @@
"*": "prettier --write", "*": "prettier --write",
"src/**/*.(ts|tsx)": ["eslint --fix"], "src/**/*.(ts|tsx)": ["eslint --fix"],
"scripts/**/*.(ts|tsx)": ["eslint --fix"], "scripts/**/*.(ts|tsx)": ["eslint --fix"],
"module_system/**/*.(ts|tsx)": ["eslint --fix --config .eslintrc-module_system.js module_system"], "module_system/**/*.(ts|tsx)": ["eslint --fix"],
"*.pcss": ["stylelint --fix"] "*.pcss": ["stylelint --fix"]
} }

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
extends: ["stylelint-config-standard"], extends: ["stylelint-config-standard"],
customSyntax: require("postcss-scss"), customSyntax: "postcss-scss",
plugins: ["stylelint-scss"], plugins: ["stylelint-scss"],
rules: { rules: {
"comment-empty-line-before": null, "comment-empty-line-before": null,

View File

@ -1,3 +1,11 @@
Changes in [1.11.85](https://github.com/element-hq/element-web/releases/tag/v1.11.85) (2024-11-12)
==================================================================================================
# Security
- Fixes for [CVE-2024-51750](https://www.cve.org/CVERecord?id=CVE-2024-51750) / [GHSA-w36j-v56h-q9pc](https://github.com/element-hq/element-web/security/advisories/GHSA-w36j-v56h-q9pc)
- Fixes for [CVE-2024-51749](https://www.cve.org/CVERecord?id=CVE-2024-51749) / [GHSA-5486-384g-mcx2](https://github.com/element-hq/element-web/security/advisories/GHSA-5486-384g-mcx2)
- Update JS SDK with the fixes for [CVE-2024-50336](https://www.cve.org/CVERecord?id=CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr)
Changes in [1.11.84](https://github.com/element-hq/element-web/releases/tag/v1.11.84) (2024-11-05) Changes in [1.11.84](https://github.com/element-hq/element-web/releases/tag/v1.11.84) (2024-11-05)
================================================================================================== ==================================================================================================
## ✨ Features ## ✨ Features

View File

@ -38,7 +38,7 @@ const config: Config = {
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js", "recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock", "^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
}, },
transformIgnorePatterns: ["/node_modules/(?!matrix-js-sdk).+$"], transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
collectCoverageFrom: [ collectCoverageFrom: [
"<rootDir>/src/**/*.{js,ts,tsx}", "<rootDir>/src/**/*.{js,ts,tsx}",
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is // getSessionLock is piped into a different JS context via stringification, and the coverage functionality is

53
knip.ts Normal file
View File

@ -0,0 +1,53 @@
import { KnipConfig } from "knip";
export default {
entry: [
"src/vector/index.ts",
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
"scripts/**",
"playwright/**",
"test/**",
"res/decoder-ring/**",
],
project: ["**/*.{js,ts,jsx,tsx}"],
ignore: [
"docs/**",
"res/jitsi_external_api.min.js",
// Used by jest
"__mocks__/maplibre-gl.js",
// Keep for now
"src/hooks/useLocalStorageState.ts",
"src/components/views/elements/InfoTooltip.tsx",
"src/components/views/elements/StyledCheckbox.tsx",
],
ignoreDependencies: [
// Required for `action-validator`
"@action-validator/*",
// Used for git pre-commit hooks
"husky",
// Used by jest
"babel-jest",
// Used by babel
"@babel/runtime",
"@babel/plugin-transform-class-properties",
// Referenced in PCSS
"github-markdown-css",
// False positive
"sw.js",
// Used by webpack
"buffer",
"process",
"util",
// Used by workflows
"ts-prune",
// Required due to bug in bloom-filters https://github.com/Callidon/bloom-filters/issues/75
"@types/seedrandom",
],
ignoreBinaries: [
// Used in scripts & workflows
"jq",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;

View File

@ -1,6 +1,6 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.84", "version": "1.11.85",
"description": "A feature-rich client for Matrix.org", "description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
@ -35,7 +35,7 @@
"i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null", "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", "i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && yarn i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"make-component": "node scripts/make-react-component.js", "make-component": "node scripts/make-react-component.js",
"rethemendex": "res/css/rethemendex.sh", "rethemendex": "./res/css/rethemendex.sh",
"clean": "rimraf lib webapp", "clean": "rimraf lib webapp",
"build": "yarn clean && yarn build:genfiles && yarn build:bundle", "build": "yarn clean && yarn build:genfiles && yarn build:bundle",
"build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats", "build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
@ -45,23 +45,20 @@
"build:bundle": "webpack --progress --mode production", "build:bundle": "webpack --progress --mode production",
"build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json", "build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json",
"build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts", "build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts",
"dist": "scripts/package.sh", "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": "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:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --server-type https\"",
"start:res": "ts-node scripts/copy-res.ts -w", "start:res": "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", "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": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows",
"lint:js": "yarn lint:js:src && yarn lint:js:module_system", "lint:js": "eslint --max-warnings 0 src test playwright module_system && prettier --check .",
"lint:js:src": "eslint --max-warnings 0 src test playwright && prettier --check .", "lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src test playwright module_system",
"lint:js:module_system": "eslint --max-warnings 0 --config .eslintrc-module_system.js module_system",
"lint:js-fix": "yarn lint:js-fix:src && yarn lint:js-fix:module_system",
"lint:js-fix:src": "prettier --log-level=warn --write . && eslint --fix src test playwright",
"lint:js-fix:module_system": "eslint --fix --config .eslintrc-module_system.js module_system",
"lint:types": "yarn lint:types:src && yarn lint:types:module_system", "lint:types": "yarn lint:types:src && yarn lint:types:module_system",
"lint:types:src": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p playwright", "lint:types:src": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p playwright",
"lint:types:module_system": "tsc --noEmit --project ./tsconfig.module_system.json", "lint:types:module_system": "tsc --noEmit --project ./tsconfig.module_system.json",
"lint:style": "stylelint \"res/css/**/*.pcss\"", "lint:style": "stylelint \"res/css/**/*.pcss\"",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "jest", "test": "jest",
"test:playwright": "playwright test", "test:playwright": "playwright test",
"test:playwright:open": "yarn test:playwright --ui", "test:playwright:open": "yarn test:playwright --ui",
@ -74,10 +71,9 @@
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
}, },
"resolutions": { "resolutions": {
"@types/seedrandom": "3.0.8",
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001668", "caniuse-lite": "1.0.30001679",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
@ -96,7 +92,7 @@
"@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2", "@zxcvbn-ts/language-en": "^3.0.2",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"bloom-filters": "^3.0.1", "bloom-filters": "^3.0.2",
"blurhash": "^2.0.3", "blurhash": "^2.0.3",
"browserslist": "^4.23.2", "browserslist": "^4.23.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
@ -123,19 +119,20 @@
"linkify-string": "4.1.3", "linkify-string": "4.1.3",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maplibre-gl": "^2.0.0", "maplibre-gl": "^4.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.9.0", "matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"mime": "^4.0.4",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "^3.0.1",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",
"posthog-js": "1.157.2", "posthog-js": "1.157.2",
"qrcode": "1.5.4", "qrcode": "1.5.4",
"re-resizable": "6.9.17", "re-resizable": "6.10.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.3.0", "react-blurhash": "^0.3.0",
@ -155,11 +152,9 @@
"@action-validator/cli": "^0.6.0", "@action-validator/cli": "^0.6.0",
"@action-validator/core": "^0.6.0", "@action-validator/core": "^0.6.0",
"@axe-core/playwright": "^4.8.1", "@axe-core/playwright": "^4.8.1",
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10", "@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10", "@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10", "@babel/eslint-plugin": "^7.12.10",
"@babel/parser": "^7.12.11",
"@babel/plugin-proposal-export-default-from": "^7.12.1", "@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-class-properties": "^7.12.1", "@babel/plugin-transform-class-properties": "^7.12.1",
@ -172,7 +167,6 @@
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10", "@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7", "@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7", "@casualbot/jest-sonar-reporter": "2.2.7",
"@peculiar/webcrypto": "^1.4.3", "@peculiar/webcrypto": "^1.4.3",
@ -186,7 +180,6 @@
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@types/commonmark": "^0.27.4", "@types/commonmark": "^0.27.4",
"@types/content-type": "^1.1.5",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/css-tree": "^2.3.8", "@types/css-tree": "^2.3.8",
"@types/diff-match-patch": "^1.0.32", "@types/diff-match-patch": "^1.0.32",
@ -211,7 +204,6 @@
"@types/react-dom": "18.3.1", "@types/react-dom": "18.3.1",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.13.0", "@types/sanitize-html": "2.13.0",
"@types/sdp-transform": "^2.4.6",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5", "@types/tar-js": "^0.3.5",
@ -219,7 +211,6 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"axe-core": "4.10.2",
"babel-jest": "^29.0.0", "babel-jest": "^29.0.0",
"babel-loader": "^9.0.0", "babel-loader": "^9.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0", "babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@ -259,14 +250,12 @@
"jest-mock": "^29.6.2", "jest-mock": "^29.6.2",
"jest-raw-loader": "^1.0.1", "jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"knip": "^5.36.2",
"lint-staged": "^15.0.2", "lint-staged": "^15.0.2",
"mailhog": "^4.16.0", "mailhog": "^4.16.0",
"matrix-mock-request": "^2.5.0",
"matrix-web-i18n": "^3.2.1", "matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.0", "mini-css-extract-plugin": "2.9.0",
"minimist": "^1.2.6", "minimist": "^1.2.6",
"mkdirp": "^3.0.0",
"mocha-junit-reporter": "^2.2.0",
"modernizr": "^3.12.0", "modernizr": "^3.12.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"playwright-core": "^1.45.1", "playwright-core": "^1.45.1",
@ -276,7 +265,7 @@
"postcss-import": "16.1.0", "postcss-import": "16.1.0",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"postcss-mixins": "^11.0.0", "postcss-mixins": "^11.0.0",
"postcss-nested": "^6.0.0", "postcss-nested": "^7.0.0",
"postcss-preset-env": "^10.0.0", "postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",

View File

@ -357,9 +357,9 @@ test.describe("Threads", () => {
await bot.joinRoom(roomId); await bot.joinRoom(roomId);
await page.goto("/#/room/" + roomId); await page.goto("/#/room/" + roomId);
// Exclude timestamp, read marker, and mapboxgl-map from snapshots // Exclude timestamp, read marker, and maplibregl-map from snapshots
const css = const css =
".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .mapboxgl-map { visibility: hidden !important; }"; ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }";
let locator = page.locator(".mx_RoomView_body"); let locator = page.locator(".mx_RoomView_body");
// User sends message // User sends message

View File

@ -6,32 +6,48 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import * as fs from "node:fs";
import type { Page } from "@playwright/test"; import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { Credentials } from "../../plugins/homeserver";
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker"; const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers"; const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
const STICKER_NAME = "Test Sticker"; const STICKER_NAME = "Test Sticker";
const ROOM_NAME_1 = "Sticker Test"; const ROOM_NAME_1 = "Sticker Test";
const ROOM_NAME_2 = "Sticker Test Two"; const ROOM_NAME_2 = "Sticker Test Two";
const STICKER_MESSAGE = JSON.stringify({ const STICKER_IMAGE = fs.readFileSync("playwright/sample-files/riot.png");
action: "m.sticker",
api: "fromWidget", function getStickerMessage(contentUri: string, mimetype: string): string {
data: { return JSON.stringify({
name: "teststicker", action: "m.sticker",
description: STICKER_NAME, api: "fromWidget",
file: "test.png", data: {
content: { name: "teststicker",
body: STICKER_NAME, description: STICKER_NAME,
msgtype: "m.sticker", file: "test.png",
url: "mxc://localhost/somewhere", content: {
body: STICKER_NAME,
info: {
h: 480,
mimetype: mimetype,
size: 13818,
w: 480,
},
msgtype: "m.sticker",
url: contentUri,
},
}, },
}, requestId: "1",
requestId: "1", widgetId: STICKER_PICKER_WIDGET_ID,
widgetId: STICKER_PICKER_WIDGET_ID, });
}); }
const WIDGET_HTML = `
function getWidgetHtml(contentUri: string, mimetype: string) {
const stickerMessage = getStickerMessage(contentUri, mimetype);
return `
<html lang="en"> <html lang="en">
<head> <head>
<title>Fake Sticker Picker</title> <title>Fake Sticker Picker</title>
@ -51,13 +67,13 @@ const WIDGET_HTML = `
<button name="Send" id="sendsticker">Press for sticker</button> <button name="Send" id="sendsticker">Press for sticker</button>
<script> <script>
document.getElementById('sendsticker').onclick = () => { document.getElementById('sendsticker').onclick = () => {
window.parent.postMessage(${STICKER_MESSAGE}, '*') window.parent.postMessage(${stickerMessage}, '*')
}; };
</script> </script>
</body> </body>
</html> </html>
`; `;
}
async function openStickerPicker(app: ElementAppPage) { async function openStickerPicker(app: ElementAppPage) {
const options = await app.openMessageComposerOptions(); const options = await app.openMessageComposerOptions();
await options.getByRole("menuitem", { name: "Sticker" }).click(); await options.getByRole("menuitem", { name: "Sticker" }).click();
@ -71,7 +87,8 @@ async function sendStickerFromPicker(page: Page) {
await expect(page.locator(".mx_AppTileFullWidth#stickers")).not.toBeVisible(); await expect(page.locator(".mx_AppTileFullWidth#stickers")).not.toBeVisible();
} }
async function expectTimelineSticker(page: Page, roomId: string) { async function expectTimelineSticker(page: Page, roomId: string, contentUri: string) {
const contentId = contentUri.split("/").slice(-1)[0];
// Make sure it's in the right room // Make sure it's in the right room
await expect(page.locator(".mx_EventTile_sticker > a")).toHaveAttribute("href", new RegExp(`/${roomId}/`)); await expect(page.locator(".mx_EventTile_sticker > a")).toHaveAttribute("href", new RegExp(`/${roomId}/`));
@ -80,13 +97,43 @@ async function expectTimelineSticker(page: Page, roomId: string) {
// download URL. // download URL.
await expect(page.locator(`img[alt="${STICKER_NAME}"]`)).toHaveAttribute( await expect(page.locator(`img[alt="${STICKER_NAME}"]`)).toHaveAttribute(
"src", "src",
new RegExp("/download/localhost/somewhere"), new RegExp(`/localhost/${contentId}`),
); );
} }
async function expectFileTile(page: Page, roomId: string, contentUri: string) {
await expect(page.locator(".mx_MFileBody_info_filename")).toContainText(STICKER_NAME);
}
async function setWidgetAccountData(
app: ElementAppPage,
user: Credentials,
stickerPickerUrl: string,
provideCreatorUserId: boolean = true,
) {
await app.client.setAccountData("m.widgets", {
[STICKER_PICKER_WIDGET_ID]: {
content: {
type: "m.stickerpicker",
name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl,
creatorUserId: provideCreatorUserId ? user.userId : undefined,
},
sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
},
});
}
test.describe("Stickers", () => { test.describe("Stickers", () => {
test.use({ test.use({
displayName: "Sally", displayName: "Sally",
room: async ({ app }, use) => {
const roomId = await app.client.createRoom({ name: ROOM_NAME_1 });
await use({ roomId });
},
}); });
// We spin up a web server for the sticker picker so that we're not testing to see if // We spin up a web server for the sticker picker so that we're not testing to see if
@ -96,34 +143,19 @@ test.describe("Stickers", () => {
// //
// See sendStickerFromPicker() for more detail on iframe comms. // See sendStickerFromPicker() for more detail on iframe comms.
let stickerPickerUrl: string; let stickerPickerUrl: string;
test.beforeEach(async ({ webserver }) => {
stickerPickerUrl = webserver.start(WIDGET_HTML);
});
test("should send a sticker to multiple rooms", async ({ page, app, user }) => { test("should send a sticker to multiple rooms", async ({ webserver, page, app, user, room }) => {
const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 });
const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 }); const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 });
const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" });
await app.client.setAccountData("m.widgets", { const widgetHtml = getWidgetHtml(contentUri, "image/png");
[STICKER_PICKER_WIDGET_ID]: { stickerPickerUrl = webserver.start(widgetHtml);
content: { setWidgetAccountData(app, user, stickerPickerUrl);
type: "m.stickerpicker",
name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl,
creatorUserId: user.userId,
},
sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
},
});
await app.viewRoomByName(ROOM_NAME_1); await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${roomId1}`); await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app); await openStickerPicker(app);
await sendStickerFromPicker(page); await sendStickerFromPicker(page);
await expectTimelineSticker(page, roomId1); await expectTimelineSticker(page, room.roomId, contentUri);
// Ensure that when we switch to a different room that the sticker // Ensure that when we switch to a different room that the sticker
// goes to the right place // goes to the right place
@ -131,31 +163,40 @@ test.describe("Stickers", () => {
await expect(page).toHaveURL(`/#/room/${roomId2}`); await expect(page).toHaveURL(`/#/room/${roomId2}`);
await openStickerPicker(app); await openStickerPicker(app);
await sendStickerFromPicker(page); await sendStickerFromPicker(page);
await expectTimelineSticker(page, roomId2); await expectTimelineSticker(page, roomId2, contentUri);
}); });
test("should handle a sticker picker widget missing creatorUserId", async ({ page, app, user }) => { test("should handle a sticker picker widget missing creatorUserId", async ({
const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 }); webserver,
page,
await app.client.setAccountData("m.widgets", { app,
[STICKER_PICKER_WIDGET_ID]: { user,
content: { room,
type: "m.stickerpicker", }) => {
name: STICKER_PICKER_WIDGET_NAME, const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" });
url: stickerPickerUrl, const widgetHtml = getWidgetHtml(contentUri, "image/png");
// No creatorUserId stickerPickerUrl = webserver.start(widgetHtml);
}, setWidgetAccountData(app, user, stickerPickerUrl, false);
sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
},
});
await app.viewRoomByName(ROOM_NAME_1); await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${roomId1}`); await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app); await openStickerPicker(app);
await sendStickerFromPicker(page); await sendStickerFromPicker(page);
await expectTimelineSticker(page, roomId1); await expectTimelineSticker(page, room.roomId, contentUri);
});
test("should render invalid mimetype as a file", async ({ webserver, page, app, user, room }) => {
const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, {
type: "application/octet-stream",
});
const widgetHtml = getWidgetHtml(contentUri, "application/octet-stream");
stickerPickerUrl = webserver.start(widgetHtml);
setWidgetAccountData(app, user, stickerPickerUrl);
await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app);
await sendStickerFromPicker(page);
await expectFileTile(page, room.roomId, contentUri);
}); });
}); });

View File

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI. // We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically. // This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:6c33604ee62f009f3b34454a3c3e85f7e3ff5de63e45011fcd79e0ddc54a4e51"; const DOCKER_TAG = "develop@sha256:b7d8089c4593d4aa12834d04849971717b17254a76257e7c5cd433a16d5e966e";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

View File

@ -102,3 +102,5 @@ experimental_features:
# messages > non-joined historical messages. # messages > non-joined historical messages.
# Can be removed after Synapse enables it by default # Can be removed after Synapse enables it by default
msc4115_membership_on_events: true msc4115_membership_on_events: true
enable_authenticated_media: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -186,7 +186,7 @@ input[type="search"].mx_textinput_icon {
/* FIXME THEME - Tint by CSS rather than referencing a duplicate asset */ /* FIXME THEME - Tint by CSS rather than referencing a duplicate asset */
input[type="text"].mx_textinput_icon.mx_textinput_search, input[type="text"].mx_textinput_icon.mx_textinput_search,
input[type="search"].mx_textinput_icon.mx_textinput_search { input[type="search"].mx_textinput_icon.mx_textinput_search {
background-image: url("$(res)/img/feather-customised/search-input.svg"); background-image: url("@vector-im/compound-design-tokens/icons/search.svg");
} }
/* dont search UI as not all browsers support it, */ /* dont search UI as not all browsers support it, */

View File

@ -32,8 +32,8 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_DeviceExpandDetailsButton_icon { .mx_DeviceExpandDetailsButton_icon {
height: 16px; height: 24px;
width: 16px; width: 24px;
transition: all 0.3s; transition: all 0.3s;
transform: var(--icon-transform); transform: var(--icon-transform);

View File

@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details.
width: 18px; width: 18px;
height: 18px; height: 18px;
background: currentColor; background: currentColor;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
mask-size: 100%; mask-size: 100%;
mask-repeat: no-repeat; mask-repeat: no-repeat;
float: right; float: right;

View File

@ -125,7 +125,7 @@ Please see LICENSE files in the repository root for full details.
padding-left: 34px; /* 28px from above, but +6px to account for the wider icon */ padding-left: 34px; /* 28px from above, but +6px to account for the wider icon */
&::before { &::before {
mask-image: url("$(res)/img/element-icons/retry.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/restart.svg");
} }
} }
} }

View File

@ -62,7 +62,7 @@ Please see LICENSE files in the repository root for full details.
&::before { &::before {
background-color: $info-plinth-fg-color; background-color: $info-plinth-fg-color;
mask: url("$(res)/img/feather-customised/search-input.svg"); mask: url("@vector-im/compound-design-tokens/icons/search.svg");
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 50px; mask-size: 50px;

View File

@ -77,7 +77,7 @@ Please see LICENSE files in the repository root for full details.
height: 16px; height: 16px;
width: 16px; width: 16px;
left: 0; left: 0;
background-image: url("$(res)/img/element-icons/warning-badge.svg"); background-image: url("@vector-im/compound-design-tokens/icons/error.svg");
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
@ -121,7 +121,7 @@ Please see LICENSE files in the repository root for full details.
background-color: $tertiary-content; background-color: $tertiary-content;
mask-size: 16px; mask-size: 16px;
transform: rotate(270deg); transform: rotate(270deg);
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
} }
&.mx_SpaceHierarchy_subspace_toggle_shown::before { &.mx_SpaceHierarchy_subspace_toggle_shown::before {

View File

@ -48,7 +48,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: $background; background-color: $background;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
transform: rotate(270deg); transform: rotate(270deg);
} }
@ -169,7 +169,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: 20px; mask-size: 20px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: $tertiary-content; background-color: $tertiary-content;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
} }
.mx_SpaceButton_icon { .mx_SpaceButton_icon {

View File

@ -29,7 +29,7 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_MessageContextMenu_iconReport::before { .mx_MessageContextMenu_iconReport::before {
mask-image: url("$(res)/img/element-icons/warning-badge.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/error.svg");
} }
.mx_MessageContextMenu_iconLink::before { .mx_MessageContextMenu_iconLink::before {
@ -61,7 +61,7 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_MessageContextMenu_iconResend::before { .mx_MessageContextMenu_iconResend::before {
mask-image: url("$(res)/img/element-icons/retry.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/restart.svg");
} }
.mx_MessageContextMenu_iconSource::before { .mx_MessageContextMenu_iconSource::before {

View File

@ -125,7 +125,7 @@ Please see LICENSE files in the repository root for full details.
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-image: url("$(res)/img/element-icons/retry.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/restart.svg");
width: 18px; width: 18px;
height: 18px; height: 18px;
left: 0; left: 0;

View File

@ -36,9 +36,24 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_AnalyticsLearnMore_bullets li { .mx_AnalyticsLearnMore_bullets li {
background: url("$(res)/img/tick-circle.svg") no-repeat;
list-style-type: none; list-style-type: none;
padding: 2px 0px 20px 32px; padding: 2px 0 0 32px;
margin-bottom: 20px;
vertical-align: middle; vertical-align: middle;
position: relative;
&::before {
content: "";
position: absolute;
width: 26px;
height: 26px;
left: 0;
top: 0;
background-color: #0dbd8b;
mask-image: url("@vector-im/compound-design-tokens/icons/check-circle.svg");
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
} }
} }

View File

@ -21,7 +21,7 @@ Please see LICENSE files in the repository root for full details.
&.mx_AccessSecretStorageDialog_resetBadge::before { &.mx_AccessSecretStorageDialog_resetBadge::before {
/* The image isn't capable of masking, so we use a background instead. */ /* The image isn't capable of masking, so we use a background instead. */
background-image: url("$(res)/img/element-icons/warning-badge.svg"); background-image: url("@vector-im/compound-design-tokens/icons/error.svg");
background-size: 24px; background-size: 24px;
background-color: transparent; background-color: transparent;
} }
@ -120,7 +120,7 @@ Please see LICENSE files in the repository root for full details.
width: 16px; width: 16px;
left: 0; left: 0;
top: 2px; /* alignment */ top: 2px; /* alignment */
background-image: url("$(res)/img/element-icons/warning-badge.svg"); background-image: url("@vector-im/compound-design-tokens/icons/error.svg");
background-size: contain; background-size: contain;
} }

View File

@ -39,11 +39,13 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_Dropdown_arrow { .mx_Dropdown_arrow {
width: 10px; width: 16px;
height: 6px; height: 16px;
padding-right: 9px; margin-right: 4px;
mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center;
mask-size: 18px;
background: $primary-content; background: $primary-content;
} }

View File

@ -51,12 +51,15 @@ Please see LICENSE files in the repository root for full details.
.mx_Field_select::before { .mx_Field_select::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 15px; top: 50%;
right: 10px; transform: translateY(-50%);
width: 10px; right: 4px;
height: 6px; width: 18px;
mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); height: 18px;
mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $primary-content; background-color: $primary-content;
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;

View File

@ -29,5 +29,5 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_InfoTooltip_icon_warning::before { .mx_InfoTooltip_icon_warning::before {
mask-image: url("$(res)/img/element-icons/warning.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/error.svg");
} }

View File

@ -30,6 +30,6 @@ Please see LICENSE files in the repository root for full details.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
background-color: var(--cpd-color-icon-secondary); background-color: var(--cpd-color-icon-secondary);
} }

View File

@ -108,6 +108,10 @@ Please see LICENSE files in the repository root for full details.
color: var(--cpd-color-icon-primary); color: var(--cpd-color-icon-primary);
} }
&.mx_MessageActionBar_retryButton {
--MessageActionBar-icon-size: 16px;
}
&.mx_MessageActionBar_downloadButton { &.mx_MessageActionBar_downloadButton {
--MessageActionBar-icon-size: 14px; --MessageActionBar-icon-size: 14px;

View File

@ -45,7 +45,7 @@ Please see LICENSE files in the repository root for full details.
width: 18px; width: 18px;
height: 18px; height: 18px;
background: currentColor; background: currentColor;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
mask-size: 100%; mask-size: 100%;
mask-repeat: no-repeat; mask-repeat: no-repeat;
float: right; float: right;

View File

@ -26,9 +26,9 @@ Please see LICENSE files in the repository root for full details.
height: 16px; height: 16px;
width: 16px; width: 16px;
padding: 4px; padding: 4px;
mask-image: url("$(res)/img/minimise.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-left.svg");
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 7px center; mask-position: center;
background-color: $header-panel-text-primary-color; background-color: $header-panel-text-primary-color;
} }
} }

View File

@ -31,8 +31,9 @@ Please see LICENSE files in the repository root for full details.
position: absolute; position: absolute;
top: calc(50% - 8px); /* center */ top: calc(50% - 8px); /* center */
right: -8px; right: -8px;
mask: url("$(res)/img/member_chevron.png"); mask: url("@vector-im/compound-design-tokens/icons/chevron-right.svg");
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center;
width: 16px; width: 16px;
height: 16px; height: 16px;
background-color: $header-panel-text-primary-color; background-color: $header-panel-text-primary-color;

View File

@ -18,7 +18,7 @@ Please see LICENSE files in the repository root for full details.
} }
} }
&:hover .mx_LinkPreviewGroup_hide img, &:hover .mx_LinkPreviewGroup_hide svg,
.mx_LinkPreviewGroup_hide:focus-visible:focus svg { .mx_LinkPreviewGroup_hide:focus-visible:focus svg {
visibility: visible; visibility: visible;
} }

View File

@ -42,7 +42,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: $tertiary-content; background-color: $tertiary-content;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
} }
&[aria-expanded="true"] { &[aria-expanded="true"] {

View File

@ -160,7 +160,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: var(--cpd-color-icon-secondary); background-color: var(--cpd-color-icon-secondary);
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
} }
&.mx_RoomSublist_collapseBtn_collapsed::before { &.mx_RoomSublist_collapseBtn_collapsed::before {
@ -276,7 +276,7 @@ Please see LICENSE files in the repository root for full details.
.mx_RoomSublist_showMoreButtonChevron, .mx_RoomSublist_showMoreButtonChevron,
.mx_RoomSublist_showLessButtonChevron { .mx_RoomSublist_showLessButtonChevron {
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
} }
.mx_RoomSublist_showLessButtonChevron { .mx_RoomSublist_showLessButtonChevron {

View File

@ -53,11 +53,11 @@ Please see LICENSE files in the repository root for full details.
content: ""; content: "";
position: absolute; position: absolute;
top: 50%; top: 50%;
right: $spacing-12; right: var(--cpd-space-1x);
transform: translateY(-50%); transform: translateY(-50%);
width: 12px; width: 24px;
height: 12px; height: 24px;
mask-image: url("$(res)/img/compound/chevron-right-12px.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-right.svg");
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;

View File

@ -67,7 +67,7 @@ Please see LICENSE files in the repository root for full details.
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 2px 3px; mask-position: 2px 3px;
mask-size: 24px; mask-size: 24px;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
} }
} }

View File

@ -147,7 +147,7 @@ Please see LICENSE files in the repository root for full details.
&::before { &::before {
content: ""; content: "";
display: inline-block; display: inline-block;
mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
mask-size: 20px; mask-size: 20px;
mask-position: center; mask-position: center;
background-color: $call-primary-content; background-color: $call-primary-content;

View File

@ -1,10 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1692_80)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.96967 10.7197C3.67678 10.4268 3.67601 9.95114 3.96795 9.6573L7.66823 5.933L3.95592 2.22069C3.66303 1.92779 3.66226 1.45215 3.9542 1.15831C4.24615 0.864473 4.72025 0.863706 5.01315 1.1566L9.25579 5.39924C9.54868 5.69213 9.54945 6.16777 9.2575 6.46161L5.02861 10.718C4.73667 11.0118 4.26256 11.0126 3.96967 10.7197Z" fill="#737D8C"/>
</g>
<defs>
<clipPath id="clip0_1692_80">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 629 B

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.02266 2.96455C5.11589 2.10004 6.49866 1.5835 8 1.5835C11.3187 1.5835 14.049 4.10294 14.3825 7.3335H15.6723C15.9336 7.3335 16.0894 7.62498 15.9445 7.8426L13.9388 10.8543C13.8094 11.0488 13.524 11.0488 13.3945 10.8543L11.3888 7.8426C11.2439 7.62498 11.3997 7.3335 11.661 7.3335H12.8719C12.5465 4.93343 10.4893 3.0835 8 3.0835C6.84828 3.0835 5.79092 3.47857 4.95308 4.14112C4.8969 4.18555 4.84851 4.22129 4.81295 4.24673C4.7951 4.2595 4.78032 4.26979 4.7692 4.27743L4.75529 4.28689L4.75051 4.2901L4.74868 4.29132L4.74791 4.29183L4.74756 4.29206L4.74739 4.29217L4.74731 4.29223L4.33341 3.66694L4.74723 4.29228C4.40181 4.52087 3.93648 4.42616 3.70788 4.08073C3.47976 3.736 3.57362 3.27185 3.91734 3.04277L3.92021 3.04081L3.94013 3.02682C3.95912 3.01323 3.988 2.99197 4.02266 2.96455ZM3.12815 8.66683H4.33901C4.60027 8.66683 4.7561 8.37534 4.61118 8.15772L2.60551 5.14598C2.47603 4.95156 2.19064 4.95156 2.06116 5.14598L0.0554881 8.15772C-0.0894338 8.37534 0.0663988 8.66683 0.327661 8.66683H1.61755C1.95103 11.8974 4.68129 14.4168 8 14.4168C9.56831 14.4168 11.0069 13.8532 12.1215 12.9184C12.4388 12.6522 12.4803 12.1791 12.2141 11.8617C11.9479 11.5444 11.4749 11.5029 11.1575 11.7691C10.303 12.4859 9.20281 12.9168 8 12.9168C5.51071 12.9168 3.4535 11.0669 3.12815 8.66683Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none">
<path
fill="currentColor"
fill-rule="evenodd"
d="M4.523 2.964a6.418 6.418 0 0 1 10.36 4.369h1.29c.26 0 .416.292.272.51l-2.006 3.011a.327.327 0 0 1-.544 0l-2.006-3.012a.327.327 0 0 1 .272-.509h1.21a4.918 4.918 0 0 0-7.918-3.192 3.684 3.684 0 0 1-.184.136l-.014.01-.004.003-.002.001h-.001v.001l-.415-.625.414.625a.75.75 0 0 1-.83-1.25l.003-.001.02-.014c.02-.014.048-.035.083-.063Zm-.895 5.703H4.84a.327.327 0 0 0 .272-.51L3.106 5.146a.327.327 0 0 0-.545 0L.555 8.157c-.144.218.011.51.273.51h1.29a6.418 6.418 0 0 0 10.503 4.251.75.75 0 0 0-.963-1.15 4.918 4.918 0 0 1-8.03-3.102Z"
clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 703 B

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
fill="none"
viewBox="0 0 24 24"
height="24"
width="24">
<metadata
id="metadata14">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs12" />
<path
id="path2"
d="M 12 2 C 6.47715 2 2 6.47715 2 12 C 2 17.5228 6.47715 22 12 22 C 17.5228 22 22 17.5228 22 12 C 22 6.47715 17.5228 2 12 2 z M 11.880859 5.5039062 C 12.720859 5.4439063 13.470547 6.0746875 13.560547 6.9296875 L 13.560547 7.1699219 L 13.080078 13.169922 C 13.035078 13.724922 12.570625 14.144531 12.015625 14.144531 L 11.925781 14.144531 C 11.400781 14.099531 10.996172 13.694922 10.951172 13.169922 L 10.470703 7.1699219 C 10.395703 6.3149219 11.025859 5.5639064 11.880859 5.5039062 z M 12 15.763672 C 12.729 15.763672 13.320312 16.354884 13.320312 17.083984 C 13.320313 17.812984 12.729 18.404297 12 18.404297 C 11.271 18.404297 10.679688 17.812984 10.679688 17.083984 C 10.679688 16.354884 11.271 15.763672 12 15.763672 z "
style="fill:#ff5b55;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM6.9806 4.5101C6.9306 3.9401 7.3506 3.4401 7.9206 3.4001C8.4806 3.3601 8.9806 3.7801 9.0406 4.3501V4.5101L8.7206 8.5101C8.6906 8.8801 8.3806 9.1601 8.0106 9.1601H7.9506C7.6006 9.1301 7.3306 8.8601 7.3006 8.5101L6.9806 4.5101ZM8.88012 11.1202C8.88012 11.6062 8.48613 12.0002 8.00012 12.0002C7.51411 12.0002 7.12012 11.6062 7.12012 11.1202C7.12012 10.6342 7.51411 10.2402 8.00012 10.2402C8.48613 10.2402 8.88012 10.6342 8.88012 11.1202Z" fill="#8D99A5"/>
</svg>

Before

Width:  |  Height:  |  Size: 713 B

View File

@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 7.5L9 10.5L12 7.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 217 B

View File

@ -1,11 +0,0 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="LifeBuoy" transform="translate(-1378.000000, -91.000000)" stroke="#61708b" stroke-width="1">
<g id="search-copy" transform="translate(1379.000000, 92.000000)">
<circle id="Oval" cx="6.22222222" cy="6.22222222" r="6.22222222"></circle>
<path d="M14,14 L10.6166667,10.6166667" id="Path"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="10px" height="16px" viewBox="-1 -1 10 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: sketchtool 3.5.1 (25234) - http://www.bohemiancoding.com/sketch -->
<title>minimise</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="02-Chat" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="02_1-Chat-collapsed-w-topic" sketch:type="MSArtboardGroup" transform="translate(-176.000000, -27.000000)" stroke-width="2" stroke="#9FA9BA">
<g id="Room-list" sketch:type="MSLayerGroup">
<g id="Room-list/Header" sketch:type="MSShapeGroup">
<g id="minimise" transform="translate(172.000000, 25.000000)">
<path d="M7,5 L15,5 L15,13" id="Path-53-Copy" transform="translate(11.000000, 9.000000) scale(-1, -1) rotate(-315.000000) translate(-11.000000, -9.000000) "></path>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2V2Z" stroke="#0DBD8B" stroke-width="2" stroke-linecap="square"/>
<path d="M6.54549 12.8882L9.80306 16.2426L17.4546 8.36377" stroke="#0DBD8B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 442 B

View File

@ -14,14 +14,6 @@ export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor
export type { Leaves } from "matrix-web-i18n"; export type { Leaves } from "matrix-web-i18n";
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object
? RecursivePartial<T[P]>
: T[P];
};
export type KeysStartingWith<Input extends object, Str extends string> = { export type KeysStartingWith<Input extends object, Str extends string> = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
[P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X

View File

@ -10,7 +10,6 @@ Please see LICENSE files in the repository root for full details.
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import "@types/modernizr"; import "@types/modernizr";
import type { Renderer } from "react-dom";
import type { logger } from "matrix-js-sdk/src/logger"; import type { logger } from "matrix-js-sdk/src/logger";
import ContentMessages from "../ContentMessages"; import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg"; import { IMatrixClientPeg } from "../MatrixClientPeg";
@ -44,6 +43,7 @@ import AutoRageshakeStore from "../stores/AutoRageshakeStore";
import { IConfigOptions } from "../IConfigOptions"; import { IConfigOptions } from "../IConfigOptions";
import { MatrixDispatcher } from "../dispatcher/dispatcher"; import { MatrixDispatcher } from "../dispatcher/dispatcher";
import { DeepReadonly } from "./common"; import { DeepReadonly } from "./common";
import MatrixChat from "../components/structures/MatrixChat";
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
@ -71,7 +71,7 @@ declare global {
interface Window { interface Window {
mxSendRageshake: (text: string, withLogs?: boolean) => void; mxSendRageshake: (text: string, withLogs?: boolean) => void;
matrixLogger: typeof logger; matrixLogger: typeof logger;
matrixChat: ReturnType<Renderer>; matrixChat?: MatrixChat;
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>; mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>; mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
mxAutoRageshakeStore?: AutoRageshakeStore; mxAutoRageshakeStore?: AutoRageshakeStore;

View File

@ -10,13 +10,13 @@ Please see LICENSE files in the repository root for full details.
import { import {
IAddThreePidOnlyBody, IAddThreePidOnlyBody,
IAuthData,
IRequestMsisdnTokenResponse, IRequestMsisdnTokenResponse,
IRequestTokenResponse, IRequestTokenResponse,
MatrixClient, MatrixClient,
MatrixError, MatrixError,
HTTPError, HTTPError,
IThreepid, IThreepid,
UIAResponse,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import Modal from "./Modal"; import Modal from "./Modal";
@ -179,7 +179,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why * with a "message" property which contains a human-readable message detailing why
* the request failed. * the request failed.
*/ */
public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> { public async checkEmailLinkClicked(): Promise<
[success?: boolean, result?: UIAResponse<IAddThreePidOnlyBody> | Error | null]
> {
try { try {
if (this.bind) { if (this.bind) {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
@ -220,7 +222,7 @@ export default class AddThreepid {
continueKind: "primary", continueKind: "primary",
}, },
}; };
const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, { const { finished } = Modal.createDialog(InteractiveAuthDialog<IAddThreePidOnlyBody>, {
title: _t("settings|general|add_email_dialog_title"), title: _t("settings|general|add_email_dialog_title"),
matrixClient: this.matrixClient, matrixClient: this.matrixClient,
authData: err.data, authData: err.data,
@ -263,7 +265,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why * with a "message" property which contains a human-readable message detailing why
* the request failed. * the request failed.
*/ */
public async haveMsisdnToken(msisdnToken: string): Promise<[success?: boolean, result?: IAuthData | Error | null]> { public async haveMsisdnToken(
msisdnToken: string,
): Promise<[success?: boolean, result?: UIAResponse<IAddThreePidOnlyBody> | Error | null]> {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
if (this.submitUrl) { if (this.submitUrl) {
@ -319,7 +323,7 @@ export default class AddThreepid {
continueKind: "primary", continueKind: "primary",
}, },
}; };
const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, { const { finished } = Modal.createDialog(InteractiveAuthDialog<IAddThreePidOnlyBody>, {
title: _t("settings|general|add_msisdn_dialog_title"), title: _t("settings|general|add_msisdn_dialog_title"),
matrixClient: this.matrixClient, matrixClient: this.matrixClient,
authData: err.data, authData: err.data,

View File

@ -6,24 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { ComponentType, PropsWithChildren } from "react"; import React, { ReactNode, Suspense } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "./languageHandler"; import { _t } from "./languageHandler";
import BaseDialog from "./components/views/dialogs/BaseDialog"; import BaseDialog from "./components/views/dialogs/BaseDialog";
import DialogButtons from "./components/views/elements/DialogButtons"; import DialogButtons from "./components/views/elements/DialogButtons";
import Spinner from "./components/views/elements/Spinner"; import Spinner from "./components/views/elements/Spinner";
type AsyncImport<T> = { default: T };
interface IProps { interface IProps {
// A promise which resolves with the real component
prom: Promise<ComponentType<any> | AsyncImport<ComponentType<any>>>;
onFinished(): void; onFinished(): void;
children: ReactNode;
} }
interface IState { interface IState {
component?: ComponentType<PropsWithChildren<any>>;
error?: Error; error?: Error;
} }
@ -32,56 +27,26 @@ interface IState {
* spinner until the real component loads. * spinner until the real component loads.
*/ */
export default class AsyncWrapper extends React.Component<IProps, IState> { export default class AsyncWrapper extends React.Component<IProps, IState> {
private unmounted = false; public static getDerivedStateFromError(error: Error): IState {
return { error };
}
public state: IState = {}; public state: IState = {};
public componentDidMount(): void {
this.unmounted = false;
this.props.prom
.then((result) => {
if (this.unmounted) return;
// Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*.
const component = (result as AsyncImport<ComponentType>).default
? (result as AsyncImport<ComponentType>).default
: (result as ComponentType);
this.setState({ component });
})
.catch((e) => {
logger.warn("AsyncWrapper promise failed", e);
this.setState({ error: e });
});
}
public componentWillUnmount(): void {
this.unmounted = true;
}
private onWrapperCancelClick = (): void => {
this.props.onFinished();
};
public render(): React.ReactNode { public render(): React.ReactNode {
if (this.state.component) { if (this.state.error) {
const Component = this.state.component;
return <Component {...this.props} />;
} else if (this.state.error) {
return ( return (
<BaseDialog onFinished={this.props.onFinished} title={_t("common|error")}> <BaseDialog onFinished={this.props.onFinished} title={_t("common|error")}>
{_t("failed_load_async_component")} {_t("failed_load_async_component")}
<DialogButtons <DialogButtons
primaryButton={_t("action|dismiss")} primaryButton={_t("action|dismiss")}
onPrimaryButtonClick={this.onWrapperCancelClick} onPrimaryButtonClick={this.props.onFinished}
hasCancel={false} hasCancel={false}
/> />
</BaseDialog> </BaseDialog>
); );
} else {
// show a spinner until the component is loaded.
return <Spinner />;
} }
return <Suspense fallback={<Spinner />}>{this.props.children}</Suspense>;
} }
} }

View File

@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
// @ts-ignore - `.ts` is needed here to make TS happy
import { Request, Response } from "./workers/blurhash.worker.ts"; import { Request, Response } from "./workers/blurhash.worker.ts";
import { WorkerManager } from "./WorkerManager"; import { WorkerManager } from "./WorkerManager";
import blurhashWorkerFactory from "./workers/blurhashWorkerFactory"; import blurhashWorkerFactory from "./workers/blurhashWorkerFactory";

View File

@ -536,9 +536,7 @@ export default class ContentMessages {
attachMentions(matrixClient.getSafeUserId(), content, null, replyToEvent); attachMentions(matrixClient.getSafeUserId(), content, null, replyToEvent);
attachRelation(content, relation); attachRelation(content, relation);
if (replyToEvent) { if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, { addReplyToMessageContent(content, replyToEvent);
includeLegacyFallback: false,
});
} }
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {

View File

@ -14,7 +14,7 @@ import { IDeferred, defer } from "matrix-js-sdk/src/utils";
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import { Glass, TooltipProvider } from "@vector-im/compound-web"; import { Glass, TooltipProvider } from "@vector-im/compound-web";
import dis, { defaultDispatcher } from "./dispatcher/dispatcher"; import defaultDispatcher from "./dispatcher/dispatcher";
import AsyncWrapper from "./AsyncWrapper"; import AsyncWrapper from "./AsyncWrapper";
import { Defaultize } from "./@types/common"; import { Defaultize } from "./@types/common";
import { ActionPayload } from "./dispatcher/payloads"; import { ActionPayload } from "./dispatcher/payloads";
@ -136,32 +136,6 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
return !!this.priorityModal || !!this.staticModal || this.modals.length > 0; return !!this.priorityModal || !!this.staticModal || this.modals.length > 0;
} }
public createDialog<C extends ComponentType>(
Element: C,
props?: ComponentProps<C>,
className?: string,
isPriorityModal = false,
isStaticModal = false,
options: IOptions<C> = {},
): IHandle<C> {
return this.createDialogAsync<C>(
Promise.resolve(Element),
props,
className,
isPriorityModal,
isStaticModal,
options,
);
}
public appendDialog<C extends ComponentType>(
Element: C,
props?: ComponentProps<C>,
className?: string,
): IHandle<C> {
return this.appendDialogAsync<C>(Promise.resolve(Element), props, className);
}
/** /**
* DEPRECATED. * DEPRECATED.
* This is used only for tests. They should be using forceCloseAllModals but that * This is used only for tests. They should be using forceCloseAllModals but that
@ -196,8 +170,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
this.reRender(); this.reRender();
} }
/**
* @typeParam C - the component type
*/
private buildModal<C extends ComponentType>( private buildModal<C extends ComponentType>(
prom: Promise<C>, Component: C,
props?: ComponentProps<C>, props?: ComponentProps<C>,
className?: string, className?: string,
options?: IOptions<C>, options?: IOptions<C>,
@ -222,9 +199,12 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// otherwise we'll get confused. // otherwise we'll get confused.
const modalCount = this.counter++; const modalCount = this.counter++;
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // Typescript doesn't like us passing props as any here, but we know that they are well typed due to the rigorous generics.
// property set here so you can't close the dialog from a button click! modal.elem = (
modal.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />; <AsyncWrapper key={modalCount} onFinished={closeDialog}>
<Component {...(props as any)} onFinished={closeDialog} />
</AsyncWrapper>
);
modal.close = closeDialog; modal.close = closeDialog;
return { modal, closeDialog, onFinishedProm }; return { modal, closeDialog, onFinishedProm };
@ -291,29 +271,30 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
* require(['<module>'], cb); * require(['<module>'], cb);
* } * }
* *
* @param {Promise} prom a promise which resolves with a React component * @param component The component to render as a dialog. This component must accept an `onFinished` prop function as
* which will be displayed as the modal view. * per the type {@link ComponentType}. If loading a component with esoteric dependencies consider
* using React.lazy to async load the component.
* e.g. `lazy(() => import('./MyComponent'))`
* *
* @param {Object} props properties to pass to the displayed * @param props properties to pass to the displayed component. (We will also pass an 'onFinished' property.)
* component. (We will also pass an 'onFinished' property.)
* *
* @param {String} className CSS class to apply to the modal wrapper * @param className CSS class to apply to the modal wrapper
* *
* @param {boolean} isPriorityModal if true, this modal will be displayed regardless * @param isPriorityModal if true, this modal will be displayed regardless
* of other modals that are currently in the stack. * of other modals that are currently in the stack.
* Also, when closed, all modals will be removed * Also, when closed, all modals will be removed
* from the stack. * from the stack.
* @param {boolean} isStaticModal if true, this modal will be displayed under other * @param isStaticModal if true, this modal will be displayed under other
* modals in the stack. When closed, all modals will * modals in the stack. When closed, all modals will
* also be removed from the stack. This is not compatible * also be removed from the stack. This is not compatible
* with being a priority modal. Only one modal can be * with being a priority modal. Only one modal can be
* static at a time. * static at a time.
* @param {Object} options? extra options for the dialog * @param options? extra options for the dialog
* @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @param options.onBeforeClose a callback to decide whether to close the dialog
* @returns {object} Object with 'close' parameter being a function that will close the dialog * @returns Object with 'close' parameter being a function that will close the dialog
*/ */
public createDialogAsync<C extends ComponentType>( public createDialog<C extends ComponentType>(
prom: Promise<C>, component: C,
props?: ComponentProps<C>, props?: ComponentProps<C>,
className?: string, className?: string,
isPriorityModal = false, isPriorityModal = false,
@ -321,7 +302,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
options: IOptions<C> = {}, options: IOptions<C> = {},
): IHandle<C> { ): IHandle<C> {
const beforeModal = this.getCurrentModal(); const beforeModal = this.getCurrentModal();
const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(prom, props, className, options); const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(component, props, className, options);
if (isPriorityModal) { if (isPriorityModal) {
// XXX: This is destructive // XXX: This is destructive
this.priorityModal = modal; this.priorityModal = modal;
@ -341,13 +322,13 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
}; };
} }
private appendDialogAsync<C extends ComponentType>( public appendDialog<C extends ComponentType>(
prom: Promise<C>, component: C,
props?: ComponentProps<C>, props?: ComponentProps<C>,
className?: string, className?: string,
): IHandle<C> { ): IHandle<C> {
const beforeModal = this.getCurrentModal(); const beforeModal = this.getCurrentModal();
const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(prom, props, className, {}); const { modal, closeDialog, onFinishedProm } = this.buildModal<C>(component, props, className, {});
this.modals.push(modal); this.modals.push(modal);
@ -396,7 +377,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) { if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
// If there is no modal to render, make all of Element available // If there is no modal to render, make all of Element available
// to screen reader users again // to screen reader users again
dis.dispatch({ defaultDispatcher.dispatch({
action: "aria_unhide_main_app", action: "aria_unhide_main_app",
}); });
ModalManager.getOrCreateRoot().render(<></>); ModalManager.getOrCreateRoot().render(<></>);
@ -407,7 +388,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// Hide the content outside the modal to screen reader users // Hide the content outside the modal to screen reader users
// so they won't be able to navigate into it and act on it using // so they won't be able to navigate into it and act on it using
// screen reader specific features // screen reader specific features
dis.dispatch({ defaultDispatcher.dispatch({
action: "aria_hide_main_app", action: "aria_hide_main_app",
}); });

View File

@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
// @ts-ignore - `.ts` is needed here to make TS happy
import { Request, Response } from "./workers/playback.worker"; import { Request, Response } from "./workers/playback.worker";
import { WorkerManager } from "./WorkerManager"; import { WorkerManager } from "./WorkerManager";
import playbackWorkerFactory from "./workers/playbackWorkerFactory"; import playbackWorkerFactory from "./workers/playbackWorkerFactory";

View File

@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { lazy } from "react";
import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix"; import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix";
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog";
import Modal from "./Modal"; import Modal from "./Modal";
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from "./languageHandler"; import { _t } from "./languageHandler";
@ -232,10 +232,8 @@ async function doAccessSecretStorage(func: () => Promise<void>, forceReset: bool
if (createNew) { if (createNew) {
// This dialog calls bootstrap itself after guiding the user through // This dialog calls bootstrap itself after guiding the user through
// passphrase creation. // passphrase creation.
const { finished } = Modal.createDialogAsync( const { finished } = Modal.createDialog(
import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise< lazy(() => import("./async-components/views/dialogs/security/CreateSecretStorageDialog")),
typeof CreateSecretStorageDialog
>,
{ {
forceReset, forceReset,
}, },

View File

@ -28,7 +28,7 @@ interface NewRecoveryMethodDialogProps {
onFinished(): void; onFinished(): void;
} }
// Export as default instead of a named export so that it can be dynamically imported with `Modal.createDialogAsync` // Export as default instead of a named export so that it can be dynamically imported with React lazy
/** /**
* Dialog to inform the user that a new recovery method has been detected. * Dialog to inform the user that a new recovery method has been detected.

View File

@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React, { lazy } from "react";
import dis from "../../../../dispatcher/dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import Modal, { ComponentType } from "../../../../Modal"; import Modal from "../../../../Modal";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
@ -28,8 +28,8 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
private onSetupClick = (): void => { private onSetupClick = (): void => {
this.props.onFinished(); this.props.onFinished();
Modal.createDialogAsync( Modal.createDialog(
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType>, lazy(() => import("./CreateKeyBackupDialog")),
undefined, undefined,
undefined, undefined,
/* priority = */ false, /* priority = */ false,

View File

@ -31,4 +31,3 @@ export const BackdropPanel: React.FC<IProps> = ({ backgroundImage, blurMultiplie
</div> </div>
); );
}; };
export default BackdropPanel;

View File

@ -49,11 +49,10 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandl
import AudioFeedArrayForLegacyCall from "../views/voip/AudioFeedArrayForLegacyCall"; import AudioFeedArrayForLegacyCall from "../views/voip/AudioFeedArrayForLegacyCall";
import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import RoomView from "./RoomView"; import { RoomView } from "./RoomView";
import type { RoomView as RoomViewType } from "./RoomView";
import ToastContainer from "./ToastContainer"; import ToastContainer from "./ToastContainer";
import UserView from "./UserView"; import UserView from "./UserView";
import BackdropPanel from "./BackdropPanel"; import { BackdropPanel } from "./BackdropPanel";
import { mediaFromMxc } from "../../customisations/Media"; import { mediaFromMxc } from "../../customisations/Media";
import { UserTab } from "../views/dialogs/UserTab"; import { UserTab } from "../views/dialogs/UserTab";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
@ -125,7 +124,7 @@ class LoggedInView extends React.Component<IProps, IState> {
public static displayName = "LoggedInView"; public static displayName = "LoggedInView";
protected readonly _matrixClient: MatrixClient; protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<RoomViewType>; protected readonly _roomView: React.RefObject<RoomView>;
protected readonly _resizeContainer: React.RefObject<HTMLDivElement>; protected readonly _resizeContainer: React.RefObject<HTMLDivElement>;
protected readonly resizeHandler: React.RefObject<HTMLDivElement>; protected readonly resizeHandler: React.RefObject<HTMLDivElement>;
protected layoutWatcherRef?: string; protected layoutWatcherRef?: string;

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { createRef } from "react"; import React, { createRef, lazy } from "react";
import { import {
ClientEvent, ClientEvent,
createClient, createClient,
@ -28,8 +28,6 @@ import { TooltipProvider } from "@vector-im/compound-web";
// what-input helps improve keyboard accessibility // what-input helps improve keyboard accessibility
import "what-input"; import "what-input";
import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog";
import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
import PosthogTrackers from "../../PosthogTrackers"; import PosthogTrackers from "../../PosthogTrackers";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg"; import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg";
@ -1649,16 +1647,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
if (haveNewVersion) { if (haveNewVersion) {
Modal.createDialogAsync( Modal.createDialog(
import( lazy(() => import("../../async-components/views/dialogs/security/NewRecoveryMethodDialog")),
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
) as unknown as Promise<typeof NewRecoveryMethodDialog>,
); );
} else { } else {
Modal.createDialogAsync( Modal.createDialog(
import( lazy(() => import("../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog")),
"../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"
) as unknown as Promise<typeof RecoveryMethodRemovedDialog>,
); );
} }
}); });

View File

@ -45,7 +45,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import ContentMessages from "../../ContentMessages"; import ContentMessages from "../../ContentMessages";
import Modal from "../../Modal"; import Modal from "../../Modal";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import dis, { defaultDispatcher } from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import * as Rooms from "../../Rooms"; import * as Rooms from "../../Rooms";
import MainSplit from "./MainSplit"; import MainSplit from "./MainSplit";
import RightPanel from "./RightPanel"; import RightPanel from "./RightPanel";
@ -437,7 +437,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onWidgetLayoutChange = (): void => { private onWidgetLayoutChange = (): void => {
if (!this.state.room) return; if (!this.state.room) return;
dis.dispatch({ defaultDispatcher.dispatch({
action: "appsDrawer", action: "appsDrawer",
show: true, show: true,
}); });
@ -598,7 +598,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// Handle the use case of a link to a thread message // Handle the use case of a link to a thread message
// ie: #/room/roomId/eventId (eventId of a thread message) // ie: #/room/roomId/eventId (eventId of a thread message)
if (thread?.rootEvent && !initialEvent?.isThreadRoot) { if (thread?.rootEvent && !initialEvent?.isThreadRoot) {
dis.dispatch<ShowThreadPayload>({ defaultDispatcher.dispatch<ShowThreadPayload>({
action: Action.ShowThread, action: Action.ShowThread,
rootEvent: thread.rootEvent, rootEvent: thread.rootEvent,
initialEvent, initialEvent,
@ -704,7 +704,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const activeCall = CallStore.instance.getActiveCall(this.state.roomId); const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
if (activeCall === null) { if (activeCall === null) {
// We disconnected from the call, so stop viewing it // We disconnected from the call, so stop viewing it
dis.dispatch<ViewRoomPayload>( defaultDispatcher.dispatch<ViewRoomPayload>(
{ {
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: this.state.roomId, room_id: this.state.roomId,
@ -850,7 +850,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public componentDidMount(): void { public componentDidMount(): void {
this.unmounted = false; this.unmounted = false;
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
if (this.context.client) { if (this.context.client) {
this.context.client.on(ClientEvent.Room, this.onRoom); this.context.client.on(ClientEvent.Room, this.onRoom);
this.context.client.on(RoomEvent.Timeline, this.onRoomTimeline); this.context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
@ -967,7 +967,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// stop tracking room changes to format permalinks // stop tracking room changes to format permalinks
this.stopAllPermalinkCreators(); this.stopAllPermalinkCreators();
dis.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
if (this.context.client) { if (this.context.client) {
this.context.client.removeListener(ClientEvent.Room, this.onRoom); this.context.client.removeListener(ClientEvent.Room, this.onRoom);
this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline); this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
@ -1045,7 +1045,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
handled = true; handled = true;
break; break;
case KeyBindingAction.UploadFile: { case KeyBindingAction.UploadFile: {
dis.dispatch( defaultDispatcher.dispatch(
{ {
action: "upload_file", action: "upload_file",
context: TimelineRenderingType.Room, context: TimelineRenderingType.Room,
@ -1145,7 +1145,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (payload.event && payload.event.getRoomId() !== this.state.roomId) { if (payload.event && payload.event.getRoomId() !== this.state.roomId) {
// If the event is in a different room (e.g. because the event to be edited is being displayed // If the event is in a different room (e.g. because the event to be edited is being displayed
// in the results of an all-rooms search), we need to view that room first. // in the results of an all-rooms search), we need to view that room first.
dis.dispatch<ViewRoomPayload>({ defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: payload.event.getRoomId(), room_id: payload.event.getRoomId(),
metricsTrigger: undefined, metricsTrigger: undefined,
@ -1188,7 +1188,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
// re-dispatch to the correct composer // re-dispatch to the correct composer
dis.dispatch<ComposerInsertPayload>({ defaultDispatcher.dispatch<ComposerInsertPayload>({
...(payload as ComposerInsertPayload), ...(payload as ComposerInsertPayload),
timelineRenderingType, timelineRenderingType,
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send, composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
@ -1197,7 +1197,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
case Action.FocusAComposer: { case Action.FocusAComposer: {
dis.dispatch<FocusComposerPayload>({ defaultDispatcher.dispatch<FocusComposerPayload>({
...(payload as FocusComposerPayload), ...(payload as FocusComposerPayload),
// re-dispatch to the correct composer (the send message will still be on screen even when editing a message) // re-dispatch to the correct composer (the send message will still be on screen even when editing a message)
action: this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer, action: this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer,
@ -1303,7 +1303,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
// For initial threads launch, chat effects are disabled see #19731 // For initial threads launch, chat effects are disabled see #19731
if (!ev.isRelation(THREAD_RELATION_TYPE.name)) { if (!ev.isRelation(THREAD_RELATION_TYPE.name)) {
dis.dispatch({ action: `effects.${effect.command}`, event: ev }); defaultDispatcher.dispatch({ action: `effects.${effect.command}`, event: ev });
} }
} }
}); });
@ -1363,7 +1363,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
liveTimeline: room.getLiveTimeline(), liveTimeline: room.getLiveTimeline(),
}); });
dis.dispatch<ActionPayload>({ action: Action.RoomLoaded }); defaultDispatcher.dispatch<ActionPayload>({ action: Action.RoomLoaded });
}; };
private onRoomTimelineReset = (room?: Room): void => { private onRoomTimelineReset = (room?: Room): void => {
@ -1561,7 +1561,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onInviteClick = (): void => { private onInviteClick = (): void => {
// open the room inviter // open the room inviter
dis.dispatch({ defaultDispatcher.dispatch({
action: "view_invite", action: "view_invite",
roomId: this.getRoomId(), roomId: this.getRoomId(),
}); });
@ -1572,7 +1572,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (this.context.client?.isGuest()) { if (this.context.client?.isGuest()) {
// Join this room once the user has registered and logged in // Join this room once the user has registered and logged in
// (If we failed to peek, we may not have a valid room object.) // (If we failed to peek, we may not have a valid room object.)
dis.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({ defaultDispatcher.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
action: Action.DoAfterSyncPrepared, action: Action.DoAfterSyncPrepared,
deferred_action: { deferred_action: {
action: Action.ViewRoom, action: Action.ViewRoom,
@ -1580,13 +1580,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
metricsTrigger: undefined, metricsTrigger: undefined,
}, },
}); });
dis.dispatch({ action: "require_registration" }); defaultDispatcher.dispatch({ action: "require_registration" });
} else { } else {
Promise.resolve().then(() => { Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl; const signUrl = this.props.threepidInvite?.signUrl;
const roomId = this.getRoomId(); const roomId = this.getRoomId();
if (isNotUndefined(roomId)) { if (isNotUndefined(roomId)) {
dis.dispatch<JoinRoomPayload>({ defaultDispatcher.dispatch<JoinRoomPayload>({
action: Action.JoinRoom, action: Action.JoinRoom,
roomId, roomId,
opts: { inviteSignUrl: signUrl }, opts: { inviteSignUrl: signUrl },
@ -1622,7 +1622,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.state.initialEventId === eventId this.state.initialEventId === eventId
) { ) {
debuglog("Removing scroll_into_view flag from initial event"); debuglog("Removing scroll_into_view flag from initial event");
dis.dispatch<ViewRoomPayload>({ defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: this.getRoomId(), room_id: this.getRoomId(),
event_id: this.state.initialEventId, event_id: this.state.initialEventId,
@ -1638,7 +1638,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const roomId = this.getRoomId(); const roomId = this.getRoomId();
if (!this.context.client || !roomId) return; if (!this.context.client || !roomId) return;
if (this.context.client.isGuest()) { if (this.context.client.isGuest()) {
dis.dispatch({ action: "require_registration" }); defaultDispatcher.dispatch({ action: "require_registration" });
return; return;
} }
@ -1688,7 +1688,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}; };
private onForgetClick = (): void => { private onForgetClick = (): void => {
dis.dispatch({ defaultDispatcher.dispatch({
action: "forget_room", action: "forget_room",
room_id: this.getRoomId(), room_id: this.getRoomId(),
}); });
@ -1702,7 +1702,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}); });
this.context.client?.leave(roomId).then( this.context.client?.leave(roomId).then(
() => { () => {
dis.dispatch({ action: Action.ViewHomePage }); defaultDispatcher.dispatch({ action: Action.ViewHomePage });
this.setState({ this.setState({
rejecting: false, rejecting: false,
}); });
@ -1736,7 +1736,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
await this.context.client!.setIgnoredUsers(ignoredUsers); await this.context.client!.setIgnoredUsers(ignoredUsers);
await this.context.client!.leave(this.state.roomId!); await this.context.client!.leave(this.state.roomId!);
dis.dispatch({ action: Action.ViewHomePage }); defaultDispatcher.dispatch({ action: Action.ViewHomePage });
this.setState({ this.setState({
rejecting: false, rejecting: false,
}); });
@ -1760,7 +1760,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// using /leave rather than /join. In the short term though, we // using /leave rather than /join. In the short term though, we
// just ignore them. // just ignore them.
// https://github.com/vector-im/vector-web/issues/1134 // https://github.com/vector-im/vector-web/issues/1134
dis.fire(Action.ViewRoomDirectory); defaultDispatcher.fire(Action.ViewRoomDirectory);
}; };
private onSearchChange = debounce((e: ChangeEvent): void => { private onSearchChange = debounce((e: ChangeEvent): void => {
@ -1786,7 +1786,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// If we were viewing a highlighted event, firing view_room without // If we were viewing a highlighted event, firing view_room without
// an event will take care of both clearing the URL fragment and // an event will take care of both clearing the URL fragment and
// jumping to the bottom // jumping to the bottom
dis.dispatch<ViewRoomPayload>({ defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: this.getRoomId(), room_id: this.getRoomId(),
metricsTrigger: undefined, // room doesn't change metricsTrigger: undefined, // room doesn't change
@ -1794,7 +1794,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} else { } else {
// Otherwise we have to jump manually // Otherwise we have to jump manually
this.messagePanel?.jumpToLiveTimeline(); this.messagePanel?.jumpToLiveTimeline();
dis.fire(Action.FocusSendMessageComposer); defaultDispatcher.fire(Action.FocusSendMessageComposer);
} }
}; };
@ -1918,7 +1918,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public onHiddenHighlightsClick = (): void => { public onHiddenHighlightsClick = (): void => {
const oldRoom = this.getOldRoom(); const oldRoom = this.getOldRoom();
if (!oldRoom) return; if (!oldRoom) return;
dis.dispatch<ViewRoomPayload>({ defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: oldRoom.roomId, room_id: oldRoom.roomId,
metricsTrigger: "Predecessor", metricsTrigger: "Predecessor",
@ -2001,7 +2001,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const roomId = this.getRoomId(); const roomId = this.getRoomId();
if (isNotUndefined(roomId)) { if (isNotUndefined(roomId)) {
dis.dispatch<SubmitAskToJoinPayload>({ defaultDispatcher.dispatch<SubmitAskToJoinPayload>({
action: Action.SubmitAskToJoin, action: Action.SubmitAskToJoin,
roomId, roomId,
opts: { reason }, opts: { reason },
@ -2018,7 +2018,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const roomId = this.getRoomId(); const roomId = this.getRoomId();
if (isNotUndefined(roomId)) { if (isNotUndefined(roomId)) {
dis.dispatch<CancelAskToJoinPayload>({ defaultDispatcher.dispatch<CancelAskToJoinPayload>({
action: Action.CancelAskToJoin, action: Action.CancelAskToJoin,
roomId, roomId,
}); });
@ -2547,5 +2547,3 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
); );
} }
} }
export default RoomView;

View File

@ -1217,7 +1217,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
return; return;
} }
const lastDisplayedEvent = this.state.events[lastDisplayedIndex]; const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
this.setReadMarker(lastDisplayedEvent.getId()!, lastDisplayedEvent.getTs()); await this.setReadMarker(lastDisplayedEvent.getId()!, lastDisplayedEvent.getTs());
// the read-marker should become invisible, so that if the user scrolls // the read-marker should become invisible, so that if the user scrolls
// down, they don't see it. // down, they don't see it.
@ -1335,7 +1335,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
} }
// Update the read marker to the values we found // Update the read marker to the values we found
this.setReadMarker(rmId, rmTs); await this.setReadMarker(rmId, rmTs);
// Send the receipts to the server immediately (don't wait for activity) // Send the receipts to the server immediately (don't wait for activity)
await this.sendReadReceipts(); await this.sendReadReceipts();
@ -1866,7 +1866,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
return receiptStore?.getEventReadUpTo(myUserId, ignoreSynthesized) ?? null; return receiptStore?.getEventReadUpTo(myUserId, ignoreSynthesized) ?? null;
} }
private setReadMarker(eventId: string | null, eventTs?: number, inhibitSetState = false): void { private async setReadMarker(eventId: string | null, eventTs?: number, inhibitSetState = false): Promise<void> {
const roomId = this.props.timelineSet.room?.roomId; const roomId = this.props.timelineSet.room?.roomId;
// don't update the state (and cause a re-render) if there is // don't update the state (and cause a re-render) if there is
@ -1890,12 +1890,17 @@ class TimelinePanel extends React.Component<IProps, IState> {
// Do the local echo of the RM // Do the local echo of the RM
// run the render cycle before calling the callback, so that // run the render cycle before calling the callback, so that
// getReadMarkerPosition() returns the right thing. // getReadMarkerPosition() returns the right thing.
this.setState( await new Promise<void>((resolve) => {
{ this.setState(
readMarkerEventId: eventId, {
}, readMarkerEventId: eventId,
this.props.onReadMarkerUpdated, },
); () => {
this.props.onReadMarkerUpdated?.();
resolve();
},
);
});
} }
private shouldPaginate(): boolean { private shouldPaginate(): boolean {

View File

@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import AccessibleButton from "../../../views/elements/AccessibleButton"; import AccessibleButton from "../../../views/elements/AccessibleButton";
import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg"; import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
import { Icon as RetryIcon } from "../../../../../res/img/compound/retry-16px.svg";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle"; import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
import { ErrorMessage } from "../../ErrorMessage"; import { ErrorMessage } from "../../ErrorMessage";
@ -60,7 +60,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span> <span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span>
<Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}> <Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}>
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}> <AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
<RetryIcon className="mx_Icon mx_Icon_16" /> <RestartIcon className="mx_Icon mx_Icon_16" />
{_t("action|resend")} {_t("action|resend")}
</AccessibleButton> </AccessibleButton>
</Tooltip> </Tooltip>

View File

@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../../views/elements/AccessibleButton"; import AccessibleButton from "../../../views/elements/AccessibleButton";
import { Icon as RetryIcon } from "../../../../../res/img/compound/retry-16px.svg";
import { Icon as EmailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg"; import { Icon as EmailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle"; import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
import { ErrorMessage } from "../../ErrorMessage"; import { ErrorMessage } from "../../ErrorMessage";
@ -59,7 +59,7 @@ export const VerifyEmailModal: React.FC<Props> = ({
<span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span> <span className="mx_VerifyEMailDialog_text-light">{_t("auth|check_email_resend_prompt")}</span>
<Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}> <Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}>
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}> <AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
<RetryIcon className="mx_Icon mx_Icon_16" /> <RestartIcon className="mx_Icon mx_Icon_16" />
{_t("action|resend")} {_t("action|resend")}
</AccessibleButton> </AccessibleButton>
</Tooltip> </Tooltip>

View File

@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import dispatcher, { defaultDispatcher } from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { ConnectionState, ElementCall } from "../../../models/Call"; import { ConnectionState, ElementCall } from "../../../models/Call";
@ -53,7 +53,7 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
return; return;
} }
dispatcher.dispatch<ViewRoomPayload>({ defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: roomId, room_id: roomId,
metricsTrigger: undefined, metricsTrigger: undefined,

View File

@ -12,6 +12,7 @@ import { Room, EventType } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t, _td, TranslationKey } from "../../../languageHandler"; import { _t, _td, TranslationKey } from "../../../languageHandler";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
@ -34,7 +35,6 @@ import LazyRenderList from "../elements/LazyRenderList";
import { useSettingValue } from "../../../hooks/useSettings"; import { useSettingValue } from "../../../hooks/useSettings";
import { filterBoolean } from "../../../utils/arrays"; import { filterBoolean } from "../../../utils/arrays";
import { NonEmptyArray } from "../../../@types/common"; import { NonEmptyArray } from "../../../@types/common";
import WarningBadgeSvg from "../../../../res/img/element-icons/warning-badge.svg";
// These values match CSS // These values match CSS
const ROW_HEIGHT = 32 + 12; const ROW_HEIGHT = 32 + 12;
@ -229,7 +229,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
if (error) { if (error) {
footer = ( footer = (
<> <>
<img src={WarningBadgeSvg} height="24" width="24" alt="" /> <ErrorIcon height="24px" width="24px" />
<span className="mx_AddExistingToSpaceDialog_error"> <span className="mx_AddExistingToSpaceDialog_error">
<div className="mx_AddExistingToSpaceDialog_errorHeading"> <div className="mx_AddExistingToSpaceDialog_errorHeading">

View File

@ -7,12 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React, { lazy } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/matrix";
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@ -116,10 +114,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
} }
private onExportE2eKeysClicked = (): void => { private onExportE2eKeysClicked = (): void => {
Modal.createDialogAsync( Modal.createDialog(
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise< lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
typeof ExportE2eKeysDialog
>,
{ {
matrixClient: MatrixClientPeg.safeGet(), matrixClient: MatrixClientPeg.safeGet(),
}, },
@ -147,10 +143,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
/* static = */ true, /* static = */ true,
); );
} else { } else {
Modal.createDialogAsync( Modal.createDialog(
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise< lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
typeof CreateKeyBackupDialog
>,
undefined, undefined,
undefined, undefined,
/* priority = */ false, /* priority = */ false,

View File

@ -22,6 +22,7 @@ import {
WidgetApiFromWidgetAction, WidgetApiFromWidgetAction,
WidgetKind, WidgetKind,
} from "matrix-widget-api"; } from "matrix-widget-api";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { _t, getUserLanguage } from "../../../languageHandler"; import { _t, getUserLanguage } from "../../../languageHandler";
@ -33,7 +34,6 @@ import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import { ELEMENT_CLIENT_ID } from "../../../identifiers"; import { ELEMENT_CLIENT_ID } from "../../../identifiers";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import WarningBadgeSvg from "../../../../res/img/element-icons/warning-badge.svg";
interface IProps { interface IProps {
widgetDefinition: IModalWidgetOpenRequestData; widgetDefinition: IModalWidgetOpenRequestData;
@ -186,7 +186,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
> >
<div className="mx_ModalWidgetDialog_warning"> <div className="mx_ModalWidgetDialog_warning">
<img src={WarningBadgeSvg} height="16" width="16" alt="" /> <ErrorIcon width="16px" height="16px" />
{_t("widget|modal_data_warning", { {_t("widget|modal_data_warning", {
widgetDomain: parsed.hostname, widgetDomain: parsed.hostname,
})} })}

View File

@ -21,7 +21,7 @@ import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePrefer
import { NonEmptyArray } from "../../../@types/common"; import { NonEmptyArray } from "../../../@types/common";
import SettingsTab from "../settings/tabs/SettingsTab"; import SettingsTab from "../settings/tabs/SettingsTab";
import { SettingsSection } from "../settings/shared/SettingsSection"; import { SettingsSection } from "../settings/shared/SettingsSection";
import SettingsSubsection, { SettingsSubsectionText } from "../settings/shared/SettingsSubsection"; import { SettingsSubsection, SettingsSubsectionText } from "../settings/shared/SettingsSubsection";
interface IProps { interface IProps {
space: Room; space: Room;

View File

@ -8,9 +8,8 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { ChangeEvent } from "react"; import React, { ChangeEvent } from "react";
import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix"; import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { decodeRecoveryKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { decodeRecoveryKey, KeyBackupInfo, KeyBackupRestoreResult } from "matrix-js-sdk/src/crypto-api";
import { IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../MatrixClientPeg";
@ -42,12 +41,11 @@ interface IProps {
interface IState { interface IState {
backupInfo: KeyBackupInfo | null; backupInfo: KeyBackupInfo | null;
backupKeyStored: Record<string, SecretStorage.SecretStorageKeyDescription> | null;
loading: boolean; loading: boolean;
loadError: boolean | null; loadError: boolean | null;
restoreError: unknown | null; restoreError: unknown | null;
recoveryKey: string; recoveryKey: string;
recoverInfo: IKeyBackupRestoreResult | null; recoverInfo: KeyBackupRestoreResult | null;
recoveryKeyValid: boolean; recoveryKeyValid: boolean;
forceRecoveryKey: boolean; forceRecoveryKey: boolean;
passPhrase: string; passPhrase: string;
@ -72,7 +70,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
super(props); super(props);
this.state = { this.state = {
backupInfo: null, backupInfo: null,
backupKeyStored: null,
loading: false, loading: false,
loadError: null, loadError: null,
restoreError: null, restoreError: null,
@ -137,7 +134,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
}; };
private onPassPhraseNext = async (): Promise<void> => { private onPassPhraseNext = async (): Promise<void> => {
if (!this.state.backupInfo) return; const crypto = MatrixClientPeg.safeGet().getCrypto();
if (!crypto) return;
this.setState({ this.setState({
loading: true, loading: true,
restoreError: null, restoreError: null,
@ -146,13 +144,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
try { try {
// We do still restore the key backup: we must ensure that the key backup key // We do still restore the key backup: we must ensure that the key backup key
// is the right one and restoring it is currently the only way we can do this. // is the right one and restoring it is currently the only way we can do this.
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithPassword( const recoverInfo = await crypto.restoreKeyBackupWithPassphrase(this.state.passPhrase, {
this.state.passPhrase, progressCallback: this.progressCallback,
undefined, });
undefined,
this.state.backupInfo,
{ progressCallback: this.progressCallback },
);
if (!this.props.showSummary) { if (!this.props.showSummary) {
this.props.onFinished(true); this.props.onFinished(true);
@ -172,7 +166,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
}; };
private onRecoveryKeyNext = async (): Promise<void> => { private onRecoveryKeyNext = async (): Promise<void> => {
if (!this.state.recoveryKeyValid || !this.state.backupInfo) return; const crypto = MatrixClientPeg.safeGet().getCrypto();
if (!this.state.recoveryKeyValid || !this.state.backupInfo?.version || !crypto) return;
this.setState({ this.setState({
loading: true, loading: true,
@ -180,13 +175,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
restoreType: RestoreType.RecoveryKey, restoreType: RestoreType.RecoveryKey,
}); });
try { try {
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithRecoveryKey( await crypto.storeSessionBackupPrivateKey(
this.state.recoveryKey, decodeRecoveryKey(this.state.recoveryKey),
undefined, this.state.backupInfo.version,
undefined,
this.state.backupInfo,
{ progressCallback: this.progressCallback },
); );
const recoverInfo = await crypto.restoreKeyBackup({
progressCallback: this.progressCallback,
});
if (!this.props.showSummary) { if (!this.props.showSummary) {
this.props.onFinished(true); this.props.onFinished(true);
return; return;
@ -210,44 +206,41 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
}); });
}; };
private async restoreWithSecretStorage(): Promise<void> { private async restoreWithSecretStorage(): Promise<boolean> {
const crypto = MatrixClientPeg.safeGet().getCrypto();
if (!crypto) return false;
this.setState({ this.setState({
loading: true,
restoreError: null, restoreError: null,
restoreType: RestoreType.SecretStorage, restoreType: RestoreType.SecretStorage,
}); });
try { try {
let recoverInfo: KeyBackupRestoreResult | null = null;
// `accessSecretStorage` may prompt for storage access as needed. // `accessSecretStorage` may prompt for storage access as needed.
await accessSecretStorage(async (): Promise<void> => { await accessSecretStorage(async (): Promise<void> => {
if (!this.state.backupInfo) return; await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
await MatrixClientPeg.safeGet().restoreKeyBackupWithSecretStorage( recoverInfo = await crypto.restoreKeyBackup({ progressCallback: this.progressCallback });
this.state.backupInfo,
undefined,
undefined,
{ progressCallback: this.progressCallback },
);
}); });
this.setState({ this.setState({
loading: false, loading: false,
recoverInfo,
}); });
return true;
} catch (e) { } catch (e) {
logger.log("Error restoring backup", e); logger.log("restoreWithSecretStorage failed:", e);
this.setState({ this.setState({
restoreError: e, restoreError: e,
loading: false, loading: false,
}); });
return false;
} }
} }
private async restoreWithCachedKey(backupInfo: KeyBackupInfo | null): Promise<boolean> { private async restoreWithCachedKey(backupInfo: KeyBackupInfo | null): Promise<boolean> {
if (!backupInfo) return false; const crypto = MatrixClientPeg.safeGet().getCrypto();
if (!crypto) return false;
try { try {
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithCache( const recoverInfo = await crypto.restoreKeyBackup({ progressCallback: this.progressCallback });
undefined /* targetRoomId */,
undefined /* targetSessionId */,
backupInfo,
{ progressCallback: this.progressCallback },
);
this.setState({ this.setState({
recoverInfo, recoverInfo,
}); });
@ -270,7 +263,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null; const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null;
this.setState({ this.setState({
backupInfo, backupInfo,
backupKeyStored,
}); });
const gotCache = await this.restoreWithCachedKey(backupInfo); const gotCache = await this.restoreWithCachedKey(backupInfo);
@ -282,9 +274,13 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
return; return;
} }
// If the backup key is stored, we can proceed directly to restore. const hasBackupFromSS = backupKeyStored && (await this.restoreWithSecretStorage());
if (backupKeyStored) { if (hasBackupFromSS) {
return this.restoreWithSecretStorage(); logger.log("RestoreKeyBackupDialog: found backup key in secret storage");
this.setState({
loading: false,
});
return;
} }
this.setState({ this.setState({
@ -398,6 +394,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
<form className="mx_RestoreKeyBackupDialog_primaryContainer"> <form className="mx_RestoreKeyBackupDialog_primaryContainer">
<input <input
data-testid="passphraseInput"
type="password" type="password"
className="mx_RestoreKeyBackupDialog_passPhraseInput" className="mx_RestoreKeyBackupDialog_passPhraseInput"
onChange={this.onPassPhraseChange} onChange={this.onPassPhraseChange}

View File

@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { createRef, CSSProperties } from "react"; import React, { createRef, CSSProperties, useRef, useState } from "react";
import FocusLock from "react-focus-lock"; import FocusLock from "react-focus-lock";
import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
@ -30,6 +30,9 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { presentableTextForFile } from "../../../utils/FileUtils"; import { presentableTextForFile } from "../../../utils/FileUtils";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import { FileDownloader } from "../../../utils/FileDownloader";
// Max scale to keep gaps around the image // Max scale to keep gaps around the image
const MAX_SCALE = 0.95; const MAX_SCALE = 0.95;
@ -309,15 +312,6 @@ export default class ImageView extends React.Component<IProps, IState> {
this.setZoomAndRotation(cur + 90); this.setZoomAndRotation(cur + 90);
}; };
private onDownloadClick = (): void => {
const a = document.createElement("a");
a.href = this.props.src;
if (this.props.name) a.download = this.props.name;
a.target = "_blank";
a.rel = "noreferrer noopener";
a.click();
};
private onOpenContextMenu = (): void => { private onOpenContextMenu = (): void => {
this.setState({ this.setState({
contextMenuDisplayed: true, contextMenuDisplayed: true,
@ -555,11 +549,7 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("lightbox|rotate_right")} title={_t("lightbox|rotate_right")}
onClick={this.onRotateClockwiseClick} onClick={this.onRotateClockwiseClick}
/> />
<AccessibleButton <DownloadButton url={this.props.src} fileName={this.props.name} />
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("action|download")}
onClick={this.onDownloadClick}
/>
{contextMenuButton} {contextMenuButton}
<AccessibleButton <AccessibleButton
className="mx_ImageView_button mx_ImageView_button_close" className="mx_ImageView_button mx_ImageView_button_close"
@ -591,3 +581,61 @@ export default class ImageView extends React.Component<IProps, IState> {
); );
} }
} }
function DownloadButton({ url, fileName }: { url: string; fileName?: string }): JSX.Element {
const downloader = useRef(new FileDownloader()).current;
const [loading, setLoading] = useState(false);
const blobRef = useRef<Blob>();
function showError(e: unknown): void {
Modal.createDialog(ErrorDialog, {
title: _t("timeline|download_failed"),
description: (
<>
<div>{_t("timeline|download_failed_description")}</div>
<div>{e instanceof Error ? e.toString() : ""}</div>
</>
),
});
setLoading(false);
}
const onDownloadClick = async (): Promise<void> => {
try {
if (loading) return;
setLoading(true);
if (blobRef.current) {
// Cheat and trigger a download, again.
return downloadBlob(blobRef.current);
}
const res = await fetch(url);
if (!res.ok) {
throw parseErrorResponse(res, await res.text());
}
const blob = await res.blob();
blobRef.current = blob;
await downloadBlob(blob);
} catch (e) {
showError(e);
}
};
async function downloadBlob(blob: Blob): Promise<void> {
await downloader.download({
blob,
name: fileName ?? _t("common|image"),
});
setLoading(false);
}
return (
<AccessibleButton
className="mx_ImageView_button mx_ImageView_button_download"
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
onClick={onDownloadClick}
disabled={loading}
/>
);
}

View File

@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { MutableRefObject, ReactNode, StrictMode } from "react"; import React, { MutableRefObject, ReactNode, StrictMode } from "react";
import ReactDOM from "react-dom"; import { createRoot, Root } from "react-dom/client";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
@ -24,7 +24,7 @@ export const getPersistKey = (appId: string): string => "widget_" + appId;
// We contain all persisted elements within a master container to allow them all to be within the same // We contain all persisted elements within a master container to allow them all to be within the same
// CSS stacking context, and thus be able to control their z-indexes relative to each other. // CSS stacking context, and thus be able to control their z-indexes relative to each other.
function getOrCreateMasterContainer(): HTMLDivElement { function getOrCreateMasterContainer(): HTMLDivElement {
let container = getContainer("mx_PersistedElement_container"); let container = document.getElementById("mx_PersistedElement_container") as HTMLDivElement;
if (!container) { if (!container) {
container = document.createElement("div"); container = document.createElement("div");
container.id = "mx_PersistedElement_container"; container.id = "mx_PersistedElement_container";
@ -34,18 +34,10 @@ function getOrCreateMasterContainer(): HTMLDivElement {
return container; return container;
} }
function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId) as HTMLDivElement;
}
function getOrCreateContainer(containerId: string): HTMLDivElement { function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId); const container = document.createElement("div");
container.id = containerId;
if (!container) { getOrCreateMasterContainer().appendChild(container);
container = document.createElement("div");
container.id = containerId;
getOrCreateMasterContainer().appendChild(container);
}
return container; return container;
} }
@ -83,6 +75,8 @@ export default class PersistedElement extends React.Component<IProps> {
private childContainer?: HTMLDivElement; private childContainer?: HTMLDivElement;
private child?: HTMLDivElement; private child?: HTMLDivElement;
private static rootMap: Record<string, [root: Root, container: Element]> = {};
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@ -99,14 +93,16 @@ export default class PersistedElement extends React.Component<IProps> {
* @param {string} persistKey Key used to uniquely identify this PersistedElement * @param {string} persistKey Key used to uniquely identify this PersistedElement
*/ */
public static destroyElement(persistKey: string): void { public static destroyElement(persistKey: string): void {
const container = getContainer("mx_persistedElement_" + persistKey); const pair = PersistedElement.rootMap[persistKey];
if (container) { if (pair) {
container.remove(); pair[0].unmount();
pair[1].remove();
} }
delete PersistedElement.rootMap[persistKey];
} }
public static isMounted(persistKey: string): boolean { public static isMounted(persistKey: string): boolean {
return Boolean(getContainer("mx_persistedElement_" + persistKey)); return Boolean(PersistedElement.rootMap[persistKey]);
} }
private collectChildContainer = (ref: HTMLDivElement): void => { private collectChildContainer = (ref: HTMLDivElement): void => {
@ -179,7 +175,14 @@ export default class PersistedElement extends React.Component<IProps> {
</StrictMode> </StrictMode>
); );
ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey)); let rootPair = PersistedElement.rootMap[this.props.persistKey];
if (!rootPair) {
const container = getOrCreateContainer("mx_persistedElement_" + this.props.persistKey);
const root = createRoot(container);
rootPair = [root, container];
PersistedElement.rootMap[this.props.persistKey] = rootPair;
}
rootPair[0].render(content);
} }
private updateChildVisibility(child?: HTMLDivElement, visible = false): void { private updateChildVisibility(child?: HTMLDivElement, visible = false): void {

View File

@ -23,7 +23,7 @@ export interface IProps {
relation?: IEventRelation; relation?: IEventRelation;
} }
export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition, relation }) => { const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition, relation }) => {
const overflowMenuCloser = useContext(OverflowMenuContext); const overflowMenuCloser = useContext(OverflowMenuContext);
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();

View File

@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { getLocationShareErrorMessage, LocationShareError } from "../../../utils/location"; import { getLocationShareErrorMessage, LocationShareError } from "../../../utils/location";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
@ -29,7 +29,7 @@ export const MapError: React.FC<MapErrorProps> = ({ error, isMinimised, classNam
className={classNames("mx_MapError", className, { mx_MapError_isMinimised: isMinimised })} className={classNames("mx_MapError", className, { mx_MapError_isMinimised: isMinimised })}
onClick={onClick} onClick={onClick}
> >
<WarningBadge className="mx_MapError_icon" /> <ErrorIcon className="mx_MapError_icon" />
<Heading className="mx_MapError_heading" size="3"> <Heading className="mx_MapError_heading" size="3">
{_t("location_sharing|failed_load_map")} {_t("location_sharing|failed_load_map")}
</Heading> </Heading>

View File

@ -22,16 +22,6 @@ export function Map(props: ComponentProps<typeof MapComponent>): JSX.Element {
); );
} }
const LocationPickerComponent = lazy(() => import("./LocationPicker"));
export function LocationPicker(props: ComponentProps<typeof LocationPickerComponent>): JSX.Element {
return (
<Suspense fallback={<Spinner />}>
<LocationPickerComponent {...props} />
</Suspense>
);
}
const SmartMarkerComponent = lazy(() => import("./SmartMarker")); const SmartMarkerComponent = lazy(() => import("./SmartMarker"));
export function SmartMarker(props: ComponentProps<typeof SmartMarkerComponent>): JSX.Element { export function SmartMarker(props: ComponentProps<typeof SmartMarkerComponent>): JSX.Element {

View File

@ -98,25 +98,29 @@ export default class DateSeparator extends React.Component<IProps, IState> {
} }
private getLabel(): string { private getLabel(): string {
const date = new Date(this.props.ts); try {
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates); const date = new Date(this.props.ts);
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates);
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date // During the time the archive is being viewed, a specific day might not make sense, so we return the full date
if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(date); if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(date);
const today = new Date(); const today = new Date();
const yesterday = new Date(); const yesterday = new Date();
const days = getDaysArray("long"); const days = getDaysArray("long");
yesterday.setDate(today.getDate() - 1); yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) { if (date.toDateString() === today.toDateString()) {
return this.relativeTimeFormat.format(0, "day"); // Today return this.relativeTimeFormat.format(0, "day"); // Today
} else if (date.toDateString() === yesterday.toDateString()) { } else if (date.toDateString() === yesterday.toDateString()) {
return this.relativeTimeFormat.format(-1, "day"); // Yesterday return this.relativeTimeFormat.format(-1, "day"); // Yesterday
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { } else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()]; // Sunday-Saturday return days[date.getDay()]; // Sunday-Saturday
} else { } else {
return formatFullDateNoTime(date); return formatFullDateNoTime(date);
}
} catch {
return _t("common|message_timestamp_invalid");
} }
} }

View File

@ -13,8 +13,8 @@ import classNames from "classnames";
import * as HtmlUtils from "../../../HtmlUtils"; import * as HtmlUtils from "../../../HtmlUtils";
import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils"; import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils";
import { formatTime } from "../../../DateUtils"; import { formatTime } from "../../../DateUtils";
import { pillifyLinks, unmountPills } from "../../../utils/pillify"; import { pillifyLinks } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify"; import { tooltipifyLinks } from "../../../utils/tooltipify";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import RedactedBody from "./RedactedBody"; import RedactedBody from "./RedactedBody";
@ -23,6 +23,7 @@ import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource"; import ViewSource from "../../structures/ViewSource";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ReactRootManager } from "../../../utils/react";
function getReplacedContent(event: MatrixEvent): IContent { function getReplacedContent(event: MatrixEvent): IContent {
const originalContent = event.getOriginalContent(); const originalContent = event.getOriginalContent();
@ -47,8 +48,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
public declare context: React.ContextType<typeof MatrixClientContext>; public declare context: React.ContextType<typeof MatrixClientContext>;
private content = createRef<HTMLDivElement>(); private content = createRef<HTMLDivElement>();
private pills: Element[] = []; private pills = new ReactRootManager();
private tooltips: Element[] = []; private tooltips = new ReactRootManager();
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -103,7 +104,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
private tooltipifyLinks(): void { private tooltipifyLinks(): void {
// not present for redacted events // not present for redacted events
if (this.content.current) { if (this.content.current) {
tooltipifyLinks(this.content.current.children, this.pills, this.tooltips); tooltipifyLinks(this.content.current.children, this.pills.elements, this.tooltips);
} }
} }
@ -113,8 +114,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
unmountPills(this.pills); this.pills.unmount();
unmountTooltips(this.tooltips); this.tooltips.unmount();
const event = this.props.mxEvent; const event = this.props.mxEvent;
event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged); event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
} }

View File

@ -27,17 +27,17 @@ import {
OverflowHorizontalIcon, OverflowHorizontalIcon,
ReplyIcon, ReplyIcon,
DeleteIcon, DeleteIcon,
RestartIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons"; } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg"; import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg";
import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg"; import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg";
import { Icon as ResendIcon } from "../../../../res/img/element-icons/retry.svg";
import { Icon as ThreadIcon } from "../../../../res/img/element-icons/message/thread.svg"; import { Icon as ThreadIcon } from "../../../../res/img/element-icons/message/thread.svg";
import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg"; import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg";
import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg"; import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg";
import type { Relations } from "matrix-js-sdk/src/matrix"; import type { Relations } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import dis, { defaultDispatcher } from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import { isContentActionable, canEditContent, editEvent, canCancel } from "../../../utils/EventUtils"; import { isContentActionable, canEditContent, editEvent, canCancel } from "../../../utils/EventUtils";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
@ -323,7 +323,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
dis.dispatch({ defaultDispatcher.dispatch({
action: "reply_to_event", action: "reply_to_event",
event: this.props.mxEvent, event: this.props.mxEvent,
context: this.context.timelineRenderingType, context: this.context.timelineRenderingType,
@ -475,14 +475,14 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
0, 0,
0, 0,
<RovingAccessibleButton <RovingAccessibleButton
className="mx_MessageActionBar_iconButton" className="mx_MessageActionBar_iconButton mx_MessageActionBar_retryButton"
title={_t("action|retry")} title={_t("action|retry")}
onClick={this.onResendClick} onClick={this.onResendClick}
onContextMenu={this.onResendClick} onContextMenu={this.onResendClick}
key="resend" key="resend"
placement="left" placement="left"
> >
<ResendIcon /> <RestartIcon />
</RovingAccessibleButton>, </RovingAccessibleButton>,
); );

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