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"],
},
},
{
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: {
react: {

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ on:
jobs:
deploy:
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
steps:
- name: 📝 Create Deployment

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,6 @@
"*": "prettier --write",
"src/**/*.(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"]
}

View File

@ -1,6 +1,6 @@
module.exports = {
extends: ["stylelint-config-standard"],
customSyntax: require("postcss-scss"),
customSyntax: "postcss-scss",
plugins: ["stylelint-scss"],
rules: {
"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)
==================================================================================================
## ✨ Features

View File

@ -38,7 +38,7 @@ const config: Config = {
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
},
transformIgnorePatterns: ["/node_modules/(?!matrix-js-sdk).+$"],
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
collectCoverageFrom: [
"<rootDir>/src/**/*.{js,ts,tsx}",
// 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",
"version": "1.11.84",
"version": "1.11.85",
"description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.",
"repository": {
@ -35,7 +35,7 @@
"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",
"make-component": "node scripts/make-react-component.js",
"rethemendex": "res/css/rethemendex.sh",
"rethemendex": "./res/css/rethemendex.sh",
"clean": "rimraf lib webapp",
"build": "yarn clean && yarn build:genfiles && yarn build:bundle",
"build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
@ -45,23 +45,20 @@
"build:bundle": "webpack --progress --mode production",
"build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json",
"build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts",
"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: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:js": "webpack serve --output-path webapp --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js --mode development",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows",
"lint:js": "yarn lint:js:src && yarn lint:js:module_system",
"lint:js:src": "eslint --max-warnings 0 src test playwright && prettier --check .",
"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:js": "eslint --max-warnings 0 src test playwright module_system && prettier --check .",
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src test playwright 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:module_system": "tsc --noEmit --project ./tsconfig.module_system.json",
"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:knip": "knip",
"test": "jest",
"test:playwright": "playwright test",
"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"
},
"resolutions": {
"@types/seedrandom": "3.0.8",
"oidc-client-ts": "3.1.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": "npm:wrap-ansi@^7.0.0"
},
@ -96,7 +92,7 @@
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
"await-lock": "^2.1.0",
"bloom-filters": "^3.0.1",
"bloom-filters": "^3.0.2",
"blurhash": "^2.0.3",
"browserslist": "^4.23.2",
"classnames": "^2.2.6",
@ -123,19 +119,20 @@
"linkify-string": "4.1.3",
"linkifyjs": "4.1.3",
"lodash": "^4.17.21",
"maplibre-gl": "^2.0.0",
"maplibre-gl": "^4.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.9.0",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
"oidc-client-ts": "^3.0.1",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.157.2",
"qrcode": "1.5.4",
"re-resizable": "6.9.17",
"re-resizable": "6.10.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.3.0",
@ -155,11 +152,9 @@
"@action-validator/cli": "^0.6.0",
"@action-validator/core": "^0.6.0",
"@axe-core/playwright": "^4.8.1",
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^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-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-class-properties": "^7.12.1",
@ -172,7 +167,6 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@peculiar/webcrypto": "^1.4.3",
@ -186,7 +180,6 @@
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/commonmark": "^0.27.4",
"@types/content-type": "^1.1.5",
"@types/counterpart": "^0.18.1",
"@types/css-tree": "^2.3.8",
"@types/diff-match-patch": "^1.0.32",
@ -211,7 +204,6 @@
"@types/react-dom": "18.3.1",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.13.0",
"@types/sdp-transform": "^2.4.6",
"@types/seedrandom": "3.0.8",
"@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5",
@ -219,7 +211,6 @@
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"axe-core": "4.10.2",
"babel-jest": "^29.0.0",
"babel-loader": "^9.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@ -259,14 +250,12 @@
"jest-mock": "^29.6.2",
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"knip": "^5.36.2",
"lint-staged": "^15.0.2",
"mailhog": "^4.16.0",
"matrix-mock-request": "^2.5.0",
"matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.0",
"minimist": "^1.2.6",
"mkdirp": "^3.0.0",
"mocha-junit-reporter": "^2.2.0",
"modernizr": "^3.12.0",
"node-fetch": "^2.6.7",
"playwright-core": "^1.45.1",
@ -276,7 +265,7 @@
"postcss-import": "16.1.0",
"postcss-loader": "8.1.1",
"postcss-mixins": "^11.0.0",
"postcss-nested": "^6.0.0",
"postcss-nested": "^7.0.0",
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",

View File

@ -357,9 +357,9 @@ test.describe("Threads", () => {
await bot.joinRoom(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 =
".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");
// 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.
*/
import * as fs from "node:fs";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Credentials } from "../../plugins/homeserver";
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
const STICKER_NAME = "Test Sticker";
const ROOM_NAME_1 = "Sticker Test";
const ROOM_NAME_2 = "Sticker Test Two";
const STICKER_MESSAGE = JSON.stringify({
action: "m.sticker",
api: "fromWidget",
data: {
name: "teststicker",
description: STICKER_NAME,
file: "test.png",
content: {
body: STICKER_NAME,
msgtype: "m.sticker",
url: "mxc://localhost/somewhere",
const STICKER_IMAGE = fs.readFileSync("playwright/sample-files/riot.png");
function getStickerMessage(contentUri: string, mimetype: string): string {
return JSON.stringify({
action: "m.sticker",
api: "fromWidget",
data: {
name: "teststicker",
description: STICKER_NAME,
file: "test.png",
content: {
body: STICKER_NAME,
info: {
h: 480,
mimetype: mimetype,
size: 13818,
w: 480,
},
msgtype: "m.sticker",
url: contentUri,
},
},
},
requestId: "1",
widgetId: STICKER_PICKER_WIDGET_ID,
});
const WIDGET_HTML = `
requestId: "1",
widgetId: STICKER_PICKER_WIDGET_ID,
});
}
function getWidgetHtml(contentUri: string, mimetype: string) {
const stickerMessage = getStickerMessage(contentUri, mimetype);
return `
<html lang="en">
<head>
<title>Fake Sticker Picker</title>
@ -51,13 +67,13 @@ const WIDGET_HTML = `
<button name="Send" id="sendsticker">Press for sticker</button>
<script>
document.getElementById('sendsticker').onclick = () => {
window.parent.postMessage(${STICKER_MESSAGE}, '*')
window.parent.postMessage(${stickerMessage}, '*')
};
</script>
</body>
</html>
`;
}
async function openStickerPicker(app: ElementAppPage) {
const options = await app.openMessageComposerOptions();
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();
}
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
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.
await expect(page.locator(`img[alt="${STICKER_NAME}"]`)).toHaveAttribute(
"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.use({
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
@ -96,34 +143,19 @@ test.describe("Stickers", () => {
//
// See sendStickerFromPicker() for more detail on iframe comms.
let stickerPickerUrl: string;
test.beforeEach(async ({ webserver }) => {
stickerPickerUrl = webserver.start(WIDGET_HTML);
});
test("should send a sticker to multiple rooms", async ({ page, app, user }) => {
const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 });
test("should send a sticker to multiple rooms", async ({ webserver, page, app, user, room }) => {
const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 });
await app.client.setAccountData("m.widgets", {
[STICKER_PICKER_WIDGET_ID]: {
content: {
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,
},
});
const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" });
const widgetHtml = getWidgetHtml(contentUri, "image/png");
stickerPickerUrl = webserver.start(widgetHtml);
setWidgetAccountData(app, user, stickerPickerUrl);
await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${roomId1}`);
await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app);
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
// goes to the right place
@ -131,31 +163,40 @@ test.describe("Stickers", () => {
await expect(page).toHaveURL(`/#/room/${roomId2}`);
await openStickerPicker(app);
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 }) => {
const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 });
await app.client.setAccountData("m.widgets", {
[STICKER_PICKER_WIDGET_ID]: {
content: {
type: "m.stickerpicker",
name: STICKER_PICKER_WIDGET_NAME,
url: stickerPickerUrl,
// No creatorUserId
},
sender: user.userId,
state_key: STICKER_PICKER_WIDGET_ID,
type: "m.widget",
id: STICKER_PICKER_WIDGET_ID,
},
});
test("should handle a sticker picker widget missing creatorUserId", async ({
webserver,
page,
app,
user,
room,
}) => {
const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" });
const widgetHtml = getWidgetHtml(contentUri, "image/png");
stickerPickerUrl = webserver.start(widgetHtml);
setWidgetAccountData(app, user, stickerPickerUrl, false);
await app.viewRoomByName(ROOM_NAME_1);
await expect(page).toHaveURL(`/#/room/${roomId1}`);
await expect(page).toHaveURL(`/#/room/${room.roomId}`);
await openStickerPicker(app);
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.
// 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.
const DOCKER_TAG = "develop@sha256:6c33604ee62f009f3b34454a3c3e85f7e3ff5de63e45011fcd79e0ddc54a4e51";
const DOCKER_TAG = "develop@sha256:b7d8089c4593d4aa12834d04849971717b17254a76257e7c5cd433a16d5e966e";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template);

View File

@ -102,3 +102,5 @@ experimental_features:
# messages > non-joined historical messages.
# Can be removed after Synapse enables it by default
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 */
input[type="text"].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, */

View File

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

View File

@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details.
width: 18px;
height: 18px;
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-repeat: no-repeat;
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 */
&::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 {
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-position: center;
mask-size: 50px;

View File

@ -77,7 +77,7 @@ Please see LICENSE files in the repository root for full details.
height: 16px;
width: 16px;
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-repeat: no-repeat;
}
@ -121,7 +121,7 @@ Please see LICENSE files in the repository root for full details.
background-color: $tertiary-content;
mask-size: 16px;
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 {

View File

@ -48,7 +48,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain;
mask-repeat: no-repeat;
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);
}
@ -169,7 +169,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: 20px;
mask-repeat: no-repeat;
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 {

View File

@ -29,7 +29,7 @@ Please see LICENSE files in the repository root for full details.
}
.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 {
@ -61,7 +61,7 @@ Please see LICENSE files in the repository root for full details.
}
.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 {

View File

@ -125,7 +125,7 @@ Please see LICENSE files in the repository root for full details.
mask-repeat: no-repeat;
mask-position: center;
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;
height: 18px;
left: 0;

View File

@ -36,9 +36,24 @@ Please see LICENSE files in the repository root for full details.
}
.mx_AnalyticsLearnMore_bullets li {
background: url("$(res)/img/tick-circle.svg") no-repeat;
list-style-type: none;
padding: 2px 0px 20px 32px;
padding: 2px 0 0 32px;
margin-bottom: 20px;
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 {
/* 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-color: transparent;
}
@ -120,7 +120,7 @@ Please see LICENSE files in the repository root for full details.
width: 16px;
left: 0;
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;
}

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ Please see LICENSE files in the repository root for full details.
width: 18px;
height: 18px;
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-repeat: no-repeat;
float: right;

View File

@ -26,9 +26,9 @@ Please see LICENSE files in the repository root for full details.
height: 16px;
width: 16px;
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-position: 7px center;
mask-position: center;
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;
top: calc(50% - 8px); /* center */
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-position: center;
width: 16px;
height: 16px;
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 {
visibility: visible;
}

View File

@ -42,7 +42,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain;
mask-repeat: no-repeat;
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"] {

View File

@ -160,7 +160,7 @@ Please see LICENSE files in the repository root for full details.
mask-size: contain;
mask-repeat: no-repeat;
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 {
@ -276,7 +276,7 @@ Please see LICENSE files in the repository root for full details.
.mx_RoomSublist_showMoreButtonChevron,
.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 {

View File

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

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 "@types/modernizr";
import type { Renderer } from "react-dom";
import type { logger } from "matrix-js-sdk/src/logger";
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
@ -44,6 +43,7 @@ import AutoRageshakeStore from "../stores/AutoRageshakeStore";
import { IConfigOptions } from "../IConfigOptions";
import { MatrixDispatcher } from "../dispatcher/dispatcher";
import { DeepReadonly } from "./common";
import MatrixChat from "../components/structures/MatrixChat";
/* eslint-disable @typescript-eslint/naming-convention */
@ -71,7 +71,7 @@ declare global {
interface Window {
mxSendRageshake: (text: string, withLogs?: boolean) => void;
matrixLogger: typeof logger;
matrixChat: ReturnType<Renderer>;
matrixChat?: MatrixChat;
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise<void>;
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise<void>;
mxAutoRageshakeStore?: AutoRageshakeStore;

View File

@ -10,13 +10,13 @@ Please see LICENSE files in the repository root for full details.
import {
IAddThreePidOnlyBody,
IAuthData,
IRequestMsisdnTokenResponse,
IRequestTokenResponse,
MatrixClient,
MatrixError,
HTTPError,
IThreepid,
UIAResponse,
} from "matrix-js-sdk/src/matrix";
import Modal from "./Modal";
@ -179,7 +179,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why
* 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 {
if (this.bind) {
const authClient = new IdentityAuthClient();
@ -220,7 +222,7 @@ export default class AddThreepid {
continueKind: "primary",
},
};
const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, {
const { finished } = Modal.createDialog(InteractiveAuthDialog<IAddThreePidOnlyBody>, {
title: _t("settings|general|add_email_dialog_title"),
matrixClient: this.matrixClient,
authData: err.data,
@ -263,7 +265,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why
* 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();
if (this.submitUrl) {
@ -319,7 +323,7 @@ export default class AddThreepid {
continueKind: "primary",
},
};
const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, {
const { finished } = Modal.createDialog(InteractiveAuthDialog<IAddThreePidOnlyBody>, {
title: _t("settings|general|add_msisdn_dialog_title"),
matrixClient: this.matrixClient,
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.
*/
import React, { ComponentType, PropsWithChildren } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import React, { ReactNode, Suspense } from "react";
import { _t } from "./languageHandler";
import BaseDialog from "./components/views/dialogs/BaseDialog";
import DialogButtons from "./components/views/elements/DialogButtons";
import Spinner from "./components/views/elements/Spinner";
type AsyncImport<T> = { default: T };
interface IProps {
// A promise which resolves with the real component
prom: Promise<ComponentType<any> | AsyncImport<ComponentType<any>>>;
onFinished(): void;
children: ReactNode;
}
interface IState {
component?: ComponentType<PropsWithChildren<any>>;
error?: Error;
}
@ -32,56 +27,26 @@ interface IState {
* spinner until the real component loads.
*/
export default class AsyncWrapper extends React.Component<IProps, IState> {
private unmounted = false;
public static getDerivedStateFromError(error: Error): IState {
return { error };
}
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 {
if (this.state.component) {
const Component = this.state.component;
return <Component {...this.props} />;
} else if (this.state.error) {
if (this.state.error) {
return (
<BaseDialog onFinished={this.props.onFinished} title={_t("common|error")}>
{_t("failed_load_async_component")}
<DialogButtons
primaryButton={_t("action|dismiss")}
onPrimaryButtonClick={this.onWrapperCancelClick}
onPrimaryButtonClick={this.props.onFinished}
hasCancel={false}
/>
</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.
*/
// @ts-ignore - `.ts` is needed here to make TS happy
import { Request, Response } from "./workers/blurhash.worker.ts";
import { WorkerManager } from "./WorkerManager";
import blurhashWorkerFactory from "./workers/blurhashWorkerFactory";

View File

@ -536,9 +536,7 @@ export default class ContentMessages {
attachMentions(matrixClient.getSafeUserId(), content, null, replyToEvent);
attachRelation(content, relation);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, {
includeLegacyFallback: false,
});
addReplyToMessageContent(content, replyToEvent);
}
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 { Glass, TooltipProvider } from "@vector-im/compound-web";
import dis, { defaultDispatcher } from "./dispatcher/dispatcher";
import defaultDispatcher from "./dispatcher/dispatcher";
import AsyncWrapper from "./AsyncWrapper";
import { Defaultize } from "./@types/common";
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;
}
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.
* 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();
}
/**
* @typeParam C - the component type
*/
private buildModal<C extends ComponentType>(
prom: Promise<C>,
Component: C,
props?: ComponentProps<C>,
className?: string,
options?: IOptions<C>,
@ -222,9 +199,12 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// otherwise we'll get confused.
const modalCount = this.counter++;
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click!
modal.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />;
// Typescript doesn't like us passing props as any here, but we know that they are well typed due to the rigorous generics.
modal.elem = (
<AsyncWrapper key={modalCount} onFinished={closeDialog}>
<Component {...(props as any)} onFinished={closeDialog} />
</AsyncWrapper>
);
modal.close = closeDialog;
return { modal, closeDialog, onFinishedProm };
@ -291,29 +271,30 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
* require(['<module>'], cb);
* }
*
* @param {Promise} prom a promise which resolves with a React component
* which will be displayed as the modal view.
* @param component The component to render as a dialog. This component must accept an `onFinished` prop function as
* 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
* component. (We will also pass an 'onFinished' property.)
* @param props properties to pass to the displayed 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.
* Also, when closed, all modals will be removed
* 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
* also be removed from the stack. This is not compatible
* with being a priority modal. Only one modal can be
* static at a time.
* @param {Object} options? extra options for the dialog
* @param {onBeforeClose} 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
* @param options? extra options for the dialog
* @param options.onBeforeClose a callback to decide whether to close the dialog
* @returns Object with 'close' parameter being a function that will close the dialog
*/
public createDialogAsync<C extends ComponentType>(
prom: Promise<C>,
public createDialog<C extends ComponentType>(
component: C,
props?: ComponentProps<C>,
className?: string,
isPriorityModal = false,
@ -321,7 +302,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
options: IOptions<C> = {},
): IHandle<C> {
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) {
// XXX: This is destructive
this.priorityModal = modal;
@ -341,13 +322,13 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
};
}
private appendDialogAsync<C extends ComponentType>(
prom: Promise<C>,
public appendDialog<C extends ComponentType>(
component: C,
props?: ComponentProps<C>,
className?: string,
): IHandle<C> {
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);
@ -396,7 +377,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
// If there is no modal to render, make all of Element available
// to screen reader users again
dis.dispatch({
defaultDispatcher.dispatch({
action: "aria_unhide_main_app",
});
ModalManager.getOrCreateRoot().render(<></>);
@ -407,7 +388,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// 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
// screen reader specific features
dis.dispatch({
defaultDispatcher.dispatch({
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.
*/
// @ts-ignore - `.ts` is needed here to make TS happy
import { Request, Response } from "./workers/playback.worker";
import { WorkerManager } from "./WorkerManager";
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.
*/
import { lazy } from "react";
import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix";
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog";
import Modal from "./Modal";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from "./languageHandler";
@ -232,10 +232,8 @@ async function doAccessSecretStorage(func: () => Promise<void>, forceReset: bool
if (createNew) {
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createDialogAsync(
import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise<
typeof CreateSecretStorageDialog
>,
const { finished } = Modal.createDialog(
lazy(() => import("./async-components/views/dialogs/security/CreateSecretStorageDialog")),
{
forceReset,
},

View File

@ -28,7 +28,7 @@ interface NewRecoveryMethodDialogProps {
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.

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.
*/
import React from "react";
import React, { lazy } from "react";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal, { ComponentType } from "../../../../Modal";
import Modal from "../../../../Modal";
import { Action } from "../../../../dispatcher/actions";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
@ -28,8 +28,8 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
private onSetupClick = (): void => {
this.props.onFinished();
Modal.createDialogAsync(
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType>,
Modal.createDialog(
lazy(() => import("./CreateKeyBackupDialog")),
undefined,
undefined,
/* priority = */ false,

View File

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

View File

@ -49,11 +49,10 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandl
import AudioFeedArrayForLegacyCall from "../views/voip/AudioFeedArrayForLegacyCall";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import RoomView from "./RoomView";
import type { RoomView as RoomViewType } from "./RoomView";
import { RoomView } from "./RoomView";
import ToastContainer from "./ToastContainer";
import UserView from "./UserView";
import BackdropPanel from "./BackdropPanel";
import { BackdropPanel } from "./BackdropPanel";
import { mediaFromMxc } from "../../customisations/Media";
import { UserTab } from "../views/dialogs/UserTab";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
@ -125,7 +124,7 @@ class LoggedInView extends React.Component<IProps, IState> {
public static displayName = "LoggedInView";
protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<RoomViewType>;
protected readonly _roomView: React.RefObject<RoomView>;
protected readonly _resizeContainer: React.RefObject<HTMLDivElement>;
protected readonly resizeHandler: React.RefObject<HTMLDivElement>;
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.
*/
import React, { createRef } from "react";
import React, { createRef, lazy } from "react";
import {
ClientEvent,
createClient,
@ -28,8 +28,6 @@ import { TooltipProvider } from "@vector-im/compound-web";
// what-input helps improve keyboard accessibility
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 { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg";
@ -1649,16 +1647,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
if (haveNewVersion) {
Modal.createDialogAsync(
import(
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
) as unknown as Promise<typeof NewRecoveryMethodDialog>,
Modal.createDialog(
lazy(() => import("../../async-components/views/dialogs/security/NewRecoveryMethodDialog")),
);
} else {
Modal.createDialogAsync(
import(
"../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"
) as unknown as Promise<typeof RecoveryMethodRemovedDialog>,
Modal.createDialog(
lazy(() => import("../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog")),
);
}
});

View File

@ -45,7 +45,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import ContentMessages from "../../ContentMessages";
import Modal from "../../Modal";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import dis, { defaultDispatcher } from "../../dispatcher/dispatcher";
import defaultDispatcher from "../../dispatcher/dispatcher";
import * as Rooms from "../../Rooms";
import MainSplit from "./MainSplit";
import RightPanel from "./RightPanel";
@ -437,7 +437,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onWidgetLayoutChange = (): void => {
if (!this.state.room) return;
dis.dispatch({
defaultDispatcher.dispatch({
action: "appsDrawer",
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
// ie: #/room/roomId/eventId (eventId of a thread message)
if (thread?.rootEvent && !initialEvent?.isThreadRoot) {
dis.dispatch<ShowThreadPayload>({
defaultDispatcher.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
@ -704,7 +704,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
if (activeCall === null) {
// We disconnected from the call, so stop viewing it
dis.dispatch<ViewRoomPayload>(
defaultDispatcher.dispatch<ViewRoomPayload>(
{
action: Action.ViewRoom,
room_id: this.state.roomId,
@ -850,7 +850,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public componentDidMount(): void {
this.unmounted = false;
this.dispatcherRef = dis.register(this.onAction);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
if (this.context.client) {
this.context.client.on(ClientEvent.Room, this.onRoom);
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
this.stopAllPermalinkCreators();
dis.unregister(this.dispatcherRef);
defaultDispatcher.unregister(this.dispatcherRef);
if (this.context.client) {
this.context.client.removeListener(ClientEvent.Room, this.onRoom);
this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
@ -1045,7 +1045,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
handled = true;
break;
case KeyBindingAction.UploadFile: {
dis.dispatch(
defaultDispatcher.dispatch(
{
action: "upload_file",
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 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.
dis.dispatch<ViewRoomPayload>({
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: payload.event.getRoomId(),
metricsTrigger: undefined,
@ -1188,7 +1188,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
// re-dispatch to the correct composer
dis.dispatch<ComposerInsertPayload>({
defaultDispatcher.dispatch<ComposerInsertPayload>({
...(payload as ComposerInsertPayload),
timelineRenderingType,
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
@ -1197,7 +1197,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
case Action.FocusAComposer: {
dis.dispatch<FocusComposerPayload>({
defaultDispatcher.dispatch<FocusComposerPayload>({
...(payload as FocusComposerPayload),
// 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,
@ -1303,7 +1303,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
// For initial threads launch, chat effects are disabled see #19731
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(),
});
dis.dispatch<ActionPayload>({ action: Action.RoomLoaded });
defaultDispatcher.dispatch<ActionPayload>({ action: Action.RoomLoaded });
};
private onRoomTimelineReset = (room?: Room): void => {
@ -1561,7 +1561,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onInviteClick = (): void => {
// open the room inviter
dis.dispatch({
defaultDispatcher.dispatch({
action: "view_invite",
roomId: this.getRoomId(),
});
@ -1572,7 +1572,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (this.context.client?.isGuest()) {
// Join this room once the user has registered and logged in
// (If we failed to peek, we may not have a valid room object.)
dis.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
defaultDispatcher.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
action: Action.DoAfterSyncPrepared,
deferred_action: {
action: Action.ViewRoom,
@ -1580,13 +1580,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
metricsTrigger: undefined,
},
});
dis.dispatch({ action: "require_registration" });
defaultDispatcher.dispatch({ action: "require_registration" });
} else {
Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl;
const roomId = this.getRoomId();
if (isNotUndefined(roomId)) {
dis.dispatch<JoinRoomPayload>({
defaultDispatcher.dispatch<JoinRoomPayload>({
action: Action.JoinRoom,
roomId,
opts: { inviteSignUrl: signUrl },
@ -1622,7 +1622,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.state.initialEventId === eventId
) {
debuglog("Removing scroll_into_view flag from initial event");
dis.dispatch<ViewRoomPayload>({
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.getRoomId(),
event_id: this.state.initialEventId,
@ -1638,7 +1638,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const roomId = this.getRoomId();
if (!this.context.client || !roomId) return;
if (this.context.client.isGuest()) {
dis.dispatch({ action: "require_registration" });
defaultDispatcher.dispatch({ action: "require_registration" });
return;
}
@ -1688,7 +1688,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private onForgetClick = (): void => {
dis.dispatch({
defaultDispatcher.dispatch({
action: "forget_room",
room_id: this.getRoomId(),
});
@ -1702,7 +1702,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
this.context.client?.leave(roomId).then(
() => {
dis.dispatch({ action: Action.ViewHomePage });
defaultDispatcher.dispatch({ action: Action.ViewHomePage });
this.setState({
rejecting: false,
});
@ -1736,7 +1736,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
await this.context.client!.setIgnoredUsers(ignoredUsers);
await this.context.client!.leave(this.state.roomId!);
dis.dispatch({ action: Action.ViewHomePage });
defaultDispatcher.dispatch({ action: Action.ViewHomePage });
this.setState({
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
// just ignore them.
// https://github.com/vector-im/vector-web/issues/1134
dis.fire(Action.ViewRoomDirectory);
defaultDispatcher.fire(Action.ViewRoomDirectory);
};
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
// an event will take care of both clearing the URL fragment and
// jumping to the bottom
dis.dispatch<ViewRoomPayload>({
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.getRoomId(),
metricsTrigger: undefined, // room doesn't change
@ -1794,7 +1794,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} else {
// Otherwise we have to jump manually
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 => {
const oldRoom = this.getOldRoom();
if (!oldRoom) return;
dis.dispatch<ViewRoomPayload>({
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: oldRoom.roomId,
metricsTrigger: "Predecessor",
@ -2001,7 +2001,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const roomId = this.getRoomId();
if (isNotUndefined(roomId)) {
dis.dispatch<SubmitAskToJoinPayload>({
defaultDispatcher.dispatch<SubmitAskToJoinPayload>({
action: Action.SubmitAskToJoin,
roomId,
opts: { reason },
@ -2018,7 +2018,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const roomId = this.getRoomId();
if (isNotUndefined(roomId)) {
dis.dispatch<CancelAskToJoinPayload>({
defaultDispatcher.dispatch<CancelAskToJoinPayload>({
action: Action.CancelAskToJoin,
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;
}
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
// 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
this.setReadMarker(rmId, rmTs);
await this.setReadMarker(rmId, rmTs);
// Send the receipts to the server immediately (don't wait for activity)
await this.sendReadReceipts();
@ -1866,7 +1866,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
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;
// 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
// run the render cycle before calling the callback, so that
// getReadMarkerPosition() returns the right thing.
this.setState(
{
readMarkerEventId: eventId,
},
this.props.onReadMarkerUpdated,
);
await new Promise<void>((resolve) => {
this.setState(
{
readMarkerEventId: eventId,
},
() => {
this.props.onReadMarkerUpdated?.();
resolve();
},
);
});
}
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 { Tooltip } from "@vector-im/compound-web";
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import AccessibleButton from "../../../views/elements/AccessibleButton";
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 { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
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>
<Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}>
<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")}
</AccessibleButton>
</Tooltip>

View File

@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import React, { ReactNode } from "react";
import { Tooltip } from "@vector-im/compound-web";
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../../languageHandler";
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 { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle";
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>
<Tooltip description={_t("auth|check_email_resend_tooltip")} placement="top" open={tooltipVisible}>
<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")}
</AccessibleButton>
</Tooltip>

View File

@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import dispatcher, { defaultDispatcher } from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { ConnectionState, ElementCall } from "../../../models/Call";
@ -53,7 +53,7 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
return;
}
dispatcher.dispatch<ViewRoomPayload>({
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
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 { sleep } from "matrix-js-sdk/src/utils";
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 BaseDialog from "./BaseDialog";
@ -34,7 +35,6 @@ import LazyRenderList from "../elements/LazyRenderList";
import { useSettingValue } from "../../../hooks/useSettings";
import { filterBoolean } from "../../../utils/arrays";
import { NonEmptyArray } from "../../../@types/common";
import WarningBadgeSvg from "../../../../res/img/element-icons/warning-badge.svg";
// These values match CSS
const ROW_HEIGHT = 32 + 12;
@ -229,7 +229,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
if (error) {
footer = (
<>
<img src={WarningBadgeSvg} height="24" width="24" alt="" />
<ErrorIcon height="24px" width="24px" />
<span className="mx_AddExistingToSpaceDialog_error">
<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.
*/
import React from "react";
import React, { lazy } from "react";
import { logger } from "matrix-js-sdk/src/logger";
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 dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
@ -116,10 +114,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
}
private onExportE2eKeysClicked = (): void => {
Modal.createDialogAsync(
import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise<
typeof ExportE2eKeysDialog
>,
Modal.createDialog(
lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
{
matrixClient: MatrixClientPeg.safeGet(),
},
@ -147,10 +143,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
/* static = */ true,
);
} else {
Modal.createDialogAsync(
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise<
typeof CreateKeyBackupDialog
>,
Modal.createDialog(
lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")),
undefined,
undefined,
/* priority = */ false,

View File

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

View File

@ -21,7 +21,7 @@ import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePrefer
import { NonEmptyArray } from "../../../@types/common";
import SettingsTab from "../settings/tabs/SettingsTab";
import { SettingsSection } from "../settings/shared/SettingsSection";
import SettingsSubsection, { SettingsSubsectionText } from "../settings/shared/SettingsSubsection";
import { SettingsSubsection, SettingsSubsectionText } from "../settings/shared/SettingsSubsection";
interface IProps {
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 { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix";
import { decodeRecoveryKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { decodeRecoveryKey, KeyBackupInfo, KeyBackupRestoreResult } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
@ -42,12 +41,11 @@ interface IProps {
interface IState {
backupInfo: KeyBackupInfo | null;
backupKeyStored: Record<string, SecretStorage.SecretStorageKeyDescription> | null;
loading: boolean;
loadError: boolean | null;
restoreError: unknown | null;
recoveryKey: string;
recoverInfo: IKeyBackupRestoreResult | null;
recoverInfo: KeyBackupRestoreResult | null;
recoveryKeyValid: boolean;
forceRecoveryKey: boolean;
passPhrase: string;
@ -72,7 +70,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
super(props);
this.state = {
backupInfo: null,
backupKeyStored: null,
loading: false,
loadError: null,
restoreError: null,
@ -137,7 +134,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
};
private onPassPhraseNext = async (): Promise<void> => {
if (!this.state.backupInfo) return;
const crypto = MatrixClientPeg.safeGet().getCrypto();
if (!crypto) return;
this.setState({
loading: true,
restoreError: null,
@ -146,13 +144,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
try {
// 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.
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithPassword(
this.state.passPhrase,
undefined,
undefined,
this.state.backupInfo,
{ progressCallback: this.progressCallback },
);
const recoverInfo = await crypto.restoreKeyBackupWithPassphrase(this.state.passPhrase, {
progressCallback: this.progressCallback,
});
if (!this.props.showSummary) {
this.props.onFinished(true);
@ -172,7 +166,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
};
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({
loading: true,
@ -180,13 +175,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
restoreType: RestoreType.RecoveryKey,
});
try {
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey,
undefined,
undefined,
this.state.backupInfo,
{ progressCallback: this.progressCallback },
await crypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(this.state.recoveryKey),
this.state.backupInfo.version,
);
const recoverInfo = await crypto.restoreKeyBackup({
progressCallback: this.progressCallback,
});
if (!this.props.showSummary) {
this.props.onFinished(true);
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({
loading: true,
restoreError: null,
restoreType: RestoreType.SecretStorage,
});
try {
let recoverInfo: KeyBackupRestoreResult | null = null;
// `accessSecretStorage` may prompt for storage access as needed.
await accessSecretStorage(async (): Promise<void> => {
if (!this.state.backupInfo) return;
await MatrixClientPeg.safeGet().restoreKeyBackupWithSecretStorage(
this.state.backupInfo,
undefined,
undefined,
{ progressCallback: this.progressCallback },
);
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
recoverInfo = await crypto.restoreKeyBackup({ progressCallback: this.progressCallback });
});
this.setState({
loading: false,
recoverInfo,
});
return true;
} catch (e) {
logger.log("Error restoring backup", e);
logger.log("restoreWithSecretStorage failed:", e);
this.setState({
restoreError: e,
loading: false,
});
return false;
}
}
private async restoreWithCachedKey(backupInfo: KeyBackupInfo | null): Promise<boolean> {
if (!backupInfo) return false;
const crypto = MatrixClientPeg.safeGet().getCrypto();
if (!crypto) return false;
try {
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithCache(
undefined /* targetRoomId */,
undefined /* targetSessionId */,
backupInfo,
{ progressCallback: this.progressCallback },
);
const recoverInfo = await crypto.restoreKeyBackup({ progressCallback: this.progressCallback });
this.setState({
recoverInfo,
});
@ -270,7 +263,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null;
this.setState({
backupInfo,
backupKeyStored,
});
const gotCache = await this.restoreWithCachedKey(backupInfo);
@ -282,9 +274,13 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
return;
}
// If the backup key is stored, we can proceed directly to restore.
if (backupKeyStored) {
return this.restoreWithSecretStorage();
const hasBackupFromSS = backupKeyStored && (await this.restoreWithSecretStorage());
if (hasBackupFromSS) {
logger.log("RestoreKeyBackupDialog: found backup key in secret storage");
this.setState({
loading: false,
});
return;
}
this.setState({
@ -398,6 +394,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
<input
data-testid="passphraseInput"
type="password"
className="mx_RestoreKeyBackupDialog_passPhraseInput"
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.
*/
import React, { createRef, CSSProperties } from "react";
import React, { createRef, CSSProperties, useRef, useState } from "react";
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 MemberAvatar from "../avatars/MemberAvatar";
@ -30,6 +30,9 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { presentableTextForFile } from "../../../utils/FileUtils";
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
const MAX_SCALE = 0.95;
@ -309,15 +312,6 @@ export default class ImageView extends React.Component<IProps, IState> {
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 => {
this.setState({
contextMenuDisplayed: true,
@ -555,11 +549,7 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("lightbox|rotate_right")}
onClick={this.onRotateClockwiseClick}
/>
<AccessibleButton
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("action|download")}
onClick={this.onDownloadClick}
/>
<DownloadButton url={this.props.src} fileName={this.props.name} />
{contextMenuButton}
<AccessibleButton
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 ReactDOM from "react-dom";
import { createRoot, Root } from "react-dom/client";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
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
// CSS stacking context, and thus be able to control their z-indexes relative to each other.
function getOrCreateMasterContainer(): HTMLDivElement {
let container = getContainer("mx_PersistedElement_container");
let container = document.getElementById("mx_PersistedElement_container") as HTMLDivElement;
if (!container) {
container = document.createElement("div");
container.id = "mx_PersistedElement_container";
@ -34,18 +34,10 @@ function getOrCreateMasterContainer(): HTMLDivElement {
return container;
}
function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId) as HTMLDivElement;
}
function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId);
if (!container) {
container = document.createElement("div");
container.id = containerId;
getOrCreateMasterContainer().appendChild(container);
}
const container = document.createElement("div");
container.id = containerId;
getOrCreateMasterContainer().appendChild(container);
return container;
}
@ -83,6 +75,8 @@ export default class PersistedElement extends React.Component<IProps> {
private childContainer?: HTMLDivElement;
private child?: HTMLDivElement;
private static rootMap: Record<string, [root: Root, container: Element]> = {};
public constructor(props: IProps) {
super(props);
@ -99,14 +93,16 @@ export default class PersistedElement extends React.Component<IProps> {
* @param {string} persistKey Key used to uniquely identify this PersistedElement
*/
public static destroyElement(persistKey: string): void {
const container = getContainer("mx_persistedElement_" + persistKey);
if (container) {
container.remove();
const pair = PersistedElement.rootMap[persistKey];
if (pair) {
pair[0].unmount();
pair[1].remove();
}
delete PersistedElement.rootMap[persistKey];
}
public static isMounted(persistKey: string): boolean {
return Boolean(getContainer("mx_persistedElement_" + persistKey));
return Boolean(PersistedElement.rootMap[persistKey]);
}
private collectChildContainer = (ref: HTMLDivElement): void => {
@ -179,7 +175,14 @@ export default class PersistedElement extends React.Component<IProps> {
</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 {

View File

@ -23,7 +23,7 @@ export interface IProps {
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 [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 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 { getLocationShareErrorMessage, LocationShareError } from "../../../utils/location";
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 })}
onClick={onClick}
>
<WarningBadge className="mx_MapError_icon" />
<ErrorIcon className="mx_MapError_icon" />
<Heading className="mx_MapError_heading" size="3">
{_t("location_sharing|failed_load_map")}
</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"));
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 {
const date = new Date(this.props.ts);
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates);
try {
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
if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(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);
const today = new Date();
const yesterday = new Date();
const days = getDaysArray("long");
yesterday.setDate(today.getDate() - 1);
const today = new Date();
const yesterday = new Date();
const days = getDaysArray("long");
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return this.relativeTimeFormat.format(0, "day"); // Today
} else if (date.toDateString() === yesterday.toDateString()) {
return this.relativeTimeFormat.format(-1, "day"); // Yesterday
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()]; // Sunday-Saturday
} else {
return formatFullDateNoTime(date);
if (date.toDateString() === today.toDateString()) {
return this.relativeTimeFormat.format(0, "day"); // Today
} else if (date.toDateString() === yesterday.toDateString()) {
return this.relativeTimeFormat.format(-1, "day"); // Yesterday
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()]; // Sunday-Saturday
} else {
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 { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils";
import { formatTime } from "../../../DateUtils";
import { pillifyLinks, unmountPills } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify";
import { pillifyLinks } from "../../../utils/pillify";
import { tooltipifyLinks } from "../../../utils/tooltipify";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import RedactedBody from "./RedactedBody";
@ -23,6 +23,7 @@ import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource";
import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ReactRootManager } from "../../../utils/react";
function getReplacedContent(event: MatrixEvent): IContent {
const originalContent = event.getOriginalContent();
@ -47,8 +48,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
public declare context: React.ContextType<typeof MatrixClientContext>;
private content = createRef<HTMLDivElement>();
private pills: Element[] = [];
private tooltips: Element[] = [];
private pills = new ReactRootManager();
private tooltips = new ReactRootManager();
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
@ -103,7 +104,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
private tooltipifyLinks(): void {
// not present for redacted events
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 {
unmountPills(this.pills);
unmountTooltips(this.tooltips);
this.pills.unmount();
this.tooltips.unmount();
const event = this.props.mxEvent;
event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
}

View File

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

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