diff --git a/.eslintrc.js b/.eslintrc.js index 3eefd22206..444388d492 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -142,6 +142,7 @@ module.exports = { "!matrix-js-sdk/src/models/read-receipt", "!matrix-js-sdk/src/models/relations-container", "!matrix-js-sdk/src/models/related-relations", + "!matrix-js-sdk/src/matrixrtc", ], message: "Please use matrix-js-sdk/src/matrix instead", }, diff --git a/.github/workflows/playwright-image-updates.yaml b/.github/workflows/playwright-image-updates.yaml index a160b77bcf..761d8a8b6c 100644 --- a/.github/workflows/playwright-image-updates.yaml +++ b/.github/workflows/playwright-image-updates.yaml @@ -9,14 +9,14 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Update matrixdotorg/synapse image + - name: Update synapse image run: | docker pull "$IMAGE" INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE") DIGEST=${INSPECT#*@} sed -i "s/const DOCKER_TAG.*/const DOCKER_TAG = \"develop@$DIGEST\";/" playwright/plugins/homeserver/synapse/index.ts env: - IMAGE: matrixdotorg/synapse:develop + IMAGE: ghcr.io/element-hq/synapse:develop - name: Create Pull Request id: cpr diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 94ed2c7488..204d3c46f4 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -36,7 +36,7 @@ jobs: - name: Switch js-sdk to release mode working-directory: node_modules/matrix-js-sdk run: | - scripts/switch_package_to_release.js + scripts/switch_package_to_release.cjs yarn install yarn run build:compile yarn run build:types diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..f0e9c9e0cb --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged --concurrent false diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 0000000000..58e673d4d2 --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,5 @@ +{ + "*": "prettier --write", + "*.(ts|tsx|js|jsx)": ["eslint --fix"], + "*.pcss": ["stylelint"] +} diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 100% rename from .prettierrc.js rename to .prettierrc.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 280d6dd562..2faa32bcec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,70 @@ +Changes in [3.108.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.108.0) (2024-08-27) +======================================================================================================= +## ✨ Features + +* Message Pinning: rework the message pinning list in the right panel ([#12825](https://github.com/matrix-org/matrix-react-sdk/pull/12825)). Contributed by @florianduros. +* Tweak UIA postMessage check to work cross-origin ([#12878](https://github.com/matrix-org/matrix-react-sdk/pull/12878)). Contributed by @t3chguy. +* Delayed events (Futures) / MSC4140 for call widget ([#12714](https://github.com/matrix-org/matrix-react-sdk/pull/12714)). Contributed by @AndrewFerr. +* Stop the ongoing ring if another device joins the call session. ([#12866](https://github.com/matrix-org/matrix-react-sdk/pull/12866)). Contributed by @toger5. +* Rich text Editor: Auto-replace plain text emoticons with emoji ([#12828](https://github.com/matrix-org/matrix-react-sdk/pull/12828)). Contributed by @langleyd. +* Clean up editor drafts for unknown rooms ([#12850](https://github.com/matrix-org/matrix-react-sdk/pull/12850)). Contributed by @langleyd. +* Rename general user settings to account ([#12841](https://github.com/matrix-org/matrix-react-sdk/pull/12841)). Contributed by @dbkr. +* Update settings tab icons ([#12867](https://github.com/matrix-org/matrix-react-sdk/pull/12867)). Contributed by @dbkr. +* Disable jump to read receipt button instead of hiding when nothing to jump to ([#12863](https://github.com/matrix-org/matrix-react-sdk/pull/12863)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* Ensure elements on Login page are disabled when in-flight ([#12895](https://github.com/matrix-org/matrix-react-sdk/pull/12895)). Contributed by @t3chguy. +* Hide pinned messages when grouped in timeline when feature pinning is disabled ([#12888](https://github.com/matrix-org/matrix-react-sdk/pull/12888)). Contributed by @florianduros. +* Add chat button on new room header for maximised widgets ([#12882](https://github.com/matrix-org/matrix-react-sdk/pull/12882)). Contributed by @t3chguy. +* Show spinner whilst initial search request is in progress ([#12883](https://github.com/matrix-org/matrix-react-sdk/pull/12883)). Contributed by @t3chguy. +* Fix user menu font ([#12879](https://github.com/matrix-org/matrix-react-sdk/pull/12879)). Contributed by @florianduros. +* Allow selecting text in the right panel topic ([#12870](https://github.com/matrix-org/matrix-react-sdk/pull/12870)). Contributed by @t3chguy. +* Add missing presence indicator to new room header ([#12865](https://github.com/matrix-org/matrix-react-sdk/pull/12865)). Contributed by @t3chguy. + + +Changes in [3.107.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.107.0) (2024-08-20) +======================================================================================================= +* No changes + + +Changes in [3.106.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.106.0) (2024-08-13) +======================================================================================================= +## ✨ Features + +* Invite dialog: display MXID on its own line ([#11756](https://github.com/matrix-org/matrix-react-sdk/pull/11756)). Contributed by @AndrewFerr. +* Align RoomSummaryCard styles with Figma ([#12793](https://github.com/matrix-org/matrix-react-sdk/pull/12793)). Contributed by @t3chguy. +* Extract Extensions into their own right panel tab ([#12844](https://github.com/matrix-org/matrix-react-sdk/pull/12844)). Contributed by @t3chguy. +* Remove topic from new room header and expand right panel topic ([#12842](https://github.com/matrix-org/matrix-react-sdk/pull/12842)). Contributed by @t3chguy. +* Rework how the onboarding notifications task works ([#12839](https://github.com/matrix-org/matrix-react-sdk/pull/12839)). Contributed by @t3chguy. +* Update toast styles to match Figma ([#12833](https://github.com/matrix-org/matrix-react-sdk/pull/12833)). Contributed by @t3chguy. +* Warn users on unsupported browsers before they lack features ([#12830](https://github.com/matrix-org/matrix-react-sdk/pull/12830)). Contributed by @t3chguy. +* Add sign out button to settings profile section ([#12666](https://github.com/matrix-org/matrix-react-sdk/pull/12666)). Contributed by @dbkr. +* Remove MatrixRTC realted import ES lint exceptions using a index.ts for matrixrtc ([#12780](https://github.com/matrix-org/matrix-react-sdk/pull/12780)). Contributed by @toger5. +* Fix unwanted ringing of other devices even though the user is already connected to the call. ([#12742](https://github.com/matrix-org/matrix-react-sdk/pull/12742)). Contributed by @toger5. +* Acknowledge `DeviceMute` widget actions ([#12790](https://github.com/matrix-org/matrix-react-sdk/pull/12790)). Contributed by @toger5. + +## 🐛 Bug Fixes + +* Fix formatting of rich text emotes ([#12862](https://github.com/matrix-org/matrix-react-sdk/pull/12862)). Contributed by @dbkr. +* Fixed custom emotes background color #27745 ([#12798](https://github.com/matrix-org/matrix-react-sdk/pull/12798)). Contributed by @asimdelvi. +* Ignore permalink\_prefix when serializing pills ([#11726](https://github.com/matrix-org/matrix-react-sdk/pull/11726)). Contributed by @herkulessi. +* Deflake the chat export test ([#12854](https://github.com/matrix-org/matrix-react-sdk/pull/12854)). Contributed by @dbkr. +* Fix alignment of RTL messages ([#12837](https://github.com/matrix-org/matrix-react-sdk/pull/12837)). Contributed by @dbkr. +* Handle media download errors better ([#12848](https://github.com/matrix-org/matrix-react-sdk/pull/12848)). Contributed by @t3chguy. +* Make micIcon display on primary ([#11908](https://github.com/matrix-org/matrix-react-sdk/pull/11908)). Contributed by @kdanielm. +* Fix compound typography font component issues ([#12826](https://github.com/matrix-org/matrix-react-sdk/pull/12826)). Contributed by @t3chguy. +* Allow Chrome page translator to translate messages in rooms ([#11113](https://github.com/matrix-org/matrix-react-sdk/pull/11113)). Contributed by @lukaszpolowczyk. + + +Changes in [3.105.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.105.1) (2024-08-06) +======================================================================================================= +Fixes for CVE-2024-42347 / GHSA-f83w-wqhc-cfp4 + +Changes in [3.105.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.105.0) (2024-08-06) +======================================================================================================= +Fixes for CVE-2024-42347 / GHSA-f83w-wqhc-cfp4 + Changes in [3.104.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.104.0) (2024-07-30) ======================================================================================================= ## ✨ Features diff --git a/babel.config.js b/babel.config.js index a9e4b5c137..541c567770 100644 --- a/babel.config.js +++ b/babel.config.js @@ -10,15 +10,15 @@ module.exports = { "last 2 Safari versions", "last 2 Edge versions", ], + include: ["@babel/plugin-transform-class-properties"], }, ], - "@babel/preset-typescript", + ["@babel/preset-typescript", { allowDeclareFields: true }], "@babel/preset-react", ], plugins: [ "@babel/plugin-proposal-export-default-from", "@babel/plugin-transform-numeric-separator", - "@babel/plugin-transform-class-properties", "@babel/plugin-transform-object-rest-spread", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-runtime", diff --git a/docs/playwright.md b/docs/playwright.md index 1d00e9781a..7eae8e783d 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -29,7 +29,7 @@ need to have Docker installed and working in order to run the Playwright tests. There are a few different ways to run the tests yourself. The simplest is to run: ```shell -docker pull matrixdotorg/synapse:develop +docker pull ghcr.io/element-hq/synapse:develop yarn run test:playwright ``` diff --git a/package.json b/package.json index 2ed9141c8d..8f6a05b32b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "matrix-react-sdk", "version": "0.0.0", - "version-matrix": "3.104.0", + "version-matrix": "3.108.0", "description": "SDK for matrix.org using React for Tchap", "author": "DINUM", "repository": { @@ -70,32 +70,36 @@ "oidc-client-ts": "3.0.1", "jwt-decode": "4.0.0", "@floating-ui/react": "0.26.11", - "@radix-ui/react-id": "1.1.0" + "@radix-ui/react-id": "1.1.0", + "caniuse-lite": "1.0.30001643", + "electron-to-chromium": "1.5.2" }, "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.24.0", "@matrix-org/emojibase-bindings": "^1.1.2", - "@matrix-org/matrix-wysiwyg": "2.37.4", + "@matrix-org/matrix-wysiwyg": "2.37.8", "@matrix-org/olm": "3.2.15", "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@testing-library/react-hooks": "^8.0.1", - "@vector-im/compound-design-tokens": "^1.6.1", - "@vector-im/compound-web": "^5.4.0", + "@vector-im/compound-design-tokens": "^1.8.0", + "@vector-im/compound-web": "^5.5.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", "await-lock": "^2.1.0", "bloom-filters": "^3.0.1", "blurhash": "^2.0.3", + "browserslist": "^4.23.2", "classnames": "^2.2.6", "commonmark": "^0.31.0", "counterpart": "^0.18.6", "css-tree": "^2.3.1", "diff-dom": "^5.0.0", "diff-match-patch": "^1.0.5", + "electron-to-chromium": "^1.5.2", "emojibase-regex": "15.3.2", "escape-html": "^1.0.3", "file-saver": "^2.0.5", @@ -116,15 +120,15 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "34.2.0", - "matrix-widget-api": "^1.5.0", + "matrix-js-sdk": "34.4.0", + "matrix-widget-api": "^1.8.2", "memoize-one": "^6.0.0", "minimist": "^1.2.5", "oidc-client-ts": "^3.0.1", "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.145.0", + "posthog-js": "1.149.1", "qrcode": "1.5.3", "re-resizable": "^6.9.0", "react": "17.0.2", @@ -199,7 +203,7 @@ "@typescript-eslint/parser": "^7.0.0", "axe-core": "4.9.1", "babel-jest": "^29.0.0", - "blob-polyfill": "^7.0.0", + "blob-polyfill": "^9.0.0", "eslint": "8.57.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", @@ -216,26 +220,28 @@ "fetch-mock-jest": "^1.5.1", "fs-extra": "^11.0.0", "glob": "^11.0.0", + "husky": "^8.0.3", "jest": "^29.6.2", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.6.2", "jest-mock": "^29.6.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", + "lint-staged": "^15.0.2", "mailhog": "^4.16.0", "matrix-web-i18n": "^3.2.1", "mocha-junit-reporter": "^2.2.0", "node-fetch": "2", "playwright-core": "^1.45.1", "postcss-scss": "^4.0.4", - "prettier": "3.3.2", + "prettier": "3.3.3", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", "ts-node": "^10.9.1", - "typescript": "5.5.3", + "typescript": "5.5.4", "web-streams-polyfill": "^4.0.0" }, "peerDependencies": { diff --git a/playwright.config.ts b/playwright.config.ts index ba491ff82a..a9fcf2242d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -37,7 +37,7 @@ export default defineConfig({ }, testDir: "playwright/e2e", outputDir: "playwright/test-results", - workers: process.env.CI ? "50%" : 1, + workers: 1, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? [["blob"], ["github"]] : [["html", { outputFolder: "playwright/html-report" }]], snapshotDir: "playwright/snapshots", diff --git a/playwright/Dockerfile b/playwright/Dockerfile index c5a9cdbb81..29e7f83f1f 100644 --- a/playwright/Dockerfile +++ b/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.45.1-jammy +FROM mcr.microsoft.com/playwright:v1.45.3-jammy WORKDIR /work/matrix-react-sdk VOLUME ["/work/element-web/node_modules"] diff --git a/playwright/e2e/app-loading/feature-detection.spec.ts b/playwright/e2e/app-loading/feature-detection.spec.ts index 2acde32c37..0c78d9d01a 100644 --- a/playwright/e2e/app-loading/feature-detection.spec.ts +++ b/playwright/e2e/app-loading/feature-detection.spec.ts @@ -21,9 +21,9 @@ test(`shows error page if browser lacks Intl support`, async ({ page }) => { await page.goto("/"); // Lack of Intl support causes the app bundle to fail to load, so we get the iframed - // static error page and need to explicitly look in the iframe becuse Playwright doesn't + // static error page and need to explicitly look in the iframe because Playwright doesn't // recurse into iframes when looking for elements - const header = await page.frameLocator("iframe").getByText("Unsupported browser"); + const header = page.frameLocator("iframe").getByText("Unsupported browser"); await expect(header).toBeVisible(); await expect(page).toMatchScreenshot("unsupported-browser.png"); @@ -34,8 +34,8 @@ test(`shows error page if browser lacks WebAssembly support`, async ({ page }) = await page.goto("/"); // Lack of WebAssembly support doesn't cause the bundle to fail loading, so we get - // CompatibilityView, ie. no iframes. - const header = await page.getByText("Unsupported browser"); + // CompatibilityView, i.e. no iframes. + const header = page.getByText("Element does not support this browser"); await expect(header).toBeVisible(); await expect(page).toMatchScreenshot("unsupported-browser-CompatibilityView.png"); diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts index b142fcec4e..30c356f492 100644 --- a/playwright/e2e/chat-export/html-export.spec.ts +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -98,6 +98,10 @@ test.describe("HTML Export", () => { }); test("should export html successfully and match screenshot", async ({ page, app, room }) => { + // Set a fixed time rather than masking off the line with the time in it: we don't need to worry + // about the width changing and we can actually test this line looks correct. + page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); + // Send a bunch of messages to populate the room for (let i = 1; i < 10; i++) { await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); @@ -123,7 +127,6 @@ test.describe("HTML Export", () => { await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); await expect(page).toMatchScreenshot("html-export.png", { mask: [ - page.getByText("This is the start of export", { exact: false }), // We need to mask the whole thing because the width of the time part changes page.locator(".mx_TimelineSeparator"), page.locator(".mx_MessageTimestamp"), diff --git a/playwright/e2e/create-room/create-room.spec.ts b/playwright/e2e/create-room/create-room.spec.ts index 651439302d..c46de21da4 100644 --- a/playwright/e2e/create-room/create-room.spec.ts +++ b/playwright/e2e/create-room/create-room.spec.ts @@ -38,6 +38,5 @@ test.describe("Create Room", () => { await expect(page).toHaveURL(/\/#\/room\/#test-room-1:localhost/); const header = page.locator(".mx_RoomHeader"); await expect(header).toContainText(name); - await expect(header).toContainText(topic); }); }); diff --git a/playwright/e2e/file-upload/image-upload.spec.ts b/playwright/e2e/file-upload/image-upload.spec.ts index d75d20f441..b886bc2a1e 100644 --- a/playwright/e2e/file-upload/image-upload.spec.ts +++ b/playwright/e2e/file-upload/image-upload.spec.ts @@ -40,6 +40,6 @@ test.describe("Image Upload", () => { await expect(page.getByRole("button", { name: "Upload" })).toBeEnabled(); await expect(page.getByRole("button", { name: "Close dialog" })).toBeEnabled(); - await expect(page).toMatchScreenshot("image-upload-preview.png"); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("image-upload-preview.png"); }); }); diff --git a/playwright/e2e/integration-manager/utils.ts b/playwright/e2e/integration-manager/utils.ts index c6a2fb998e..0ea59e6ff7 100644 --- a/playwright/e2e/integration-manager/utils.ts +++ b/playwright/e2e/integration-manager/utils.ts @@ -19,8 +19,6 @@ import type { ElementAppPage } from "../../pages/ElementAppPage"; export async function openIntegrationManager(app: ElementAppPage) { const { page } = app; await app.toggleRoomInfoPanel(); - await page - .locator(".mx_RoomSummaryCard_appsGroup") - .getByRole("button", { name: "Add widgets, bridges & bots" }) - .click(); + await page.getByRole("tab", { name: "Extensions" }).click(); + await page.getByRole("button", { name: "Add extensions" }).click(); } diff --git a/playwright/e2e/messages/messages.spec.ts b/playwright/e2e/messages/messages.spec.ts new file mode 100644 index 0000000000..f34fa15e82 --- /dev/null +++ b/playwright/e2e/messages/messages.spec.ts @@ -0,0 +1,127 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* See readme.md for tips on writing these tests. */ + +import { Locator, Page } from "playwright-core"; + +import { test, expect } from "../../element-web-test"; + +async function sendMessage(page: Page, message: string): Promise { + await page.getByRole("textbox", { name: "Send a message…" }).fill(message); + await page.getByRole("button", { name: "Send message" }).click(); + + const msgTile = await page.locator(".mx_EventTile_last"); + await msgTile.locator(".mx_EventTile_receiptSent").waitFor(); + return msgTile; +} + +async function editMessage(page: Page, message: Locator, newMsg: string): Promise { + const line = message.locator(".mx_EventTile_line"); + await line.hover(); + await line.getByRole("button", { name: "Edit" }).click(); + const editComposer = page.getByRole("textbox", { name: "Edit message" }); + await page.getByLabel("User menu").hover(); // Just to un-hover the message line + await editComposer.fill(newMsg); + await editComposer.press("Enter"); +} + +test.describe("Message rendering", () => { + [ + { direction: "ltr", displayName: "Quentin" }, + { direction: "rtl", displayName: "كوينتين" }, + ].forEach(({ direction, displayName }) => { + test.describe(`with ${direction} display name`, () => { + test.use({ + displayName, + room: async ({ user, app }, use) => { + const roomId = await app.client.createRoom({ name: "Test room" }); + await use({ roomId }); + }, + }); + + test("should render a basic LTR text message", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "Hello, world!"); + await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }); + + test("should render an LTR emote", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "/me lays an egg"); + await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`); + }); + + test("should render an LTR rich text emote", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "/me lays a *free range* egg"); + await expect(msgTile).toMatchScreenshot(`emote-rich-ltr-${direction}displayname.png`); + }); + + test("should render an edited LTR message", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "Hello, world!"); + + await editMessage(page, msgTile, "Hello, universe!"); + + await expect(msgTile).toMatchScreenshot(`edited-message-ltr-${direction}displayname.png`, { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }); + + test("should render a basic RTL text message", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "مرحبا بالعالم!"); + await expect(msgTile).toMatchScreenshot(`basic-message-rtl-${direction}displayname.png`, { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }); + + test("should render an RTL emote", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "/me يضع بيضة"); + await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`); + }); + + test("should render a richtext RTL emote", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "/me أضع بيضة *حرة النطاق*"); + await expect(msgTile).toMatchScreenshot(`emote-rich-rtl-${direction}displayname.png`); + }); + + test("should render an edited RTL message", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + + const msgTile = await sendMessage(page, "مرحبا بالعالم!"); + + await editMessage(page, msgTile, "مرحبا بالكون!"); + + await expect(msgTile).toMatchScreenshot(`edited-message-rtl-${direction}displayname.png`, { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }); + }); + }); +}); diff --git a/playwright/e2e/oidc/oidc-aware.spec.ts b/playwright/e2e/oidc/oidc-aware.spec.ts index 2df450243a..45e99e06bb 100644 --- a/playwright/e2e/oidc/oidc-aware.spec.ts +++ b/playwright/e2e/oidc/oidc-aware.spec.ts @@ -31,7 +31,7 @@ test.describe("OIDC Aware", () => { await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible(); // Open settings and navigate to account management - await app.settings.openUserSettings("General"); + await app.settings.openUserSettings("Account"); const newPagePromise = context.waitForEvent("page"); await page.getByRole("button", { name: "Manage account" }).click(); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 61795a85e5..6860dc1104 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -44,7 +44,7 @@ test.describe("OIDC Native", () => { const deviceId = await page.evaluate(() => window.localStorage.mx_device_id); - await app.settings.openUserSettings("General"); + await app.settings.openUserSettings("Account"); const newPagePromise = context.waitForEvent("page"); await page.getByRole("button", { name: "Manage account" }).click(); await app.settings.closeDialog(); diff --git a/playwright/e2e/pinned-messages/index.ts b/playwright/e2e/pinned-messages/index.ts new file mode 100644 index 0000000000..a67df09d86 --- /dev/null +++ b/playwright/e2e/pinned-messages/index.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Page } from "@playwright/test"; + +import { test as base, expect } from "../../element-web-test"; +import { Client } from "../../pages/client"; +import { ElementAppPage } from "../../pages/ElementAppPage"; +import { Bot } from "../../pages/bot"; + +/** + * Set up for pinned message tests. + */ +export const test = base.extend<{ + room1Name?: string; + room1: { name: string; roomId: string }; + util: Helpers; +}>({ + displayName: "Alice", + botCreateOpts: { displayName: "Other User" }, + + room1Name: "Room 1", + room1: async ({ room1Name: name, app, user, bot }, use) => { + const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] }); + await use({ name, roomId }); + }, + + util: async ({ page, app, bot }, use) => { + await use(new Helpers(page, app, bot)); + }, +}); + +export class Helpers { + constructor( + private page: Page, + private app: ElementAppPage, + private bot: Bot, + ) {} + + /** + * Sends messages into given room as a bot + * @param room - the name of the room to send messages into + * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` + */ + async receiveMessages(room: string | { name: string }, messages: string[]) { + await this.sendMessageAsClient(this.bot, room, messages); + } + + /** + * Use the supplied client to send messages or perform actions as specified by + * the supplied {@link Message} items. + */ + private async sendMessageAsClient(cli: Client, roomName: string | { name: string }, messages: string[]) { + const room = await this.findRoomByName(typeof roomName === "string" ? roomName : roomName.name); + const roomId = await room.evaluate((room) => room.roomId); + + for (const message of messages) { + await cli.sendMessage(roomId, { body: message, msgtype: "m.text" }); + + // TODO: without this wait, some tests that send lots of messages flake + // from time to time. I (andyb) have done some investigation, but it + // needs more work to figure out. The messages do arrive over sync, but + // they never appear in the timeline, and they never fire a + // Room.timeline event. I think this only happens with events that refer + // to other events (e.g. replies), so it might be caused by the + // referring event arriving before the referred-to event. + await this.page.waitForTimeout(100); + } + } + + /** + * Find a room by its name + * @param roomName + * @private + */ + private async findRoomByName(roomName: string) { + return this.app.client.evaluateHandle((cli, roomName) => { + return cli.getRooms().find((r) => r.name === roomName); + }, roomName); + } + + /** + * Open the room with the supplied name. + */ + async goTo(room: string | { name: string }) { + await this.app.viewRoomByName(typeof room === "string" ? room : room.name); + } + + /** + * Pin the given message + * @param message + */ + async pinMessage(message: string) { + const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message }); + await timelineMessage.click({ button: "right" }); + await this.page.getByRole("menuitem", { name: "Pin" }).click(); + } + + /** + * Pin the given messages + * @param messages + */ + async pinMessages(messages: string[]) { + for (const message of messages) { + await this.pinMessage(message); + } + } + + /** + * Open the room info panel + */ + async openRoomInfo() { + await this.page.getByRole("button", { name: "Room info" }).nth(1).click(); + } + + /** + * Assert that the pinned count in the room info is correct + * Open the room info and check the pinned count + * @param count + */ + async assertPinnedCountInRoomInfo(count: number) { + await expect(this.page.getByRole("menuitem", { name: "Pinned messages" })).toHaveText( + `Pinned messages${count}`, + ); + } + + /** + * Open the pinned messages list + */ + async openPinnedMessagesList() { + await this.page.getByRole("menuitem", { name: "Pinned messages" }).click(); + } + + /** + * Return the right panel + * @private + */ + private getRightPanel() { + return this.page.locator("#mx_RightPanel"); + } + + /** + * Assert that the pinned message list contains the given messages + * @param messages + */ + async assertPinnedMessagesList(messages: string[]) { + const rightPanel = this.getRightPanel(); + await expect(rightPanel.getByRole("heading", { name: "Pinned messages" })).toHaveText( + `${messages.length} Pinned messages`, + ); + await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-messages-${messages.length}.png`); + + const list = rightPanel.getByRole("list"); + await expect(list.getByRole("listitem")).toHaveCount(messages.length); + + for (const message of messages) { + await expect(list.getByText(message)).toBeVisible(); + } + } + + /** + * Assert that the pinned message list is empty + */ + async assertEmptyPinnedMessagesList() { + const rightPanel = this.getRightPanel(); + await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-empty.png`); + } + + /** + * Open the unpin all dialog + */ + async openUnpinAllDialog() { + await this.openRoomInfo(); + await this.openPinnedMessagesList(); + await this.page.getByRole("button", { name: "Unpin all" }).click(); + } + + /** + * Return the unpin all dialog + */ + getUnpinAllDialog() { + return this.page.locator(".mx_Dialog", { hasText: "Unpin all messages?" }); + } + + /** + * Click on the Continue button of the unoin all dialog + */ + async confirmUnpinAllDialog() { + await this.getUnpinAllDialog().getByRole("button", { name: "Continue" }).click(); + } + + /** + * Go back from the pinned messages list + */ + async backPinnedMessagesList() { + await this.page.locator("#mx_RightPanel").getByTestId("base-card-back-button").click(); + } + + /** + * Open the contextual menu of a message in the pin message list and click on unpin + * @param message + */ + async unpinMessageFromMessageList(message: string) { + const item = this.getRightPanel().getByRole("list").getByRole("listitem").filter({ + hasText: message, + }); + + await item.getByRole("button").click(); + await this.page.getByRole("menu", { name: "Open menu" }).getByRole("menuitem", { name: "Unpin" }).click(); + } +} + +export { expect }; diff --git a/playwright/e2e/pinned-messages/pinned-messages.spec.ts b/playwright/e2e/pinned-messages/pinned-messages.spec.ts new file mode 100644 index 0000000000..be1c92223f --- /dev/null +++ b/playwright/e2e/pinned-messages/pinned-messages.spec.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test } from "./index"; +import { expect } from "../../element-web-test"; + +test.describe("Pinned messages", () => { + test.use({ + labsFlags: ["feature_pinning"], + }); + + test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.openRoomInfo(); + await util.assertPinnedCountInRoomInfo(0); + await util.openPinnedMessagesList(); + await util.assertEmptyPinnedMessagesList(); + }); + + test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); + + await util.pinMessages(["Msg1", "Msg2", "Msg4"]); + await util.openRoomInfo(); + await util.assertPinnedCountInRoomInfo(3); + }); + + test("should pin messages and show them in the pinned message panel", async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); + + // Pin the messages + await util.pinMessages(["Msg1", "Msg2", "Msg4"]); + await util.openRoomInfo(); + await util.openPinnedMessagesList(); + await util.assertPinnedMessagesList(["Msg1", "Msg2", "Msg4"]); + }); + + test("should unpin one message", async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); + await util.pinMessages(["Msg1", "Msg2", "Msg4"]); + + await util.openRoomInfo(); + await util.openPinnedMessagesList(); + await util.unpinMessageFromMessageList("Msg2"); + await util.assertPinnedMessagesList(["Msg1", "Msg4"]); + await util.backPinnedMessagesList(); + await util.assertPinnedCountInRoomInfo(2); + }); + + test("should unpin all messages", async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); + await util.pinMessages(["Msg1", "Msg2", "Msg4"]); + + await util.openUnpinAllDialog(); + await expect(util.getUnpinAllDialog()).toMatchScreenshot("unpin-all-dialog.png"); + await util.confirmUnpinAllDialog(); + + await util.assertEmptyPinnedMessagesList(); + await util.backPinnedMessagesList(); + await util.assertPinnedCountInRoomInfo(0); + }); +}); diff --git a/playwright/e2e/polls/pollHistory.spec.ts b/playwright/e2e/polls/pollHistory.spec.ts index e9ebf0a30d..20a8b912fe 100644 --- a/playwright/e2e/polls/pollHistory.spec.ts +++ b/playwright/e2e/polls/pollHistory.spec.ts @@ -69,7 +69,7 @@ test.describe("Poll history", () => { async function openPollHistory(app: ElementAppPage): Promise { const { page } = app; await app.toggleRoomInfoPanel(); - await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "Poll history" }).click(); + await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "Polls" }).click(); } test.use({ diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index 900012d8fa..edb06ac2d9 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -37,7 +37,7 @@ test.describe("Registration", () => { await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); // Hide the server text as it contains the randomly allocated Homeserver port - const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; + const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true }; await expect(page).toMatchScreenshot("registration.png", screenshotOptions); await checkA11y(); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index f282d83d62..f7b2958509 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -73,7 +73,8 @@ test.describe("RightPanel", () => { test("should handle clicking add widgets", async ({ page, app }) => { await viewRoomSummaryByName(page, app, ROOM_NAME); - await page.getByRole("button", { name: "Add widgets, bridges & bots" }).click(); + await page.getByRole("tab", { name: "Extensions" }).click(); + await page.getByRole("button", { name: "Add extensions" }).click(); await expect(page.locator(".mx_IntegrationManager")).toBeVisible(); }); diff --git a/playwright/e2e/settings/general-user-settings-tab.spec.ts b/playwright/e2e/settings/account-user-settings-tab.spec.ts similarity index 89% rename from playwright/e2e/settings/general-user-settings-tab.spec.ts rename to playwright/e2e/settings/account-user-settings-tab.spec.ts index 0ba85e890b..5ec149f041 100644 --- a/playwright/e2e/settings/general-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/account-user-settings-tab.spec.ts @@ -19,23 +19,23 @@ import { test, expect } from "../../element-web-test"; const USER_NAME = "Bob"; const USER_NAME_NEW = "Alice"; -test.describe("General user settings tab", () => { +test.describe("Account user settings tab", () => { test.use({ displayName: USER_NAME, config: { default_country_code: "US", // For checking the international country calling code }, uut: async ({ app, user }, use) => { - const locator = await app.settings.openUserSettings("General"); + const locator = await app.settings.openUserSettings("Account"); await use(locator); }, }); test("should be rendered properly", async ({ uut, user }) => { - await expect(uut).toMatchScreenshot("general.png"); + await expect(uut).toMatchScreenshot("account.png"); // Assert that the top heading is rendered - await expect(uut.getByRole("heading", { name: "General" })).toBeVisible(); + await expect(uut.getByRole("heading", { name: "Account", exact: true })).toBeVisible(); const profile = uut.locator(".mx_UserProfileSettings_profile"); await profile.scrollIntoViewIfNeeded(); @@ -49,12 +49,11 @@ test.describe("General user settings tab", () => { await expect(uut.getByTestId("discoverySection").locator(".mx_Spinner")).not.toBeVisible(); const accountSection = uut.getByTestId("accountSection"); + accountSection.scrollIntoViewIfNeeded(); // Assert that input areas for changing a password exists - const changePassword = accountSection.locator("form.mx_GeneralUserSettingsTab_section--account_changePassword"); - await changePassword.scrollIntoViewIfNeeded(); - await expect(changePassword.getByLabel("Current password")).toBeVisible(); - await expect(changePassword.getByLabel("New Password")).toBeVisible(); - await expect(changePassword.getByLabel("Confirm password")).toBeVisible(); + await expect(accountSection.getByLabel("Current password")).toBeVisible(); + await expect(accountSection.getByLabel("New Password")).toBeVisible(); + await expect(accountSection.getByLabel("Confirm password")).toBeVisible(); // Check email addresses area const emailAddresses = uut.getByTestId("mx_AccountEmailAddresses"); @@ -82,13 +81,13 @@ test.describe("General user settings tab", () => { test("should respond to small screen sizes", async ({ page, uut }) => { await page.setViewportSize({ width: 700, height: 600 }); - await expect(uut).toMatchScreenshot("general-smallscreen.png"); + await expect(uut).toMatchScreenshot("account-smallscreen.png"); }); test("should show tooltips on narrow screen", async ({ page, uut }) => { await page.setViewportSize({ width: 700, height: 600 }); - await page.getByRole("tab", { name: "General" }).hover(); - await expect(page.getByRole("tooltip")).toHaveText("General"); + await page.getByRole("tab", { name: "Account" }).hover(); + await expect(page.getByRole("tooltip")).toHaveText("Account"); }); test("should support adding and removing a profile picture", async ({ uut, page }) => { diff --git a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts index aa00681f61..471d9c992f 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -44,7 +44,7 @@ test.describe("Appearance user settings tab", () => { // -4 value is 12px await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); - await expect(page).toMatchScreenshot("window-12px.png"); + await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); }); test("should support enabling system font", async ({ page, app, user }) => { diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index a67909b47b..368935e91c 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -36,12 +36,12 @@ test.describe("Preferences user settings tab", () => { test("should be able to change the app language", async ({ uut, user }) => { // Check language and region setting dropdown - const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput"); + const languageInput = uut.getByRole("button", { name: "Language Dropdown" }); await languageInput.scrollIntoViewIfNeeded(); // Check the default value await expect(languageInput.getByText("English")).toBeVisible(); // Click the button to display the dropdown menu - await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); + await languageInput.click(); // Assert that the default option is rendered and highlighted languageInput.getByRole("option", { name: /Albanian/ }); await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass( @@ -49,7 +49,7 @@ test.describe("Preferences user settings tab", () => { ); await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible(); // Click again to close the dropdown - await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); + await languageInput.click(); // Assert that the default value is rendered again await expect(languageInput.getByText("English")).toBeVisible(); }); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 6068385194..b00179c0c0 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -410,7 +410,6 @@ test.describe("Timeline", () => { { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], - hideTooltips: true, }, ); @@ -428,7 +427,6 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-and-messages-irc-layout.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], - hideTooltips: true, }); // 3. Alignment of expanded GELS and placeholder of deleted message @@ -449,7 +447,6 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-redaction-placeholder.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], - hideTooltips: true, }); // 4. Alignment of expanded GELS, placeholder of deleted message, and emote @@ -472,7 +469,6 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], - hideTooltips: true, }); }); @@ -485,7 +481,6 @@ test.describe("Timeline", () => { display: none !important; } `, - hideTooltips: true, }; await sendEvent(app.client, room.roomId); @@ -725,11 +720,16 @@ test.describe("Timeline", () => { ).toBeVisible(); }); - test("should render url previews", async ({ page, app, room, axe, checkA11y }) => { + test("should render url previews", async ({ page, app, room, axe, checkA11y, context }) => { axe.disableRules("color-contrast"); - await page.route( - "**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", + // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but + // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it + // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because + // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until + // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. + await context.route( + "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", async (route) => { await route.fulfill({ path: "playwright/sample-files/riot.png", @@ -755,6 +755,7 @@ test.describe("Timeline", () => { const requestPromises: Promise[] = [ page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), + // see context.route above for why we listen for the unauthenticated endpoint page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), ]; diff --git a/playwright/e2e/user-menu/user-menu.spec.ts b/playwright/e2e/user-menu/user-menu.spec.ts index d727ae7b12..84e849c156 100644 --- a/playwright/e2e/user-menu/user-menu.spec.ts +++ b/playwright/e2e/user-menu/user-menu.spec.ts @@ -25,5 +25,6 @@ test.describe("User Menu", () => { await expect(menu.locator(".mx_UserMenu_contextMenu_displayName", { hasText: user.displayName })).toBeVisible(); await expect(menu.locator(".mx_UserMenu_contextMenu_userId", { hasText: user.userId })).toBeVisible(); + await expect(menu).toMatchScreenshot("user-menu.png"); }); }); diff --git a/playwright/e2e/user-view/user-view.spec.ts b/playwright/e2e/user-view/user-view.spec.ts index eddc466fec..1ff2e6568a 100644 --- a/playwright/e2e/user-view/user-view.spec.ts +++ b/playwright/e2e/user-view/user-view.spec.ts @@ -30,6 +30,12 @@ test.describe("UserView", () => { await expect(rightPanel.getByText("1 session")).toBeVisible(); await expect(rightPanel).toMatchScreenshot("user-info.png", { mask: [page.locator(".mx_UserInfo_profile_mxid")], + css: ` + /* Use monospace font for consistent mask width */ + .mx_UserInfo_profile_mxid { + font-family: Inconsolata !important; + } + `, }); }); }); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index d6fd1b48c1..debe0d0f11 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -313,8 +313,8 @@ export const expect = baseExpect.extend({ name: `${string}.png`, options?: { mask?: Array; - omitBackground?: boolean; - hideTooltips?: boolean; + includeDialogBackground?: boolean; + showTooltips?: boolean; timeout?: number; css?: string; }, @@ -324,45 +324,57 @@ export const expect = baseExpect.extend({ const page = "page" in receiver ? receiver.page() : receiver; - let hideTooltipsCss: string | undefined; - if (options?.hideTooltips) { - hideTooltipsCss = ` + let css = ` + .mx_MessagePanel_myReadMarker { + display: none !important; + } + .mx_RoomView_MessageList { + height: auto !important; + } + .mx_DisambiguatedProfile_displayName { + color: var(--cpd-color-blue-1200) !important; + } + .mx_BaseAvatar { + background-color: var(--cpd-color-fuchsia-1200) !important; + color: white !important; + } + .mx_ReplyChain { + border-left-color: var(--cpd-color-blue-1200) !important; + } + /* Avoid flakiness from hover styling */ + .mx_ReplyChain_show { + color: var(--cpd-color-text-secondary) !important; + } + /* Use monospace font for timestamp for consistent mask width */ + .mx_MessageTimestamp { + font-family: Inconsolata !important; + } + `; + + if (!options?.showTooltips) { + css += ` .mx_Tooltip_visible { visibility: hidden !important; } `; } + if (!options?.includeDialogBackground) { + css += ` + /* Make the dialog backdrop solid so any dialog screenshots don't show any components behind them */ + .mx_Dialog_background { + background-color: #030c1b !important; + } + `; + } + + if (options?.css) { + css += options.css; + } + // We add a custom style tag before taking screenshots const style = (await page.addStyleTag({ - content: ` - .mx_MessagePanel_myReadMarker { - display: none !important; - } - .mx_RoomView_MessageList { - height: auto !important; - } - .mx_DisambiguatedProfile_displayName { - color: var(--cpd-color-blue-1200) !important; - } - .mx_BaseAvatar { - background-color: var(--cpd-color-fuchsia-1200) !important; - color: white !important; - } - .mx_ReplyChain { - border-left-color: var(--cpd-color-blue-1200) !important; - } - /* Avoid flakiness from hover styling */ - .mx_ReplyChain_show { - color: var(--cpd-color-text-secondary) !important; - } - /* Use monospace font for timestamp for consistent mask width */ - .mx_MessageTimestamp { - font-family: Inconsolata !important; - } - ${hideTooltipsCss ?? ""} - ${options?.css ?? ""} - `, + content: css, })) as ElementHandle; const screenshotName = sanitizeFilePathBeforeExtension(name); diff --git a/playwright/flaky-reporter.ts b/playwright/flaky-reporter.ts index 95023e31ba..3fcdc406fe 100644 --- a/playwright/flaky-reporter.ts +++ b/playwright/flaky-reporter.ts @@ -25,6 +25,13 @@ const REPO = "element-hq/element-web"; const LABEL = "Z-Flaky-Test"; const ISSUE_TITLE_PREFIX = "Flaky playwright test: "; +type PaginationLinks = { + prev?: string; + next?: string; + last?: string; + first?: string; +}; + class FlakyReporter implements Reporter { private flakes = new Set(); @@ -35,6 +42,54 @@ class FlakyReporter implements Reporter { } } + /** + * Parse link header to retrieve pagination links + * @see https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28#using-link-headers + * @param link link header from response or undefined + * @returns an empty object if link is undefined otherwise returns a map from type to link + */ + private parseLinkHeader(link: string): PaginationLinks { + /** + * link looks like: + * ; rel="prev", ; + */ + const map: PaginationLinks = {}; + if (!link) return map; + const matches = link.matchAll(/(<(?.+?)>; rel="(?.+?)")/g); + for (const match of matches) { + const { link, type } = match.groups; + map[type] = link; + } + return map; + } + + /** + * Fetch all flaky test issues that were updated since Jan-1-2024 + * @returns A promise that resolves to a list of issues + */ + async getAllIssues(): Promise { + const issues = []; + const { GITHUB_TOKEN, GITHUB_API_URL } = process.env; + // See https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues + let url = `${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=100&sort=updated&since=2024-01-01`; + const headers = { + Authorization: `Bearer ${GITHUB_TOKEN}`, + Accept: "application / vnd.github + json", + }; + while (url) { + // Fetch issues and add to list + const issuesResponse = await fetch(url, { headers }); + const fetchedIssues = await issuesResponse.json(); + issues.push(...fetchedIssues); + + // Get the next link for fetching more results + const linkHeader = issuesResponse.headers.get("Link"); + const parsed = this.parseLinkHeader(linkHeader); + url = parsed.next; + } + return issues; + } + public async onExit(): Promise { if (this.flakes.size === 0) { console.log("No flakes found"); @@ -49,18 +104,12 @@ class FlakyReporter implements Reporter { const { GITHUB_TOKEN, GITHUB_API_URL, GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID } = process.env; if (!GITHUB_TOKEN) return; - const body = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; - - const headers = { Authorization: `Bearer ${GITHUB_TOKEN}` }; - // Fetch all existing issues with the flaky-test label. - const issuesRequest = await fetch( - `${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=100&sort=created`, - { headers }, - ); - const issues = await issuesRequest.json(); + const issues = await this.getAllIssues(); for (const flake of this.flakes) { const title = ISSUE_TITLE_PREFIX + "`" + flake + "`"; const existingIssue = issues.find((issue) => issue.title === title); + const headers = { Authorization: `Bearer ${GITHUB_TOKEN}` }; + const body = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; if (existingIssue) { console.log(`Found issue ${existingIssue.number} for ${flake}, adding comment...`); diff --git a/playwright/pages/toasts.ts b/playwright/pages/toasts.ts index 0785f33c23..436ea42952 100644 --- a/playwright/pages/toasts.ts +++ b/playwright/pages/toasts.ts @@ -45,7 +45,7 @@ export class Toasts { */ public async acceptToast(expectedTitle: string): Promise { const toast = await this.getToast(expectedTitle); - await toast.locator(".mx_Toast_buttons .mx_AccessibleButton_kind_primary").click(); + await toast.locator('.mx_Toast_buttons button[data-kind="primary"]').click(); } /** @@ -55,6 +55,6 @@ export class Toasts { */ public async rejectToast(expectedTitle: string): Promise { const toast = await this.getToast(expectedTitle); - await toast.locator(".mx_Toast_buttons .mx_AccessibleButton_kind_danger_outline").click(); + await toast.locator('.mx_Toast_buttons button[data-kind="secondary"]').click(); } } diff --git a/playwright/plugins/homeserver/dendrite/index.ts b/playwright/plugins/homeserver/dendrite/index.ts index 603bd360a8..0ddaecc1b7 100644 --- a/playwright/plugins/homeserver/dendrite/index.ts +++ b/playwright/plugins/homeserver/dendrite/index.ts @@ -29,7 +29,6 @@ const dendriteConfigFile = "dendrite.yaml"; // Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it export class Dendrite extends Synapse implements Homeserver, HomeserverInstance { - public config: HomeserverConfig & { serverId: string }; protected image = "matrixdotorg/dendrite-monolith:main"; protected entrypoint = "/usr/bin/dendrite"; diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 5cf6391720..86490bb0f1 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -25,10 +25,10 @@ import { Docker } from "../../docker"; import { HomeserverConfig, HomeserverInstance, Homeserver, StartHomeserverOpts, Credentials } from ".."; import { randB64Bytes } from "../../utils/rand"; -// Docker tag to use for `matrixdotorg/synapse` image. +// Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:9e193236098ae5ff66c9bf79252e318fd561ceb1322d5495780a11d9dbdcfb17"; +const DOCKER_TAG = "develop@sha256:92bd527fb219e2b8bad770f25140c0117fe08fd948b991bf669c3f86bf4f2c61"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); @@ -110,7 +110,7 @@ export class Synapse implements Homeserver, HomeserverInstance { console.log(`Starting synapse with config dir ${synCfg.configDir}...`); const dockerSynapseParams = ["-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`]; const synapseId = await this.docker.run({ - image: `matrixdotorg/synapse:${DOCKER_TAG}`, + image: `ghcr.io/element-hq/synapse:${DOCKER_TAG}`, containerName: `react-sdk-playwright-synapse`, params: dockerSynapseParams, cmd: ["run"], diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png index df1a44f523..dd8b24beea 100644 Binary files a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png differ diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png index ae11ec9eec..9d301e7919 100644 Binary files a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 2c6160f2a1..f7e0d12ea1 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png index c188081189..d2afccb990 100644 Binary files a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png and b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png index 22b4e109c8..b43d78bec8 100644 Binary files a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png index 7e8992dca1..0e57ae9995 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png index dfa6d4f0aa..154f94a247 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index 614533956b..2130dd5d1a 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index 51f365f353..de476106e9 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png new file mode 100644 index 0000000000..fe92443694 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png new file mode 100644 index 0000000000..a0a5dbb8b0 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png new file mode 100644 index 0000000000..cf2da6f023 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png new file mode 100644 index 0000000000..e9aded5a5f Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png new file mode 100644 index 0000000000..1e29c40c73 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png new file mode 100644 index 0000000000..104b8f469e Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png new file mode 100644 index 0000000000..f15894f2b3 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png new file mode 100644 index 0000000000..bec538f32d Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png new file mode 100644 index 0000000000..772bbbbeec Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png new file mode 100644 index 0000000000..04f4e0d1f5 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png new file mode 100644 index 0000000000..8cc8d6e088 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png new file mode 100644 index 0000000000..feb0651650 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png new file mode 100644 index 0000000000..181e1fb9ca Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png new file mode 100644 index 0000000000..ffae099bdc Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png new file mode 100644 index 0000000000..ee9d8b8a43 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png new file mode 100644 index 0000000000..19075ea869 Binary files /dev/null and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png new file mode 100644 index 0000000000..28099d338c Binary files /dev/null and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-2-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-2-linux.png new file mode 100644 index 0000000000..82666b0d95 Binary files /dev/null and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-2-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-3-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-3-linux.png new file mode 100644 index 0000000000..98e804d897 Binary files /dev/null and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-messages-3-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png new file mode 100644 index 0000000000..e6f1005395 Binary files /dev/null and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index 9992923226..3936b29fdf 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index 050a82a8af..c1007f06e7 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png index 3d4ea984a4..51c70fc196 100644 Binary files a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png index 94b5505b7a..c0e398debb 100644 Binary files a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png and b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png differ diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png index 6439fe305b..f21145e02c 100644 Binary files a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png differ diff --git a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png index d18266534d..cc1ea27dfc 100644 Binary files a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index 943cc9dfc8..c33ff1ce34 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png index e0fc9cc5bd..66a6d79b53 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png index a962c3cbad..8d89b18087 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png index 7271a50cf9..bb10e28aba 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png new file mode 100644 index 0000000000..ec4a0f030c Binary files /dev/null and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png new file mode 100644 index 0000000000..bbf74913ab Binary files /dev/null and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index 12ea3aa847..76a9308b35 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index 1ce0ffd520..bf47c91388 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index 57e2a4026c..01a6c6089b 100644 Binary files a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png deleted file mode 100644 index 4cf8df1c32..0000000000 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png and /dev/null differ diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png deleted file mode 100644 index d0380e6a4f..0000000000 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png and /dev/null differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index ca2e75dfbb..56745f9a19 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png index c59d60178d..d2852b7c0f 100644 Binary files a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png index 6f55f2fd00..ea9e428244 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png index dcac67dc1f..7f76175fcf 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png index 37405cd821..7f76175fcf 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index d30d8e98a8..ab4ec4ec3b 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png index 8826429198..c7dae33916 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png new file mode 100644 index 0000000000..f713a7dc98 Binary files /dev/null and b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png differ diff --git a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png index ea1fa63bde..3112b0fcf2 100644 Binary files a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png and b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png differ diff --git a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png index b6d6d2a210..2cedb4d1f5 100644 Binary files a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png and b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png differ diff --git a/playwright/tsconfig.json b/playwright/tsconfig.json index 55f979f3ce..5f3083fd57 100644 --- a/playwright/tsconfig.json +++ b/playwright/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2022", "jsx": "react", - "lib": ["ESNext", "es2021", "dom", "dom.iterable"], + "lib": ["ESNext", "es2022", "dom", "dom.iterable"], "resolveJsonModule": true, "esModuleInterop": true, "moduleResolution": "node", diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 8264ccb704..605ad41b81 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -25,8 +25,6 @@ limitations under the License. @import url("maplibre-gl/dist/maplibre-gl.css"); :root { - font-size: 10px; - --container-border-width: 8px; --container-gap-width: 8px; /* only even numbers should be used because otherwise we get 0.5px margin values. */ --transition-short: 0.1s; @@ -606,7 +604,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -626,14 +624,14 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -645,7 +643,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -658,7 +656,7 @@ legend { .mx_Dialog_buttons button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( .mx_ThemeChoicePanel_CustomTheme button - ), + ):not(.mx_UnpinAllDialog button), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -674,7 +672,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 20a6d2fe54..96c285bc0a 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -37,6 +37,7 @@ @import "./components/views/messages/shared/_MediaProcessingError.pcss"; @import "./components/views/pips/_WidgetPip.pcss"; @import "./components/views/polls/_PollOption.pcss"; +@import "./components/views/settings/_AddRemoveThreepids.pcss"; @import "./components/views/settings/devices/_CurrentDeviceSection.pcss"; @import "./components/views/settings/devices/_DeviceDetailHeading.pcss"; @import "./components/views/settings/devices/_DeviceDetails.pcss"; @@ -116,6 +117,7 @@ @import "./views/avatars/_BaseAvatar.pcss"; @import "./views/avatars/_DecoratedRoomAvatar.pcss"; @import "./views/avatars/_WidgetAvatar.pcss"; +@import "./views/avatars/_WithPresenceIndicator.pcss"; @import "./views/beta/_BetaCard.pcss"; @import "./views/context_menus/_DeviceContextMenu.pcss"; @import "./views/context_menus/_IconizedContextMenu.pcss"; @@ -165,6 +167,7 @@ @import "./views/dialogs/_SpaceSettingsDialog.pcss"; @import "./views/dialogs/_SpotlightDialog.pcss"; @import "./views/dialogs/_TermsDialog.pcss"; +@import "./views/dialogs/_UnpinAllDialog.pcss"; @import "./views/dialogs/_UntrustedDeviceDialog.pcss"; @import "./views/dialogs/_UploadConfirmDialog.pcss"; @import "./views/dialogs/_UserSettingsDialog.pcss"; @@ -260,6 +263,7 @@ @import "./views/right_panel/_BaseCard.pcss"; @import "./views/right_panel/_EmptyState.pcss"; @import "./views/right_panel/_EncryptionInfo.pcss"; +@import "./views/right_panel/_ExtensionsCard.pcss"; @import "./views/right_panel/_PinnedMessagesCard.pcss"; @import "./views/right_panel/_RightPanelTabs.pcss"; @import "./views/right_panel/_RoomSummaryCard.pcss"; @@ -355,10 +359,10 @@ @import "./views/settings/tabs/room/_RolesRoomSettingsTab.pcss"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.pcss"; @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss"; -@import "./views/settings/tabs/user/_GeneralUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_KeyboardUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss"; +@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_SidebarUserSettingsTab.pcss"; @import "./views/spaces/_SpaceBasicSettings.pcss"; diff --git a/res/css/components/views/settings/_AddRemoveThreepids.pcss b/res/css/components/views/settings/_AddRemoveThreepids.pcss new file mode 100644 index 0000000000..0e9ef83ae7 --- /dev/null +++ b/res/css/components/views/settings/_AddRemoveThreepids.pcss @@ -0,0 +1,49 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + * These used to live in General User Settings. These components are horribly duplicative + * but share the same styles. For now I'm putting them here so I can renamed the general + * tab sensibly and before I can refactor these components. + */ + +.mx_AddRemoveThreepids_existing { + display: flex; + align-items: center; +} + +.mx_AddRemoveThreepids_existing_address, +.mx_AddRemoveThreepids_existing_promptText { + flex: 1; + margin-right: 10px; +} + +.mx_AddRemoveThreepids_existing_button { + margin-left: 5px; +} + +.mx_EmailAddressesPhoneNumbers_verify { + display: flex; +} + +.mx_EmailAddressesPhoneNumbers_existing_button { + justify-content: right; +} + +.mx_EmailAddressesPhoneNumbers_verify_instructions { + flex: 1; +} diff --git a/res/css/structures/_TabbedView.pcss b/res/css/structures/_TabbedView.pcss index 04f0587b0a..bb71c8be4e 100644 --- a/res/css/structures/_TabbedView.pcss +++ b/res/css/structures/_TabbedView.pcss @@ -136,6 +136,12 @@ limitations under the License. transition: color 0.1s, background-color 0.1s; + + svg { + width: 20px; + height: 20px; + margin-right: var(--cpd-space-3x); + } } .mx_TabbedView_maskedIcon { @@ -184,6 +190,10 @@ limitations under the License. } .mx_TabbedView_tabLabel { padding-inline: 0 0; + justify-content: center; + svg { + margin-right: 0; + } } } } diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss index 6b18836776..f8f15bca40 100644 --- a/res/css/structures/_ToastContainer.pcss +++ b/res/css/structures/_ToastContainer.pcss @@ -119,8 +119,7 @@ limitations under the License. h2 { margin: 0; - font: var(--cpd-font-heading-sm-medium); - font-weight: var(--cpd-font-weight-medium); + font: var(--cpd-font-body-lg-semibold); display: inline; width: auto; } @@ -154,6 +153,7 @@ limitations under the License. overflow: hidden; text-overflow: ellipsis; margin: 4px 0 11px 0; + color: $secondary-content; font: var(--cpd-font-body-sm-regular); a { @@ -161,10 +161,6 @@ limitations under the License. } } - .mx_Toast_detail { - color: $secondary-content; - } - .mx_Toast_deviceID { font-size: $font-10px; } diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 24e06e9a2c..1f0fc9d2f5 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -111,7 +111,7 @@ limitations under the License. .mx_UserMenu_contextMenu_displayName, .mx_UserMenu_contextMenu_userId { - font: var(--cpd-font-heading-sm-regular); + font: var(--cpd-font-body-lg-regular); /* Automatically grow subelements to fit the container */ flex: 1; diff --git a/res/css/views/avatars/_WithPresenceIndicator.pcss b/res/css/views/avatars/_WithPresenceIndicator.pcss new file mode 100644 index 0000000000..0a7f0c2ad9 --- /dev/null +++ b/res/css/views/avatars/_WithPresenceIndicator.pcss @@ -0,0 +1,54 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_WithPresenceIndicator { + position: relative; + contain: content; + line-height: 0; + + .mx_WithPresenceIndicator_icon { + position: absolute; + right: -2px; + bottom: -2px; + } + + .mx_WithPresenceIndicator_icon::before { + content: ""; + width: 100%; + height: 100%; + right: 0; + bottom: 0; + position: absolute; + border: 2px solid var(--cpd-color-bg-canvas-default); + border-radius: 50%; + } + + .mx_WithPresenceIndicator_icon_offline::before { + background-color: $presence-offline; + } + + .mx_WithPresenceIndicator_icon_online::before { + background-color: $accent; + } + + .mx_WithPresenceIndicator_icon_away::before { + background-color: $presence-away; + } + + .mx_WithPresenceIndicator_icon_busy::before { + background-color: $presence-busy; + } +} diff --git a/res/css/views/dialogs/_UnpinAllDialog.pcss b/res/css/views/dialogs/_UnpinAllDialog.pcss new file mode 100644 index 0000000000..fb05809523 --- /dev/null +++ b/res/css/views/dialogs/_UnpinAllDialog.pcss @@ -0,0 +1,38 @@ +/* + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.mx_UnpinAllDialog { + /* 396 is coming from figma and we remove the left and right paddings of the dialog */ + width: calc(396px - (var(--cpd-space-10x) * 2)); + padding-bottom: var(--cpd-space-2x); + + .mx_UnpinAllDialog_title { + /* Override the default heading style */ + font: var(--cpd-font-heading-sm-semibold) !important; + margin-bottom: var(--cpd-space-3x); + } + + .mx_UnpinAllDialog_buttons { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + margin: var(--cpd-space-8x) var(--cpd-space-2x) 0 var(--cpd-space-2x); + + button { + width: 100%; + } + } +} diff --git a/res/css/views/dialogs/_UserSettingsDialog.pcss b/res/css/views/dialogs/_UserSettingsDialog.pcss index 6c09e90886..5e564196b0 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.pcss +++ b/res/css/views/dialogs/_UserSettingsDialog.pcss @@ -30,54 +30,3 @@ limitations under the License. font: var(--cpd-font-heading-md-semibold); } } - -/* ICONS */ -/* ========================================================== */ - -.mx_UserSettingsDialog_settingsIcon::before { - mask-image: url("$(res)/img/element-icons/settings.svg"); -} - -.mx_UserSettingsDialog_appearanceIcon::before { - mask-image: url("$(res)/img/element-icons/settings/appearance.svg"); -} - -.mx_UserSettingsDialog_voiceIcon::before { - mask-image: url("$(res)/img/element-icons/call/voice-call.svg"); -} - -.mx_UserSettingsDialog_bellIcon::before { - mask-image: url("$(res)/img/element-icons/notifications.svg"); -} - -.mx_UserSettingsDialog_preferencesIcon::before { - mask-image: url("$(res)/img/element-icons/settings/preference.svg"); -} - -.mx_UserSettingsDialog_keyboardIcon::before { - mask-image: url("$(res)/img/element-icons/settings/keyboard.svg"); -} - -.mx_UserSettingsDialog_sidebarIcon::before { - mask-image: url("$(res)/img/element-icons/settings/sidebar.svg"); -} - -.mx_UserSettingsDialog_securityIcon::before { - mask-image: url("$(res)/img/element-icons/security.svg"); -} - -.mx_UserSettingsDialog_sessionsIcon::before { - mask-image: url("$(res)/img/element-icons/settings/devices.svg"); -} - -.mx_UserSettingsDialog_helpIcon::before { - mask-image: url("$(res)/img/element-icons/settings/help.svg"); -} - -.mx_UserSettingsDialog_labsIcon::before { - mask-image: url("$(res)/img/element-icons/settings/flask.svg"); -} - -.mx_UserSettingsDialog_mjolnirIcon::before { - mask-image: url("$(res)/img/element-icons/room/composer/emoji.svg"); -} diff --git a/res/css/views/messages/_MEmoteBody.pcss b/res/css/views/messages/_MEmoteBody.pcss index cf722e5ae8..18940844f7 100644 --- a/res/css/views/messages/_MEmoteBody.pcss +++ b/res/css/views/messages/_MEmoteBody.pcss @@ -16,6 +16,7 @@ limitations under the License. .mx_MEmoteBody { white-space: pre-wrap; + text-align: start; } .mx_MEmoteBody_sender { diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index 692f7d23b3..12a0abda1b 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -60,10 +60,11 @@ limitations under the License. flex: 1; .mx_BaseCard_header_title_heading { - color: $primary-content; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + font: var(--cpd-font-body-md-medium); + color: var(--cpd-color-text-secondary); } .mx_BaseCard_header_title_button--option { @@ -98,50 +99,6 @@ limitations under the License. scrollbar-gutter: stable; } - .mx_BaseCard_Group { - margin: $spacing-20 0 $spacing-16; - - & > * { - margin-left: $spacing-12; - margin-right: $spacing-12; - } - - > h2 { - color: $tertiary-content; - font: var(--cpd-font-body-sm-medium); - margin: $spacing-12; - } - - .mx_BaseCard_Button { - padding: 10px; - padding-inline-start: $spacing-12; - margin: 0; - position: relative; - font: var(--cpd-font-heading-sm-medium); - height: 20px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - display: flex; - - .mx_BaseCard_Button_sublabel { - color: $tertiary-content; - margin-left: auto; - } - - &:hover { - background-color: rgba(141, 151, 165, 0.1); - } - - &.mx_AccessibleButton_disabled { - padding-right: $spacing-12; - &::after { - content: unset; - } - } - } - } - .mx_BaseCard_footer { padding-top: $spacing-4; text-align: center; diff --git a/res/css/views/right_panel/_ExtensionsCard.pcss b/res/css/views/right_panel/_ExtensionsCard.pcss new file mode 100644 index 0000000000..ea5431fb36 --- /dev/null +++ b/res/css/views/right_panel/_ExtensionsCard.pcss @@ -0,0 +1,145 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_ExtensionsCard { + --cpd-separator-inset: var(--cpd-space-4x); + --cpd-separator-spacing: var(--cpd-space-4x); + + .mx_BaseCard_header { + /* Hide the line between the header and the body of the card */ + border-block-end: none; + + /* Styling for the "Add extensions" button */ + button { + width: 100%; + } + } + + .mx_AutoHideScrollbar { + padding: 0 var(--cpd-space-4x); + box-sizing: border-box; + } + + .mx_ExtensionsCard_container { + text-align: center; + margin: $spacing-20 var(--cpd-space-4x) 0; + } + + .mx_ExtensionsCard_Button { + /* this button is special so we have to override some of the original styling */ + /* as we will be applying it in its children */ + padding: 0; + height: auto; + color: $tertiary-content; + position: relative; + + .mx_WidgetAvatar { + flex-shrink: 0; + } + + .mx_ExtensionsCard_icon_app { + padding: var(--cpd-space-2x) var(--cpd-space-12x) var(--cpd-space-2x) var(--cpd-space-3x); + text-overflow: ellipsis; + overflow: hidden; + display: flex; + align-items: center; + + p { + margin: 0 var(--cpd-space-3x); + color: $primary-content; + } + } + + .mx_ExtensionsCard_app_pinToggle, + .mx_ExtensionsCard_app_options { + position: absolute; + top: 0; + height: 100%; /* to give bigger interactive zone */ + width: 24px; + padding: var(--cpd-space-3x) var(--cpd-space-1x); + box-sizing: border-box; + min-width: 24px; /* prevent flexbox crushing */ + + &:hover { + &::after { + content: ""; + position: absolute; + height: 24px; + width: 24px; + top: var(--cpd-space-2x); /* equal to padding-top of parent */ + left: 0; + border-radius: 12px; + background-color: rgba(141, 151, 165, 0.1); + } + } + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 16px; + background-color: $icon-button-color; + } + } + + .mx_ExtensionsCard_app_pinToggle { + right: 8px; + + &::before { + mask-image: url("$(res)/img/element-icons/room/pin-upright.svg"); + } + } + + .mx_ExtensionsCard_app_options { + right: 32px; /* 24 + 8 */ + &::before { + mask-image: url("$(res)/img/element-icons/room/ellipsis.svg"); + } + } + + &.mx_ExtensionsCard_Button_pinned { + &::after { + opacity: 0.2; + } + + .mx_ExtensionsCard_app_pinToggle::before { + background-color: $accent; + } + } + + &::before { + content: unset; + } + + &::after { + top: var(--cpd-space-2x); /* re-align based on the height change */ + pointer-events: none; /* pass through to the real button */ + } + } + + /* Set layout for everyone button */ + a[data-kind="primary"] { + margin-top: var(--cpd-space-10x); + } + + .mx_EmptyState::before { + /* Overlap the Add extensions button */ + top: -76px; + } +} diff --git a/res/css/views/right_panel/_PinnedMessagesCard.pcss b/res/css/views/right_panel/_PinnedMessagesCard.pcss index 5cdafcf7c5..23e23bae85 100644 --- a/res/css/views/right_panel/_PinnedMessagesCard.pcss +++ b/res/css/views/right_panel/_PinnedMessagesCard.pcss @@ -15,48 +15,38 @@ limitations under the License. */ .mx_PinnedMessagesCard { - .mx_PinnedMessagesCard_empty_wrapper { - display: flex; - height: 100%; - - .mx_PinnedMessagesCard_empty { - height: max-content; - text-align: center; - margin: auto 40px; - - .mx_PinnedMessagesCard_MessageActionBar { - pointer-events: none; - width: max-content; - margin: 0 auto; + --unpin-height: 76px; - /* Cancel the default values for non-interactivity */ - position: unset; - visibility: visible; - cursor: unset; - - &::before { - content: unset; - } - - .mx_MessageActionBar_optionsButton { - background: var(--MessageActionBar-item-hover-background); - border-radius: var(--MessageActionBar-item-hover-borderRadius); - z-index: var(--MessageActionBar-item-hover-zIndex); - color: var(--cpd-color-icon-primary); - } - } + .mx_PinnedMessagesCard_wrapper { + display: flex; + flex-direction: column; + padding: var(--cpd-space-4x); + gap: var(--cpd-space-6x); + overflow-y: auto; + + .mx_PinnedMessagesCard_Separator { + min-height: 1px; + /* Override default compound value */ + margin-block: 0; + } + } - .mx_PinnedMessagesCard_empty_header { - color: $primary-content; - margin-block: $spacing-24 $spacing-20; - } + .mx_PinnedMessagesCard_wrapper_unpin_all { + /* Remove the unpin all button height and the top and bottom padding */ + height: calc(100% - var(--unpin-height) - calc(var(--cpd-space-4x) * 2)); + } - > span { - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-content; - } - } + .mx_PinnedMessagesCard_unpin { + /* Make it float at the bottom of the unpin panel */ + position: absolute; + bottom: 0; + width: 100%; + height: var(--unpin-height); + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 4px 24px 0 rgba(27, 29, 34, 0.1); + background: var(--cpd-color-bg-canvas-default); } .mx_EventTile_body { diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index 549eb69ee4..da1a5a2510 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -15,6 +15,9 @@ limitations under the License. */ .mx_RoomSummaryCard { + --cpd-separator-inset: var(--cpd-space-4x); + --cpd-separator-spacing: var(--cpd-space-4x); + .mx_RoomSummaryCard_container { text-align: center; margin: $spacing-20 var(--cpd-space-4x) 0; @@ -33,32 +36,16 @@ limitations under the License. text-overflow: ellipsis; } - .mx_RoomSummaryCard_aboutGroup { - .mx_RoomSummaryCard_Button { - padding-left: 44px; - - &::before { - content: ""; - position: absolute; - top: 8px; - left: 10px; - height: 24px; - width: 24px; - mask-repeat: no-repeat; - mask-position: center; - background-color: $icon-button-color; - } - } - } - .mx_RoomSummaryCard_topic { padding: 0 12px; + color: var(--cpd-color-text-secondary); .mx_Box { width: 100%; } .mx_RoomSummaryCard_topic_container { + text-align: start; display: flex; } @@ -97,131 +84,6 @@ limitations under the License. } } - .mx_RoomSummaryCard_appsGroup { - .mx_RoomSummaryCard_Button { - /* this button is special so we have to override some of the original styling */ - /* as we will be applying it in its children */ - padding: 0; - height: auto; - color: $tertiary-content; - - .mx_RoomSummaryCard_icon_app { - padding: 10px 48px 10px 12px; /* based on typical mx_RoomSummaryCard_Button padding */ - text-overflow: ellipsis; - overflow: hidden; - display: flex; - justify-content: center; - span { - /* Center aligned and Spacing matched with the About section above the Widgets section */ - margin-right: 10px; - display: flex; - justify-content: center; - align-items: center; - color: $primary-content; - } - } - - .mx_RoomSummaryCard_app_pinToggle, - .mx_RoomSummaryCard_app_maximiseToggle, - .mx_RoomSummaryCard_app_options { - position: absolute; - top: 0; - height: 100%; /* to give bigger interactive zone */ - width: 24px; - padding: 12px 4px; - box-sizing: border-box; - min-width: 24px; /* prevent flexbox crushing */ - - &:hover { - &::after { - content: ""; - position: absolute; - height: 24px; - width: 24px; - top: 8px; /* equal to padding-top of parent */ - left: 0; - border-radius: 12px; - background-color: rgba(141, 151, 165, 0.1); - } - } - - &::before { - content: ""; - position: absolute; - height: 16px; - width: 16px; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 16px; - background-color: $icon-button-color; - } - } - - .mx_RoomSummaryCard_app_pinToggle { - right: 8px; - - &::before { - mask-image: url("$(res)/img/element-icons/room/pin-upright.svg"); - } - } - .mx_RoomSummaryCard_app_maximiseToggle { - right: 32px; /* 24 + 8 */ - - &::before { - mask-size: 14px; - mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); - } - } - - .mx_RoomSummaryCard_app_options { - right: 56px; /* 2*24 + 8 */ - display: none; - &::before { - mask-image: url("$(res)/img/element-icons/room/ellipsis.svg"); - } - } - - &.mx_RoomSummaryCard_Button_pinned { - &::after { - opacity: 0.2; - } - - .mx_RoomSummaryCard_app_pinToggle::before { - background-color: $accent; - } - } - - &.mx_RoomSummaryCard_Button_maximised { - &::after { - opacity: 0.2; - } - - .mx_RoomSummaryCard_app_maximiseToggle::before { - background-color: $accent; - } - } - - &:hover { - .mx_RoomSummaryCard_icon_app { - padding-right: 72px; - } - - .mx_RoomSummaryCard_app_options { - display: unset; - } - } - - &::before { - content: unset; - } - - &::after { - top: 8px; /* re-align based on the height change */ - pointer-events: none; /* pass through to the real button */ - } - } - } - .mx_AccessibleButton_kind_link { margin-top: 12px; margin-bottom: 12px; diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index a2e156e0e5..186eb78a32 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -97,7 +97,7 @@ limitations under the License. h2 { text-transform: uppercase; color: $tertiary-content; - font: var(--cpd-font-heading-sm-semibold); + font: var(--cpd-font-body-md-semibold); font-weight: var(--cpd-font-weight-semibold); margin: $spacing-4 0; } diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 66c60f5f15..62204b52c3 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -43,6 +43,7 @@ $left-gutter: 64px; .mx_EventTile_body { overflow-y: hidden; + text-align: start; } .mx_EventTile_receiptSent, @@ -676,7 +677,7 @@ $left-gutter: 64px; font-size: $font-12px; color: $secondary-content; display: inline-block; - margin-left: 9px; + margin-inline-start: 9px; } .mx_EventTile_edited { @@ -786,6 +787,17 @@ $left-gutter: 64px; ul { list-style-type: disc; } + + /* override styles from the base markdown CSS that put markdown content on its own line, + as this isn't what we want for richtext emote content. + */ + &::before { + display: none; + } + + &::after { + display: none; + } } } @@ -886,6 +898,10 @@ $left-gutter: 64px; &.markdown-body img { object-fit: contain; object-position: left top; + + /* Override the default colors of the 'github-markdown-css' library + (#fff for light theme, #000 for dark theme) to match the inherited theme */ + background-color: inherit !important; } .mx_EventTile_clamp & { @@ -1443,6 +1459,10 @@ $left-gutter: 64px; } } +.mx_EventTile_annotated { + display: flex; +} + /* Media query for mobile UI */ @media only screen and (max-width: 480px) { .mx_EventTile_content { diff --git a/res/css/views/rooms/_PinnedEventTile.pcss b/res/css/views/rooms/_PinnedEventTile.pcss index e755c3a71d..b42de75649 100644 --- a/res/css/views/rooms/_PinnedEventTile.pcss +++ b/res/css/views/rooms/_PinnedEventTile.pcss @@ -15,95 +15,27 @@ limitations under the License. */ .mx_PinnedEventTile { - min-height: 40px; - width: 100%; - padding: 0 4px 12px; - - display: grid; - grid-template-areas: - "avatar name remove" - "content content content" - "footer footer footer"; - grid-template-rows: max-content auto max-content; - grid-template-columns: 24px auto 24px; - grid-row-gap: 12px; - grid-column-gap: 8px; - - & + .mx_PinnedEventTile { - padding: 12px 4px; - border-top: 1px solid $menu-border-color; - } - - .mx_PinnedEventTile_senderAvatar, - .mx_PinnedEventTile_sender, - .mx_PinnedEventTile_unpinButton, - .mx_PinnedEventTile_message, - .mx_PinnedEventTile_footer { - min-width: 0; /* Prevent a grid blowout */ - } - - .mx_PinnedEventTile_senderAvatar { - grid-area: avatar; - } - - .mx_PinnedEventTile_sender { - grid-area: name; - font-weight: var(--cpd-font-weight-semibold); - font-size: $font-15px; - line-height: $font-24px; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - .mx_PinnedEventTile_unpinButton { - visibility: hidden; - grid-area: remove; - position: relative; - width: 24px; - height: 24px; - border-radius: 8px; - - &:hover { - background-color: $roomheader-addroom-bg-color; - } - - &::before { - content: ""; - position: absolute; - height: inherit; - width: inherit; - background: $secondary-content; - mask-position: center; - mask-size: 8px; - mask-repeat: no-repeat; - mask-image: url("$(res)/img/image-view/close.svg"); - } - } - - .mx_PinnedEventTile_message { - grid-area: content; - } - - .mx_PinnedEventTile_footer { - grid-area: footer; - font-size: $font-10px; - line-height: 12px; - - .mx_PinnedEventTile_timestamp { - color: $secondary-content; - display: unset; - width: unset; /* Cancel the default width value */ - } - - .mx_AccessibleButton_kind_link { - margin-left: 12px; - } - } - - &:hover { - .mx_PinnedEventTile_unpinButton { - visibility: visible; + display: flex; + gap: var(--cpd-space-4x); + align-items: flex-start; + + .mx_PinnedEventTile_wrapper { + display: flex; + flex-direction: column; + gap: var(--cpd-space-1x); + width: 100%; + + .mx_PinnedEventTile_top { + display: flex; + gap: var(--cpd-space-1x); + justify-content: space-between; + align-items: center; + + .mx_PinnedEventTile_sender { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } } } } diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 5409c050bb..86eabd703e 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -63,31 +63,6 @@ limitations under the License. align-items: center; } -.mx_RoomHeader_topic { - height: 0; - opacity: 0; - transition: all var(--transition-standard) ease 0.1s; - /* Emojis are rendered a bit bigger than text in the timeline - Make them compact/the same size as text here */ - .mx_Emoji { - font-size: inherit; - } -} - -.mx_RoomHeader:hover, -.mx_RoomHeader:focus-within { - .mx_RoomHeader_topic { - /* height needed to compute the transition, it equals to the `line-height` - value in pixels */ - height: calc($font-13px * 1.5); - opacity: 1; - - a:hover { - text-decoration: underline; - } - } -} - .mx_RoomHeader_icon { flex-shrink: 0; } diff --git a/res/css/views/rooms/_RoomPreviewBar.pcss b/res/css/views/rooms/_RoomPreviewBar.pcss index be50c9faf2..a3fae0e008 100644 --- a/res/css/views/rooms/_RoomPreviewBar.pcss +++ b/res/css/views/rooms/_RoomPreviewBar.pcss @@ -163,6 +163,10 @@ a.mx_RoomPreviewBar_inviter { cursor: pointer; } +.mx_RoomPreviewBar_inviter_mxid { + color: var(--cpd-color-text-secondary); +} + .mx_RoomPreviewBar_icon { margin-right: 8px; vertical-align: text-top; diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.pcss similarity index 54% rename from res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss rename to res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.pcss index a59f64b391..0cc2b3d5af 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.pcss @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,28 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GeneralUserSettingsTab_section--discovery_existing { - display: flex; - align-items: center; -} - -.mx_GeneralUserSettingsTab_section--discovery_existing_address, -.mx_GeneralUserSettingsTab_section--discovery_existing_promptText { - flex: 1; - margin-right: 10px; -} - -.mx_GeneralUserSettingsTab_section--discovery_existing_button { - margin-left: 5px; -} - -.mx_GeneralUserSettingsTab_warningIcon { - vertical-align: middle; - margin-right: $spacing-8; - margin-bottom: 2px; -} - -.mx_GeneralUserSettingsTab_section_hint { +.mx_PreferencesUserSettingsTab_section_hint { font: var(--cpd-font-body-sm-regular); color: var(--cpd-color-text-secondary); } diff --git a/res/css/views/user-onboarding/_UserOnboardingTask.pcss b/res/css/views/user-onboarding/_UserOnboardingTask.pcss index 05232da8c5..6bb54207b7 100644 --- a/res/css/views/user-onboarding/_UserOnboardingTask.pcss +++ b/res/css/views/user-onboarding/_UserOnboardingTask.pcss @@ -44,6 +44,10 @@ limitations under the License. transition: all 500ms; + .mx_UserOnboardingTask_title { + font: var(--cpd-font-body-md-medium); + } + .mx_UserOnboardingTask_description { font-size: $font-12px; } diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss index a306e769b0..213c641440 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -64,14 +64,6 @@ $accent-1400: var(--cpd-color-green-1400); outline-offset: 2px; } -/* Add padding, so the outline is not chopped off on the left */ -.mx_BaseCard { - padding-left: 4px !important; /* Remove 4 to allow 4 in mx_BaseCard_Group */ -} -.mx_BaseCard_Group { - padding-left: 4px !important; -} - .mx_BasicMessageComposer .mx_BasicMessageComposer_inputEmpty > :first-child::before { color: $secondary-content; opacity: 1 !important; diff --git a/src/@types/electron-to-chromium.d.ts b/src/@types/electron-to-chromium.d.ts new file mode 100644 index 0000000000..8ee13e92ad --- /dev/null +++ b/src/@types/electron-to-chromium.d.ts @@ -0,0 +1,22 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +declare module "electron-to-chromium/versions" { + const versionMap: { + [electronVersion: string]: string; + }; + export default versionMap; +} diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index e42e83d58d..fab9791ada 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -144,69 +144,14 @@ declare global { usageDetails?: { [key: string]: number }; } - // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas - interface OffscreenCanvas { - convertToBlob(opts?: { type?: string; quality?: number }): Promise; - } - - interface HTMLAudioElement { - type?: string; - } - - interface HTMLVideoElement { - type?: string; - } - - // Add Chrome-specific `instant` ScrollBehaviour - type _ScrollBehavior = ScrollBehavior | "instant"; - - interface _ScrollOptions { - behavior?: _ScrollBehavior; - } - - interface _ScrollIntoViewOptions extends _ScrollOptions { - block?: ScrollLogicalPosition; - inline?: ScrollLogicalPosition; - } - interface Element { // Safari & IE11 only have this prefixed: we used prefixed versions // previously so let's continue to support them for now webkitRequestFullScreen(options?: FullscreenOptions): Promise; msRequestFullscreen(options?: FullscreenOptions): Promise; - scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void; - } - - interface Error { - // Standard - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause - cause?: unknown; - - // Non-standard - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName - fileName?: string; - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/lineNumber - lineNumber?: number; - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber - columnNumber?: number; - } - - // We can remove these pieces if we ever update to `target: "es2022"` in our - // TypeScript config which supports the new `cause` property, see - // https://github.com/vector-im/element-web/issues/24913 - interface ErrorOptions { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause - cause?: unknown; - } - - interface ErrorConstructor { - new (message?: string, options?: ErrorOptions): Error; - (message?: string, options?: ErrorOptions): Error; + // scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void; } - // eslint-disable-next-line no-var - var Error: ErrorConstructor; - // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 interface AudioWorkletProcessor { readonly port: MessagePort; diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index ee244f2c92..9549e6d08b 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -271,9 +271,7 @@ 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] | undefined> { + public async haveMsisdnToken(msisdnToken: string): Promise<[success?: boolean, result?: IAuthData | Error | null]> { const authClient = new IdentityAuthClient(); if (this.submitUrl) { @@ -301,13 +299,14 @@ export default class AddThreepid { id_server: getIdServerDomain(this.matrixClient), id_access_token: await authClient.getAccessToken(), }); + return [true]; } else { try { await this.makeAddThreepidOnlyRequest(); // The spec has always required this to use UI auth but synapse briefly // implemented it without, so this may just succeed and that's OK. - return; + return [true]; } catch (err) { if (!(err instanceof MatrixError) || err.httpStatus !== 401 || !err.data || !err.data.flows) { // doesn't look like an interactive-auth failure diff --git a/src/DraftCleaner.ts b/src/DraftCleaner.ts new file mode 100644 index 0000000000..5e6c1cbae7 --- /dev/null +++ b/src/DraftCleaner.ts @@ -0,0 +1,78 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; + +import { MatrixClientPeg } from "./MatrixClientPeg"; +import { EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/SendMessageComposer"; + +// The key used to persist the the timestamp we last cleaned up drafts +export const DRAFT_LAST_CLEANUP_KEY = "mx_draft_cleanup"; +// The period of time we wait between cleaning drafts +export const DRAFT_CLEANUP_PERIOD = 1000 * 60 * 60 * 24 * 30; + +/** + * Checks if `DRAFT_CLEANUP_PERIOD` has expired, if so, deletes any stord editor drafts that exist for rooms that are not in the known list. + */ +export function cleanUpDraftsIfRequired(): void { + if (!shouldCleanupDrafts()) { + return; + } + logger.debug(`Cleaning up editor drafts...`); + cleaupDrafts(); + try { + localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(Date.now())); + } catch (error) { + logger.error("Failed to persist draft cleanup key", error); + } +} + +/** + * + * @returns {bool} True if the timestamp has not been persisted or the `DRAFT_CLEANUP_PERIOD` has expired. + */ +function shouldCleanupDrafts(): boolean { + try { + const lastCleanupTimestamp = localStorage.getItem(DRAFT_LAST_CLEANUP_KEY); + if (!lastCleanupTimestamp) { + return true; + } + const parsedTimestamp = Number.parseInt(lastCleanupTimestamp || "", 10); + if (!Number.isInteger(parsedTimestamp)) { + return true; + } + return Date.now() > parsedTimestamp + DRAFT_CLEANUP_PERIOD; + } catch (error) { + return true; + } +} + +/** + * Clear all drafts for the CIDER editor if the room does not exist in the known rooms. + */ +function cleaupDrafts(): void { + for (let i = 0; i < localStorage.length; i++) { + const keyName = localStorage.key(i); + if (!keyName?.startsWith(EDITOR_STATE_STORAGE_PREFIX)) continue; + // Remove the prefix and the optional event id suffix to leave the room id + const roomId = keyName.slice(EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0]; + const room = MatrixClientPeg.safeGet().getRoom(roomId); + if (!room) { + logger.debug(`Removing draft for unknown room with key ${keyName}`); + localStorage.removeItem(keyName); + } + } +} diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 888c30d76c..774cef87a1 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -246,23 +246,6 @@ class HtmlHighlighter extends BaseHighlighter { } } -interface IOpts { - highlightLink?: string; - disableBigEmoji?: boolean; - stripReplyFallback?: boolean; - returnString?: boolean; - forComposerQuote?: boolean; - ref?: React.Ref; -} - -export interface IOptsReturnNode extends IOpts { - returnString?: false | undefined; -} - -export interface IOptsReturnString extends IOpts { - returnString: true; -} - const emojiToHtmlSpan = (emoji: string): string => `${emoji}`; const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => ( @@ -307,35 +290,35 @@ export function formatEmojis(message: string | undefined, isHtmlMessage?: boolea return result; } -/* turn a matrix event body into html - * - * content: 'content' of the MatrixEvent - * - * highlights: optional list of words to highlight, ordered by longest word first - * - * opts.highlightLink: optional href to add to highlighted words - * opts.disableBigEmoji: optional argument to disable the big emoji class. - * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing - * opts.returnString: return an HTML string rather than JSX elements - * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer - * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) - */ -export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnString): string; -export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnNode): ReactNode; -export function bodyToHtml(content: IContent, highlights: Optional, opts: IOpts = {}): ReactNode | string { - const isFormattedBody = content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string"; - let bodyHasEmoji = false; - let isHtmlMessage = false; +interface EventAnalysis { + bodyHasEmoji: boolean; + isHtmlMessage: boolean; + strippedBody: string; + safeBody?: string; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext + isFormattedBody: boolean; +} +export interface EventRenderOpts { + highlightLink?: string; + disableBigEmoji?: boolean; + stripReplyFallback?: boolean; + forComposerQuote?: boolean; +} + +function analyseEvent(content: IContent, highlights: Optional, opts: EventRenderOpts = {}): EventAnalysis { let sanitizeParams = sanitizeHtmlParams; if (opts.forComposerQuote) { sanitizeParams = composerSanitizeHtmlParams; } - let strippedBody: string; - let safeBody: string | undefined; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext - try { + const isFormattedBody = + content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string"; + let bodyHasEmoji = false; + let isHtmlMessage = false; + + let safeBody: string | undefined; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext + // sanitizeHtml can hang if an unclosed HTML tag is thrown at it // A search for `, op const plainBody = typeof content.body === "string" ? content.body : ""; if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody); - strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody; + const strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody; bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody! : plainBody); const highlighter = safeHighlights?.length @@ -384,13 +367,73 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } else if (highlighter) { safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join(""); } + + return { bodyHasEmoji, isHtmlMessage, strippedBody, safeBody, isFormattedBody }; } finally { delete sanitizeParams.textFilter; } +} + +export function bodyToDiv( + content: IContent, + highlights: Optional, + opts: EventRenderOpts = {}, + ref?: React.Ref, +): ReactNode { + const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts); + + return formattedBody ? ( +
+ ) : ( +
+ {emojiBodyElements || strippedBody} +
+ ); +} + +export function bodyToSpan( + content: IContent, + highlights: Optional, + opts: EventRenderOpts = {}, + ref?: React.Ref, + includeDir = true, +): ReactNode { + const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts); + + return formattedBody ? ( + + ) : ( + + {emojiBodyElements || strippedBody} + + ); +} + +interface BodyToNodeReturn { + strippedBody: string; + formattedBody?: string; + emojiBodyElements: JSX.Element[] | undefined; + className: string; +} + +function bodyToNode(content: IContent, highlights: Optional, opts: EventRenderOpts = {}): BodyToNodeReturn { + const eventInfo = analyseEvent(content, highlights, opts); let emojiBody = false; - if (!opts.disableBigEmoji && bodyHasEmoji) { - const contentBody = safeBody ?? strippedBody; + if (!opts.disableBigEmoji && eventInfo.bodyHasEmoji) { + const contentBody = eventInfo.safeBody ?? eventInfo.strippedBody; let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : ""; // Remove zero width joiner, zero width spaces and other spaces in body @@ -405,44 +448,56 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // Prevent user pills expanding for users with only emoji in // their username. Permalinks (links in pills) can be any URL // now, so we just check for an HTTP-looking thing. - (strippedBody === safeBody || // replies have the html fallbacks, account for that here + (eventInfo.strippedBody === eventInfo.safeBody || // replies have the html fallbacks, account for that here content.formatted_body === undefined || (!content.formatted_body.includes("http:") && !content.formatted_body.includes("https:"))); } - if (isFormattedBody && bodyHasEmoji && safeBody) { - // This has to be done after the emojiBody check above as to not break big emoji on replies - safeBody = formatEmojis(safeBody, true).join(""); - } - - if (opts.returnString) { - return safeBody ?? strippedBody; - } - const className = classNames({ "mx_EventTile_body": true, "mx_EventTile_bigEmoji": emojiBody, - "markdown-body": isHtmlMessage && !emojiBody, + "markdown-body": eventInfo.isHtmlMessage && !emojiBody, + // Override the global `notranslate` class set by the top-level `matrixchat` div. + "translate": true, }); + let formattedBody = eventInfo.safeBody; + if (eventInfo.isFormattedBody && eventInfo.bodyHasEmoji && eventInfo.safeBody) { + // This has to be done after the emojiBody check as to not break big emoji on replies + formattedBody = formatEmojis(eventInfo.safeBody, true).join(""); + } + let emojiBodyElements: JSX.Element[] | undefined; - if (!safeBody && bodyHasEmoji) { - emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; + if (!eventInfo.safeBody && eventInfo.bodyHasEmoji) { + emojiBodyElements = formatEmojis(eventInfo.strippedBody, false) as JSX.Element[]; } - return safeBody ? ( - - ) : ( - - {emojiBodyElements || strippedBody} - - ); + return { strippedBody: eventInfo.strippedBody, formattedBody, emojiBodyElements, className }; +} + +/** + * Turn a matrix event body into html + * + * content: 'content' of the MatrixEvent + * + * highlights: optional list of words to highlight, ordered by longest word first + * + * opts.highlightLink: optional href to add to highlighted words + * opts.disableBigEmoji: optional argument to disable the big emoji class. + * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing + * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer + * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) + */ +export function bodyToHtml(content: IContent, highlights: Optional, opts: EventRenderOpts = {}): string { + const eventInfo = analyseEvent(content, highlights, opts); + + let formattedBody = eventInfo.safeBody; + if (eventInfo.isFormattedBody && eventInfo.bodyHasEmoji && formattedBody) { + // This has to be done after the emojiBody check above as to not break big emoji on replies + formattedBody = formatEmojis(eventInfo.safeBody, true).join(""); + } + + return formattedBody ?? eventInfo.strippedBody; } /** diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 4b57e2098d..ab8dfcad17 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -75,6 +75,10 @@ export interface IConfigOptions { available: boolean; logo: string; // url url: string; // download url + url_macos?: string; + url_win64?: string; + url_win32?: string; + url_linux?: string; }; mobile_builds: { ios: string | null; // download url diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index e2141cd94d..38852c68fb 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -58,7 +58,6 @@ import { setSentryUser } from "./sentry"; import SdkConfig from "./SdkConfig"; import { DialogOpener } from "./utils/DialogOpener"; import { Action } from "./dispatcher/actions"; -import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler"; import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload"; import { SdkContextClass } from "./contexts/SDKContext"; import { messageForLoginError } from "./utils/ErrorUtils"; @@ -83,6 +82,7 @@ import { tryDecryptToken, } from "./utils/tokens/tokens"; import { TokenRefresher } from "./utils/oidc/TokenRefresher"; +import { checkBrowserSupport } from "./SupportedBrowser"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -102,7 +102,7 @@ dis.register((payload) => { // If we unset the client and the component is updated, the render will fail and unmount everything. // (The module dialog closes and fires a `aria_unhide_main_app` that will trigger a re-render) stopMatrixClient(false); - doSetLoggedIn(typed.credentials, true).catch((e) => { + doSetLoggedIn(typed.credentials, true, true).catch((e) => { // XXX we might want to fire a new event here to let the app know that the login failed ? // The module api could use it to display a message to the user. logger.warn("Failed to overwrite login", e); @@ -208,6 +208,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise guest: true, }, true, + false, ).then(() => true); } const success = await restoreFromLocalStorage({ @@ -465,6 +466,7 @@ function registerAsGuest(hsUrl: string, isUrl?: string, defaultDeviceDisplayName guest: true, }, true, + true, ).then(() => true); }, (err) => { @@ -586,7 +588,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): const pickleKey = (await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "")) ?? undefined; if (pickleKey) { - logger.log("Got pickle key"); + logger.log(`Got pickle key for ${userId}|${deviceId}`); } else { logger.log("No pickle key available"); } @@ -610,6 +612,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): freshLogin: freshLogin, }, false, + false, ); return true; } else { @@ -658,12 +661,12 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { +async function doSetLoggedIn( + credentials: IMatrixClientCreds, + clearStorageEnabled: boolean, + isFreshLogin: boolean, +): Promise { checkSessionLock(); credentials.guest = Boolean(credentials.guest); @@ -840,6 +848,9 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable clientPegOpts.rustCryptoStoreKey?.fill(0); } + // Run the migrations after the MatrixClientPeg has been assigned + SettingsStore.runMigrations(isFreshLogin); + return client; } @@ -1001,6 +1012,7 @@ async function startMatrixClient( IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.instance.start(); LegacyCallHandler.instance.start(); + checkBrowserSupport(); // Start Mjolnir even though we haven't checked the feature flag yet. Starting // the thing just wastes CPU cycles, but should result in no actual functionality @@ -1020,9 +1032,6 @@ async function startMatrixClient( checkSessionLock(); - // Run the migrations after the MatrixClientPeg has been assigned - SettingsStore.runMigrations(); - // This needs to be started after crypto is set up DeviceListener.sharedInstance().start(client); // Similarly, don't start sending presence updates until we've started @@ -1085,7 +1094,6 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise string | null> = { }, }; -class NotifierClass { +export const enum NotifierEvent { + NotificationHiddenChange = "notification_hidden_change", +} + +interface EmittedEvents { + [NotifierEvent.NotificationHiddenChange]: (hidden: boolean) => void; +} + +class NotifierClass extends TypedEventEmitter { private notifsByRoom: Record = {}; // A list of event IDs that we've received but need to wait until @@ -356,6 +366,7 @@ class NotifierClass { if (persistent && global.localStorage) { global.localStorage.setItem("notifications_hidden", String(hidden)); } + this.emit(NotifierEvent.NotificationHiddenChange, hidden); } public shouldShowPrompt(): boolean { @@ -505,10 +516,16 @@ class NotifierClass { * Some events require special handling such as showing in-app toasts */ private performCustomEventHandling(ev: MatrixEvent): void { + const cli = MatrixClientPeg.safeGet(); + const room = cli.getRoom(ev.getRoomId()); + const thisUserHasConnectedDevice = + room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId()); + if ( EventType.CallNotify === ev.getType() && SettingsStore.getValue("feature_group_calls") && - (ev.getAge() ?? 0) < 10000 + (ev.getAge() ?? 0) < 10000 && + !thisUserHasConnectedDevice ) { const content = ev.getContent(); const roomId = ev.getRoomId(); diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index a73a31a68e..92b31ae918 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -67,6 +67,10 @@ export const DEFAULTS: DeepReadonly = { available: true, logo: "vector-icons/1024.png", url: "https://element.io/download", + url_macos: "https://packages.element.io/desktop/install/macos/Element.dmg", + url_win64: "https://packages.element.io/desktop/install/win32/x64/Element%20Setup.exe", + url_win32: "https://packages.element.io/desktop/install/win32/ia32/Element%20Setup.exe", + url_linux: "https://element.io/download#linux", }, mobile_builds: { ios: "https://apps.apple.com/app/vector/id1083446067", diff --git a/src/SupportedBrowser.ts b/src/SupportedBrowser.ts new file mode 100644 index 0000000000..071a64f4fc --- /dev/null +++ b/src/SupportedBrowser.ts @@ -0,0 +1,125 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; +import browserlist from "browserslist"; +import electronToChromium from "electron-to-chromium/versions"; +import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; + +import { DeviceType, parseUserAgent } from "./utils/device/parseUserAgent"; +import ToastStore from "./stores/ToastStore"; +import GenericToast from "./components/views/toasts/GenericToast"; +import { _t } from "./languageHandler"; +import SdkConfig from "./SdkConfig"; + +export const LOCAL_STORAGE_KEY = "mx_accepts_unsupported_browser"; +const TOAST_KEY = "unsupportedbrowser"; + +const SUPPORTED_DEVICE_TYPES = [DeviceType.Web, DeviceType.Desktop]; +const SUPPORTED_BROWSER_QUERY = + "last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 Edge versions"; +const LEARN_MORE_URL = "https://github.com/element-hq/element-web#supported-environments"; + +function onLearnMoreClick(): void { + onDismissClick(); + window.open(LEARN_MORE_URL, "_blank", "noopener,noreferrer"); +} + +function onDismissClick(): void { + localStorage.setItem(LOCAL_STORAGE_KEY, String(true)); + ToastStore.sharedInstance().dismissToast(TOAST_KEY); +} + +function getBrowserNameVersion(browser: string): [name: string, version: number] { + const [browserName, browserVersion] = browser.split(" "); + const browserNameLc = browserName.toLowerCase(); + if (browserNameLc === "electron") { + // The electron-to-chromium map is keyed by the major and minor version of Electron + const chromiumVersion = electronToChromium[browserVersion.split(".").slice(0, 2).join(".")]; + if (chromiumVersion) { + return ["chrome", parseInt(chromiumVersion, 10)]; + } + } + + return [browserNameLc, parseInt(browserVersion, 10)]; +} + +/** + * Function to check if the current browser is considered supported by our support policy. + * Based on user agent parsing so may be inaccurate if the user has fingerprint prevention turned up to 11. + */ +export function getBrowserSupport(): boolean { + const browsers = browserlist(SUPPORTED_BROWSER_QUERY).sort(); + const minimumBrowserVersions = new Map(); + for (const browser of browsers) { + const [browserName, browserVersion] = getBrowserNameVersion(browser); + // We sorted the browsers so will encounter the minimum version first + if (minimumBrowserVersions.has(browserName)) continue; + minimumBrowserVersions.set(browserName, browserVersion); + } + + const details = parseUserAgent(navigator.userAgent); + + let supported = true; + if (!SUPPORTED_DEVICE_TYPES.includes(details.deviceType)) { + logger.warn("Browser unsupported, unsupported device type", details.deviceType); + supported = false; + } + + if (details.client) { + const [browserName, browserVersion] = getBrowserNameVersion(details.client); + const minimumVersion = minimumBrowserVersions.get(browserName); + // Check both with the sub-version cut off and without as some browsers have less granular versioning e.g. Safari + if (!minimumVersion || browserVersion < minimumVersion) { + logger.warn("Browser unsupported, unsupported user agent", details.client); + supported = false; + } + } else { + logger.warn("Browser unsupported, unknown client", navigator.userAgent); + supported = false; + } + + return supported; +} + +/** + * Shows a user warning toast if the user's browser is not supported. + */ +export function checkBrowserSupport(): void { + const supported = getBrowserSupport(); + if (supported) return; + + if (localStorage.getItem(LOCAL_STORAGE_KEY)) { + logger.warn("Browser unsupported, but user has previously accepted"); + return; + } + + const brand = SdkConfig.get().brand; + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("unsupported_browser|title", { brand }), + props: { + description: _t("unsupported_browser|description", { brand }), + secondaryLabel: _t("action|learn_more"), + SecondaryIcon: PopOutIcon, + onSecondaryClick: onLearnMoreClick, + primaryLabel: _t("action|dismiss"), + onPrimaryClick: onDismissClick, + }, + component: GenericToast, + priority: 40, + }); +} diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index c0d7835747..2a37e53859 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -45,10 +45,11 @@ interface IState { export default class EmbeddedPage extends React.PureComponent { public static contextType = MatrixClientContext; + public declare context: React.ContextType; private unmounted = false; private dispatcherRef: string | null = null; - public constructor(props: IProps, context: typeof MatrixClientContext) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 9c3550804f..86a3e1cf83 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -59,6 +59,7 @@ interface IState { */ class FilePanel extends React.Component { public static contextType = RoomContext; + public declare context: React.ContextType; // This is used to track if a decrypted event was a live event and should be // added to the timeline. diff --git a/src/components/structures/GenericDropdownMenu.tsx b/src/components/structures/GenericDropdownMenu.tsx index e0fd3b7f9b..06854768db 100644 --- a/src/components/structures/GenericDropdownMenu.tsx +++ b/src/components/structures/GenericDropdownMenu.tsx @@ -97,6 +97,12 @@ type WithKeyFunction = T extends Key toKey: (key: T) => Key; }; +export interface AdditionalOptionsProps { + menuDisplayed: boolean; + closeMenu: () => void; + openMenu: () => void; +} + type IProps = WithKeyFunction & { value: T; options: readonly GenericDropdownMenuOption[] | readonly GenericDropdownMenuGroup[]; @@ -105,11 +111,7 @@ type IProps = WithKeyFunction & { onOpen?: (ev: ButtonEvent) => void; onClose?: (ev: ButtonEvent) => void; className?: string; - AdditionalOptions?: FunctionComponent<{ - menuDisplayed: boolean; - closeMenu: () => void; - openMenu: () => void; - }>; + AdditionalOptions?: FunctionComponent; }; export function GenericDropdownMenu({ diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 5fea4b15af..c1d1396ce2 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -144,6 +144,7 @@ import { SessionLockStolenView } from "./auth/SessionLockStolenView"; import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"; import { LoginSplashView } from "./auth/LoginSplashView"; import TchapUrls from "../../../../../src/tchap/util/TchapUrls"; // :TCHAP: activate-cross-signing-and-secure-storage-react +import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; // legacy export export { default as Views } from "../../Views"; @@ -1391,8 +1392,8 @@ export default class MatrixChat extends React.PureComponent { title: userNotice.title, props: { description: {userNotice.description}, - acceptLabel: _t("action|ok"), - onAccept: () => { + primaryLabel: _t("action|ok"), + onPrimaryClick: () => { ToastStore.sharedInstance().dismissToast(key); localStorage.setItem(key, "1"); }, @@ -1529,6 +1530,9 @@ export default class MatrixChat extends React.PureComponent { } if (state === SyncState.Syncing && prevState === SyncState.Syncing) { + // We know we have performabed a live update and known rooms should be in a good state. + // Now is a good time to clean up drafts. + cleanUpDraftsIfRequired(); return; } logger.debug(`MatrixClient sync state => ${state}`); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 3a97f27b38..07b600484a 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -205,7 +205,7 @@ interface IReadReceiptForUser { */ export default class MessagePanel extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; public static defaultProps = { disableGrouping: false, diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index dff2716c19..7c8fd71b79 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -42,11 +42,12 @@ interface IState { */ export default class NotificationPanel extends React.PureComponent { public static contextType = RoomContext; + public declare context: React.ContextType; private card = React.createRef(); - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { narrow: false, diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index bc80692459..8f1ba7aecf 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -34,7 +34,7 @@ import ThreadView from "./ThreadView"; import ThreadPanel from "./ThreadPanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; +import { PinnedMessagesCard } from "../views/right_panel/PinnedMessagesCard"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { E2EStatus } from "../../utils/ShieldUtils"; import TimelineCard from "../views/right_panel/TimelineCard"; @@ -43,6 +43,7 @@ import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/ import { Action } from "../../dispatcher/actions"; import { XOR } from "../../@types/common"; import { RightPanelTabs } from "../views/right_panel/RightPanelTabs"; +import ExtensionsCard from "../views/right_panel/ExtensionsCard"; interface BaseProps { overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView) @@ -72,7 +73,7 @@ interface IState { export default class RightPanel extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: Props, context: React.ContextType) { super(props, context); @@ -306,6 +307,12 @@ export default class RightPanel extends React.Component { } break; + case RightPanelPhases.Extensions: + if (!!this.props.room) { + card = ; + } + break; + case RightPanelPhases.Widget: if (!!this.props.room && !!cardState?.widgetId) { card = ; @@ -315,7 +322,7 @@ export default class RightPanel extends React.Component { return ( ); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 9af1121ab3..85419c3be3 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -15,7 +15,16 @@ limitations under the License. */ import React, { ReactNode } from "react"; -import { EventStatus, MatrixEvent, Room, MatrixError, SyncState, SyncStateData } from "matrix-js-sdk/src/matrix"; +import { + ClientEvent, + EventStatus, + MatrixError, + MatrixEvent, + Room, + RoomEvent, + SyncState, + SyncStateData, +} from "matrix-js-sdk/src/matrix"; import { Icon as WarningIcon } from "../../../res/img/feather-customised/warning-triangle.svg"; import { _t, _td } from "../../languageHandler"; @@ -80,8 +89,8 @@ interface IProps { } interface IState { - syncState: SyncState; - syncStateData: SyncStateData; + syncState: SyncState | null; + syncStateData: SyncStateData | null; unsentMessages: MatrixEvent[]; isResending: boolean; } @@ -89,8 +98,9 @@ interface IState { export default class RoomStatusBar extends React.PureComponent { private unmounted = false; public static contextType = MatrixClientContext; + public declare context: React.ContextType; - public constructor(props: IProps, context: typeof MatrixClientContext) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { @@ -103,8 +113,8 @@ export default class RoomStatusBar extends React.PureComponent { public componentDidMount(): void { const client = this.context; - client.on("sync", this.onSyncStateChange); - client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated); + client.on(ClientEvent.Sync, this.onSyncStateChange); + client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); this.checkSize(); } @@ -118,19 +128,19 @@ export default class RoomStatusBar extends React.PureComponent { // we may have entirely lost our client as we're logging out before clicking login on the guest bar... const client = this.context; if (client) { - client.removeListener("sync", this.onSyncStateChange); - client.removeListener("Room.localEchoUpdated", this.onRoomLocalEchoUpdated); + client.removeListener(ClientEvent.Sync, this.onSyncStateChange); + client.removeListener(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); } } - private onSyncStateChange = (state: SyncState, prevState: SyncState, data: SyncStateData): void => { + private onSyncStateChange = (state: SyncState, prevState: SyncState | null, data?: SyncStateData): void => { if (state === "SYNCING" && prevState === "SYNCING") { return; } if (this.unmounted) return; this.setState({ syncState: state, - syncStateData: data, + syncStateData: data ?? null, }); }; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a0555abbf7..70ee16b542 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -161,7 +161,7 @@ interface IRoomProps { // This defines the content of the mainSplit. // If the mainSplit does not contain the Timeline, the chat is shown in the right panel. -enum MainSplitContentType { +export enum MainSplitContentType { Timeline, MaximisedWidget, Call, @@ -417,7 +417,7 @@ export class RoomView extends React.Component { private roomViewBody = createRef(); public static contextType = SDKContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IRoomProps, context: React.ContextType) { super(props, context); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 7e1ad0389b..cbd12ec5a0 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -607,7 +607,7 @@ const SpaceSetupPrivateInvite: React.FC<{ export default class SpaceRoomView extends React.PureComponent { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private readonly dispatcherRef: string; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index ecbe7fa181..c2ce7d7289 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -34,14 +34,14 @@ export class Tab { * Creates a new tab. * @param {string} id The tab's ID. * @param {string} label The untranslated tab label. - * @param {string} icon The class for the tab icon. This should be a simple mask. + * @param {string|JSX.Element} icon An SVG element to use for the tab icon. Can also be a string for legacy icons, in which case it is the class for the tab icon. This should be a simple mask. * @param {React.ReactNode} body The JSX for the tab container. * @param {string} screenName The screen name to report to Posthog. */ public constructor( public readonly id: T, public readonly label: TranslationKey, - public readonly icon: string | null, + public readonly icon: string | JSX.Element | null, public readonly body: React.ReactNode, public readonly screenName?: ScreenName, ) {} @@ -99,7 +99,11 @@ function TabLabel({ tab, isActive, showToolip, onClick }: ITab let tabIcon: JSX.Element | undefined; if (tab.icon) { - tabIcon = ; + if (typeof tab.icon === "object") { + tabIcon = tab.icon; + } else if (typeof tab.icon === "string") { + tabIcon = ; + } } const id = domIDForTabID(tab.id); diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 8951cfcb91..792c97bc52 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -117,7 +117,7 @@ export const ThreadPanelHeader: React.FC<{ ) : null; const onMarkAllThreadsReadClick = React.useCallback( - (e) => { + (e: React.MouseEvent) => { PosthogTrackers.trackInteraction("WebThreadsMarkAllReadButton", e); if (!roomContext.room) { logger.error("No room in context to mark all threads read"); diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index a345faf9f7..f2d775464e 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -83,7 +83,7 @@ interface IState { export default class ThreadView extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private dispatcherRef: string | null = null; private readonly layoutWatcherRef: string; @@ -93,8 +93,8 @@ export default class ThreadView extends React.Component { // Set by setEventId in ctor. private eventId!: string; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.setEventId(this.props.mxEvent); const thread = this.props.room.getThread(this.eventId) ?? undefined; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 45198fff74..976c00e2fd 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -241,7 +241,7 @@ interface IEventIndexOpts { */ class TimelinePanel extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; // a map from room id to read marker event timestamp public static roomReadMarkerTsMap: Record = {}; @@ -273,7 +273,6 @@ class TimelinePanel extends React.Component { public constructor(props: IProps, context: React.ContextType) { super(props, context); - this.context = context; debuglog("mounting"); @@ -969,8 +968,8 @@ class TimelinePanel extends React.Component { private readMarkerTimeout(readMarkerPosition: number | null): number { return readMarkerPosition === 0 - ? this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs - : this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs; + ? (this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs) + : (this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs); } private async updateReadMarkerOnUserActivity(): Promise { diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index c95f5a1099..43c8f13e2b 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -16,6 +16,7 @@ limitations under the License. import * as React from "react"; import classNames from "classnames"; +import { Text } from "@vector-im/compound-web"; import ToastStore, { IToast } from "../../stores/ToastStore"; @@ -78,7 +79,9 @@ export default class ToastContainer extends React.Component<{}, IState> { if (title) { titleElement = (
-

{title}

+ + {title} + {countIndicator}
); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index ef33f82b0d..825779067a 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -27,7 +27,7 @@ import { UserTab } from "../views/dialogs/UserTab"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; -import LogoutDialog from "../views/dialogs/LogoutDialog"; +import LogoutDialog, { shouldShowLogoutDialog } from "../views/dialogs/LogoutDialog"; import SettingsStore from "../../settings/SettingsStore"; import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme"; import { RovingAccessibleButton } from "../../accessibility/RovingTabIndex"; @@ -90,7 +90,7 @@ const below = (rect: PartialDOMRect): MenuProps => { export default class UserMenu extends React.Component { public static contextType = SDKContext; - public context!: React.ContextType; + public declare context: React.ContextType; private dispatcherRef?: string; private themeWatcherRef?: string; @@ -100,7 +100,6 @@ export default class UserMenu extends React.Component { public constructor(props: IProps, context: React.ContextType) { super(props, context); - this.context = context; this.state = { contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), @@ -293,7 +292,7 @@ export default class UserMenu extends React.Component { ev.preventDefault(); ev.stopPropagation(); - if (await this.shouldShowLogoutDialog()) { + if (await shouldShowLogoutDialog(MatrixClientPeg.safeGet())) { Modal.createDialog(LogoutDialog); } else { defaultDispatcher.dispatch({ action: "logout" }); @@ -302,27 +301,6 @@ export default class UserMenu extends React.Component { this.setState({ contextMenuPosition: null }); // also close the menu }; - /** - * Checks if the `LogoutDialog` should be shown instead of the simple logout flow. - * The `LogoutDialog` will check the crypto recovery status of the account and - * help the user setup recovery properly if needed. - * @private - */ - private async shouldShowLogoutDialog(): Promise { - const cli = MatrixClientPeg.get(); - const crypto = cli?.getCrypto(); - if (!crypto) return false; - - // If any room is encrypted, we need to show the advanced logout flow - const allRooms = cli!.getRooms(); - for (const room of allRooms) { - const isE2e = await crypto.isEncryptionEnabledInRoom(room.roomId); - if (isE2e) return true; - } - - return false; - } - private onSignInClick = (): void => { defaultDispatcher.dispatch({ action: "start_login" }); this.setState({ contextMenuPosition: null }); // also close the menu diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 4d5dd258e0..7aa2d92d0c 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -41,10 +41,10 @@ interface IState { export default class UserView extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { loading: true, }; diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index f633ef0707..646b70ebda 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -515,6 +515,7 @@ export default class LoginComponent extends React.PureComponent fragmentAfterLogin={this.props.fragmentAfterLogin} primary={!this.state.flows?.find((flow) => flow.type === "m.login.password")} action={SSOAction.LOGIN} + disabled={this.isBusy()} /> ); }; @@ -589,6 +590,7 @@ export default class LoginComponent extends React.PureComponent end :TCHAP :*/} {this.renderLoginComponentForFlows()} diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 4c3ddb2f00..9d72c1d92d 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -252,15 +252,20 @@ export default class Registration extends React.Component { logger.error("Failed to get login flows to check for SSO support", e); } - this.setState(({ flows }) => ({ - matrixClient: cli, - ssoFlow, - oidcNativeFlow, - // if we are using oidc native we won't continue with flow discovery on HS - // so set an empty array to indicate flows are no longer loading - flows: oidcNativeFlow ? [] : flows, - busy: false, - })); + await new Promise((resolve) => { + this.setState( + ({ flows }) => ({ + matrixClient: cli, + ssoFlow, + oidcNativeFlow, + // if we are using oidc native we won't continue with flow discovery on HS + // so set an empty array to indicate flows are no longer loading + flows: oidcNativeFlow ? [] : flows, + busy: false, + }), + resolve, + ); + }); // don't need to check with homeserver for login flows // since we are going to use OIDC native flow diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index f623ae7dcb..ca34e3830a 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -72,13 +72,11 @@ interface IState { export default class SoftLogout extends React.Component { public static contextType = SDKContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); - this.context = context; - this.state = { loginView: LoginView.Loading, busy: false, diff --git a/src/components/structures/grouper/MainGrouper.tsx b/src/components/structures/grouper/MainGrouper.tsx index 28a62d7ac9..b1137c1729 100644 --- a/src/components/structures/grouper/MainGrouper.tsx +++ b/src/components/structures/grouper/MainGrouper.tsx @@ -26,6 +26,7 @@ import DateSeparator from "../../views/messages/DateSeparator"; import HistoryTile from "../../views/rooms/HistoryTile"; import EventListSummary from "../../views/elements/EventListSummary"; import { SeparatorKind } from "../../views/messages/TimelineSeparator"; +import SettingsStore from "../../../settings/SettingsStore"; const groupedStateEvents = [ EventType.RoomMember, @@ -97,6 +98,12 @@ export class MainGrouper extends BaseGrouper { // absorb hidden events to not split the summary return; } + + if (ev.getType() === EventType.RoomPinnedEvents && !SettingsStore.getValue("feature_pinning")) { + // If pinned messages are disabled, don't show the summary + return; + } + this.events.push(wrappedEvent); } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 7bed60d603..d54b52c1a0 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -833,7 +833,7 @@ export class SSOAuthEntry extends React.Component { - if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { + if (event.data === "authDone" && event.source === this.popupWindow) { if (this.popupWindow) { this.popupWindow.close(); this.popupWindow = null; @@ -950,7 +950,7 @@ export class FallbackAuthEntry extends React.Component { }; private onReceiveMessage = (event: MessageEvent): void => { - if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { + if (event.data === "authDone" && event.source === this.popupWindow) { this.props.submitAuthDict({}); } }; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index d956c26da2..b35109f5d7 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -19,7 +19,7 @@ limitations under the License. import React, { forwardRef, useCallback, useContext, useEffect, useState } from "react"; import classNames from "classnames"; -import { ClientEvent } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, SyncState } from "matrix-js-sdk/src/matrix"; import { Avatar } from "@vector-im/compound-web"; import SettingsStore from "../../../settings/SettingsStore"; @@ -80,7 +80,7 @@ const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [ }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps const cli = useContext(MatrixClientContext); - const onClientSync = useCallback((syncState, prevState) => { + const onClientSync = useCallback((syncState: SyncState, prevState: SyncState | null) => { // Consider the client reconnected if there is no error with syncing. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. const reconnected = syncState !== "ERROR" && prevState !== syncState; diff --git a/src/components/views/avatars/WithPresenceIndicator.tsx b/src/components/views/avatars/WithPresenceIndicator.tsx new file mode 100644 index 0000000000..1d32a99e00 --- /dev/null +++ b/src/components/views/avatars/WithPresenceIndicator.tsx @@ -0,0 +1,141 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { ReactNode, useEffect, useState } from "react"; +import { ClientEvent, Room, RoomMember, RoomStateEvent, UserEvent } from "matrix-js-sdk/src/matrix"; +import { Tooltip } from "@vector-im/compound-web"; + +import { isPresenceEnabled } from "../../../utils/presence"; +import { _t } from "../../../languageHandler"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { BUSY_PRESENCE_NAME } from "../rooms/PresenceLabel"; + +interface Props { + room: Room; + size: string; // CSS size + tooltipProps?: { + tabIndex?: number; + }; + children: ReactNode; +} + +enum Presence { + // Note: the names here are used in CSS class names + Online = "ONLINE", + Away = "AWAY", + Offline = "OFFLINE", + Busy = "BUSY", +} + +function tooltipText(variant: Presence): string { + switch (variant) { + case Presence.Online: + return _t("presence|online"); + case Presence.Away: + return _t("presence|away"); + case Presence.Offline: + return _t("presence|offline"); + case Presence.Busy: + return _t("presence|busy"); + } +} + +function getDmMember(room: Room): RoomMember | null { + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); + return otherUserId ? room.getMember(otherUserId) : null; +} + +export const useDmMember = (room: Room): RoomMember | null => { + const [dmMember, setDmMember] = useState(getDmMember(room)); + const updateDmMember = (): void => { + setDmMember(getDmMember(room)); + }; + + useEventEmitter(room.currentState, RoomStateEvent.Members, updateDmMember); + useEventEmitter(room.client, ClientEvent.AccountData, updateDmMember); + useEffect(updateDmMember, [room]); + + return dmMember; +}; + +function getPresence(member: RoomMember | null): Presence | null { + if (!member?.user) return null; + + const presence = member.user.presence; + const isOnline = member.user.currentlyActive || presence === "online"; + if (BUSY_PRESENCE_NAME.matches(member.user.presence)) { + return Presence.Busy; + } + if (isOnline) { + return Presence.Online; + } + if (presence === "offline") { + return Presence.Offline; + } + if (presence === "unavailable") { + return Presence.Away; + } + + return null; +} + +const usePresence = (room: Room, member: RoomMember | null): Presence | null => { + const [presence, setPresence] = useState(getPresence(member)); + const updatePresence = (): void => { + setPresence(getPresence(member)); + }; + + useEventEmitter(member?.user, UserEvent.Presence, updatePresence); + useEventEmitter(member?.user, UserEvent.CurrentlyActive, updatePresence); + useEffect(updatePresence, [member]); + + if (getJoinedNonFunctionalMembers(room).length !== 2 || !isPresenceEnabled(room.client)) return null; + return presence; +}; + +const WithPresenceIndicator: React.FC = ({ room, size, tooltipProps, children }) => { + const dmMember = useDmMember(room); + const presence = usePresence(room, dmMember); + + let icon: JSX.Element | undefined; + if (presence) { + icon = ( +
+ ); + } + + if (!presence) return <>{children}; + + return ( +
+ {children} + + {icon} + +
+ ); +}; + +export default WithPresenceIndicator; diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 95a0e832e1..bdae76bd6a 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -135,12 +135,12 @@ interface IState { export default class MessageContextMenu extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private reactButtonRef = createRef(); // XXX Ref to a functional component - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { canRedact: false, diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 3c77f32cb3..0dbd4b5395 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -52,6 +52,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent import { UIComponent } from "../../../settings/UIFeature"; import { DeveloperToolsOption } from "./DeveloperToolsOption"; import { tagRoom } from "../../../utils/room/tagRoom"; +import { useIsVideoRoom } from "../../../utils/video-rooms"; interface IProps extends IContextMenuProps { room: Room; @@ -113,10 +114,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { } const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId); - const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms"); - const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); - const isVideoRoom = - videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())); + const isVideoRoom = useIsVideoRoom(room); const canInvite = useEventEmitterState(cli, RoomMemberEvent.PowerLevel, () => room.canInvite(cli.getUserId()!)); let inviteOption: JSX.Element | undefined; if (canInvite && !isDm && shouldShowComponent(UIComponent.InviteUsers)) { diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index d58b8362f3..6be19cea58 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -33,6 +33,7 @@ import { sendSentryReport } from "../../../sentry"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import TchapUtils from "../../../../../../src/tchap/util/TchapUtils"; // :TCHAP: +import { getBrowserSupport } from "../../../SupportedBrowser"; interface IProps { onFinished: (success: boolean) => void; @@ -232,7 +233,10 @@ export default class BugReportDialog extends React.Component { } let warning: JSX.Element | undefined; - if (window.Modernizr && Object.values(window.Modernizr).some((support) => support === false)) { + if ( + (window.Modernizr && Object.values(window.Modernizr).some((support) => support === false)) || + !getBrowserSupport() + ) { warning = (

{_t("bug_reporting|unsupported_browser")} diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 253a43b0f0..3714a71e8c 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -17,6 +17,7 @@ limitations under the License. import React 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"; @@ -58,6 +59,25 @@ interface IState { backupStatus: BackupStatus; } +/** + * Checks if the `LogoutDialog` should be shown instead of the simple logout flow. + * The `LogoutDialog` will check the crypto recovery status of the account and + * help the user setup recovery properly if needed. + */ +export async function shouldShowLogoutDialog(cli: MatrixClient): Promise { + const crypto = cli?.getCrypto(); + if (!crypto) return false; + + // If any room is encrypted, we need to show the advanced logout flow + const allRooms = cli!.getRooms(); + for (const room of allRooms) { + const isE2e = await crypto.isEncryptionEnabledInRoom(room.roomId); + if (isE2e) return true; + } + + return false; +} + export default class LogoutDialog extends React.Component { public static defaultProps = { onFinished: function () {}, diff --git a/src/components/views/dialogs/UnpinAllDialog.tsx b/src/components/views/dialogs/UnpinAllDialog.tsx new file mode 100644 index 0000000000..ef7439d858 --- /dev/null +++ b/src/components/views/dialogs/UnpinAllDialog.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { JSX } from "react"; +import { Button, Text } from "@vector-im/compound-web"; +import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import BaseDialog from "../dialogs/BaseDialog"; +import { _t } from "../../../languageHandler"; + +/** + * Properties for {@link UnpinAllDialog}. + */ +interface UnpinAllDialogProps { + /* + * The matrix client to use. + */ + matrixClient: MatrixClient; + /* + * The room ID to unpin all events in. + */ + roomId: string; + /* + * Callback for when the dialog is closed. + */ + onFinished: () => void; +} + +/** + * A dialog that asks the user to confirm unpinning all events in a room. + */ +export function UnpinAllDialog({ matrixClient, roomId, onFinished }: UnpinAllDialogProps): JSX.Element { + return ( + + {_t("right_panel|pinned_messages|unpin_all|content")} +

+ + +
+ + ); +} diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 1ab39acb5c..aa2a35dc85 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -17,10 +17,22 @@ limitations under the License. import { Toast } from "@vector-im/compound-web"; import React, { useState } from "react"; +import UserProfileIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-profile"; +import DevicesIcon from "@vector-im/compound-design-tokens/assets/web/icons/devices"; +import VisibilityOnIcon from "@vector-im/compound-design-tokens/assets/web/icons/visibility-on"; +import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications"; +import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences"; +import KeyboardIcon from "@vector-im/compound-design-tokens/assets/web/icons/keyboard"; +import SidebarIcon from "@vector-im/compound-design-tokens/assets/web/icons/sidebar"; +import MicOnIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on"; +import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock"; +import LabsIcon from "@vector-im/compound-design-tokens/assets/web/icons/labs"; +import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block"; +import HelpIcon from "@vector-im/compound-design-tokens/assets/web/icons/help"; import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView"; import { _t, _td } from "../../../languageHandler"; -import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; +import AccountUserSettingsTab from "../settings/tabs/user/AccountUserSettingsTab"; import SettingsStore from "../../../settings/SettingsStore"; import LabsUserSettingsTab, { showLabsFlags } from "../settings/tabs/user/LabsUserSettingsTab"; import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; @@ -53,8 +65,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode { strong: (sub: string) => {sub}, }; switch (tabId) { - case UserTab.General: - return _t("settings|general|dialog_title", undefined, subs); + case UserTab.Account: + return _t("settings|account|dialog_title", undefined, subs); case UserTab.SessionManager: return _t("settings|sessions|dialog_title", undefined, subs); case UserTab.Appearance: @@ -91,10 +103,10 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { tabs.push( new Tab( - UserTab.General, - _td("common|general"), - "mx_UserSettingsDialog_settingsIcon", - , + UserTab.Account, + _td("settings|account|title"), + , + , "UserSettingsGeneral", ), ); @@ -102,7 +114,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.SessionManager, _td("settings|sessions|title"), - "mx_UserSettingsDialog_sessionsIcon", + , , undefined, ), @@ -111,7 +123,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Appearance, _td("common|appearance"), - "mx_UserSettingsDialog_appearanceIcon", + , , "UserSettingsAppearance", ), @@ -120,7 +132,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Notifications, _td("notifications|enable_prompt_toast_title"), - "mx_UserSettingsDialog_bellIcon", + , , "UserSettingsNotifications", ), @@ -129,7 +141,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Preferences, _td("common|preferences"), - "mx_UserSettingsDialog_preferencesIcon", + , , "UserSettingsPreferences", ), @@ -138,7 +150,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Keyboard, _td("settings|keyboard|title"), - "mx_UserSettingsDialog_keyboardIcon", + , , "UserSettingsKeyboard", ), @@ -147,7 +159,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Sidebar, _td("settings|sidebar|title"), - "mx_UserSettingsDialog_sidebarIcon", + , , "UserSettingsSidebar", ), @@ -158,7 +170,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Voice, _td("settings|voip|title"), - "mx_UserSettingsDialog_voiceIcon", + , , "UserSettingsVoiceVideo", ), @@ -169,7 +181,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Security, _td("room_settings|security|title"), - "mx_UserSettingsDialog_securityIcon", + , , "UserSettingsSecurityPrivacy", ), @@ -177,13 +189,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) { tabs.push( - new Tab( - UserTab.Labs, - _td("common|labs"), - "mx_UserSettingsDialog_labsIcon", - , - "UserSettingsLabs", - ), + new Tab(UserTab.Labs, _td("common|labs"), , , "UserSettingsLabs"), ); } if (mjolnirEnabled) { @@ -191,7 +197,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Mjolnir, _td("labs_mjolnir|title"), - "mx_UserSettingsDialog_mjolnirIcon", + , , "UserSettingMjolnir", ), @@ -201,7 +207,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { new Tab( UserTab.Help, _td("setting|help_about|title"), - "mx_UserSettingsDialog_helpIcon", + , , "UserSettingsHelpAbout", ), @@ -210,7 +216,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { return tabs as NonEmptyArray>; }; - const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.General, props.initialTabId); + const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.Account, props.initialTabId); const setActiveTabId = (tabId: UserTab): void => { _setActiveTabId(tabId); // Clear this so switching away from the tab and back to it will not show the QR code again diff --git a/src/components/views/dialogs/UserTab.ts b/src/components/views/dialogs/UserTab.ts index ab6a213211..fa0e23242e 100644 --- a/src/components/views/dialogs/UserTab.ts +++ b/src/components/views/dialogs/UserTab.ts @@ -15,7 +15,7 @@ limitations under the License. */ export enum UserTab { - General = "USER_GENERAL_TAB", + Account = "USER_ACCOUNT_TAB", Appearance = "USER_APPEARANCE_TAB", Notifications = "USER_NOTIFICATIONS_TAB", Preferences = "USER_PREFERENCES_TAB", diff --git a/src/components/views/dialogs/spotlight/Option.tsx b/src/components/views/dialogs/spotlight/Option.tsx index c7d504aa0b..d15b781fcf 100644 --- a/src/components/views/dialogs/spotlight/Option.tsx +++ b/src/components/views/dialogs/spotlight/Option.tsx @@ -26,6 +26,7 @@ interface OptionProps { id?: string; className?: string; onClick: ((ev: ButtonEvent) => void) | null; + children?: ReactNode; } export const Option: React.FC = ({ inputRef, children, endAdornment, className, ...props }) => { diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 902a8fa170..2fe11e5642 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -26,7 +26,11 @@ import SdkConfig from "../../../SdkConfig"; import { SettingLevel } from "../../../settings/SettingLevel"; import SettingsStore from "../../../settings/SettingsStore"; import { Protocols } from "../../../utils/DirectoryUtils"; -import { GenericDropdownMenu, GenericDropdownMenuItem } from "../../structures/GenericDropdownMenu"; +import { + AdditionalOptionsProps, + GenericDropdownMenu, + GenericDropdownMenuItem, +} from "../../structures/GenericDropdownMenu"; import TextInputDialog from "../dialogs/TextInputDialog"; import AccessibleButton from "../elements/AccessibleButton"; import withValidation from "../elements/Validation"; @@ -183,7 +187,7 @@ export const NetworkDropdown: React.FC = ({ protocols, config, setConfig })); const addNewServer = useCallback( - ({ closeMenu }) => ( + ({ closeMenu }: AdditionalOptionsProps) => ( <> { public static contextType = MatrixClientContext; - public context!: ContextType; + public declare context: ContextType; public static defaultProps: Partial = { waitForIframeLoad: true, @@ -143,8 +143,7 @@ export default class AppTile extends React.Component { private unmounted = false; public constructor(props: IProps, context: ContextType) { - super(props); - this.context = context; // XXX: workaround for lack of `declare` support on `public context!:` definition + super(props, context); // Tiles in miniMode are floating, and therefore not docked if (!this.props.miniMode) { diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index a1270427cc..4f5817e4c7 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -81,7 +81,7 @@ export default class EventListSummary extends React.Component< IProps & Required> > { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; public static defaultProps = { summaryLength: 1, diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 812d343249..0e876d9a37 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ContextType, CSSProperties, MutableRefObject } from "react"; +import React, { ContextType, CSSProperties, MutableRefObject, ReactNode } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; import WidgetUtils from "../../../utils/WidgetUtils"; @@ -28,11 +28,12 @@ interface IProps { persistentRoomId: string; pointerEvents?: CSSProperties["pointerEvents"]; movePersistedElement: MutableRefObject<(() => void) | undefined>; + children?: ReactNode; } export default class PersistentApp extends React.Component { public static contextType = MatrixClientContext; - public context!: ContextType; + public declare context: ContextType; private room: Room; public constructor(props: IProps, context: ContextType) { diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index b7e833a629..621a360bc5 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -73,7 +73,7 @@ interface IState { // be low as each event being loaded (after the first) is triggered by an explicit user action. export default class ReplyChain extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private unmounted = false; private room: Room; diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index d5353dcabc..1ec057fff1 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -41,7 +41,7 @@ interface IState { // Controlled form component wrapping Field for inputting a room alias scoped to a given domain export default class RoomAliasField extends React.PureComponent { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private fieldRef = createRef(); diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx index c0647e504f..be795ee50a 100644 --- a/src/components/views/elements/SSOButtons.tsx +++ b/src/components/views/elements/SSOButtons.tsx @@ -151,11 +151,20 @@ interface IProps { fragmentAfterLogin?: string; primary?: boolean; action?: SSOAction; + disabled?: boolean; } const MAX_PER_ROW = 6; -const SSOButtons: React.FC = ({ matrixClient, flow, loginType, fragmentAfterLogin, primary, action }) => { +const SSOButtons: React.FC = ({ + matrixClient, + flow, + loginType, + fragmentAfterLogin, + primary, + action, + disabled, +}) => { const providers = flow.identity_providers || []; if (providers.length < 2) { return ( @@ -168,6 +177,7 @@ const SSOButtons: React.FC = ({ matrixClient, flow, loginType, fragmentA primary={primary} action={action} flow={flow} + disabled={disabled} />
); diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx index c6f505e804..fd489bd67b 100644 --- a/src/components/views/elements/ServerPicker.tsx +++ b/src/components/views/elements/ServerPicker.tsx @@ -29,6 +29,7 @@ interface IProps { title?: string; dialogTitle?: string; serverConfig: ValidatedServerConfig; + disabled?: boolean; onServerConfigChange?(config: ValidatedServerConfig): void; } @@ -55,7 +56,7 @@ const onHelpClick = (): void => { ); }; -const ServerPicker: React.FC = ({ title, dialogTitle, serverConfig, onServerConfigChange }) => { +const ServerPicker: React.FC = ({ title, dialogTitle, serverConfig, onServerConfigChange, disabled }) => { const disableCustomUrls = SdkConfig.get("disable_custom_urls"); let editBtn; @@ -68,7 +69,7 @@ const ServerPicker: React.FC = ({ title, dialogTitle, serverConfig, onSe }); }; editBtn = ( - + {_t("action|edit")} ); diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index 075a6e6cee..8265331a3c 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -37,7 +37,7 @@ interface IState { class ReactionPicker extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index 33549b7489..d7077f0591 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -31,7 +31,7 @@ interface IProps { class Search extends React.PureComponent { public static contextType = RovingTabIndexContext; - public context!: React.ContextType; + public declare context: React.ContextType; private inputRef = React.createRef(); diff --git a/src/components/views/location/LocationPicker.tsx b/src/components/views/location/LocationPicker.tsx index 2ddd19dfe3..d4739935c7 100644 --- a/src/components/views/location/LocationPicker.tsx +++ b/src/components/views/location/LocationPicker.tsx @@ -50,13 +50,13 @@ const isSharingOwnLocation = (shareType: LocationShareType): boolean => class LocationPicker extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private map?: maplibregl.Map; private geolocate?: maplibregl.GeolocateControl; private marker?: maplibregl.Marker; - public constructor(props: ILocationPickerProps) { - super(props); + public constructor(props: ILocationPickerProps, context: React.ContextType) { + super(props, context); this.state = { position: undefined, diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 457a79b8db..5bb6b17881 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -24,6 +24,8 @@ import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex"; import Spinner from "../elements/Spinner"; import { _t, _td, TranslationKey } from "../../../languageHandler"; import { FileDownloader } from "../../../utils/FileDownloader"; +import Modal from "../../../Modal"; +import ErrorDialog from "../dialogs/ErrorDialog"; interface IProps { mxEvent: MatrixEvent; @@ -53,6 +55,23 @@ export default class DownloadActionButton extends React.PureComponent => { + try { + await this.doDownload(); + } catch (e) { + Modal.createDialog(ErrorDialog, { + title: _t("timeline|download_failed"), + description: ( + <> +
{_t("timeline|download_failed_description")}
+
{e instanceof Error ? e.toString() : ""}
+ + ), + }); + this.setState({ loading: false }); + } + }; + + private async doDownload(): Promise { const mediaEventHelper = this.props.mediaEventHelperGet(); if (this.state.loading || !mediaEventHelper) return; @@ -64,15 +83,15 @@ export default class DownloadActionButton extends React.PureComponent { + private async downloadBlob(blob: Blob): Promise { await this.downloader.download({ blob, name: this.props.mediaEventHelperGet()!.fileName, diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index 49e0f1f7de..de2ba8a1b4 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -52,15 +52,14 @@ interface IState { export default class EditHistoryMessage extends React.PureComponent { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private content = createRef(); private pills: Element[] = []; private tooltips: Element[] = []; public constructor(props: IProps, context: React.ContextType) { - super(props); - this.context = context; + super(props, context); const cli = this.context; const userId = cli.getSafeUserId(); @@ -172,9 +171,8 @@ export default class EditHistoryMessage extends React.PureComponent { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; - public constructor(props: IBodyProps) { - super(props); - - this.state = {}; - } + public state: IState = {}; public async componentDidMount(): Promise { let buffer: ArrayBuffer; diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index bf4922615f..d7542b9740 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -106,7 +106,9 @@ interface IState { export default class MFileBody extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; + + public state: IState = {}; public static defaultProps = { showGenericPlaceholder: true, @@ -117,12 +119,6 @@ export default class MFileBody extends React.Component { private userDidClick = false; private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current); - public constructor(props: IProps) { - super(props); - - this.state = {}; - } - private getContentUrl(): string | null { if (this.props.forExport) return null; const media = mediaFromContent(this.props.mxEvent.getContent()); diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 54c7c5f40b..8c3c3431aa 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -20,7 +20,7 @@ import { Blurhash } from "react-blurhash"; import classNames from "classnames"; import { CSSTransition, SwitchTransition } from "react-transition-group"; import { logger } from "matrix-js-sdk/src/logger"; -import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; import { ImageContent } from "matrix-js-sdk/src/types"; import { Tooltip } from "@vector-im/compound-web"; @@ -65,29 +65,22 @@ interface IState { export default class MImageBody extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private unmounted = true; private image = createRef(); private timeout?: number; private sizeWatcher?: string; - private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; - - public constructor(props: IBodyProps) { - super(props); - - this.reconnectedListener = createReconnectedListener(this.clearError); - - this.state = { - contentUrl: null, - thumbUrl: null, - imgError: false, - imgLoaded: false, - hover: false, - showImage: SettingsStore.getValue("showImages"), - placeholder: Placeholder.NoImage, - }; - } + + public state: IState = { + contentUrl: null, + thumbUrl: null, + imgError: false, + imgLoaded: false, + hover: false, + showImage: SettingsStore.getValue("showImages"), + placeholder: Placeholder.NoImage, + }; protected showImage(): void { localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true"); @@ -160,10 +153,10 @@ export default class MImageBody extends React.Component { imgElement.src = url; }; - private clearError = (): void => { + private reconnectedListener = createReconnectedListener((): void => { MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener); this.setState({ imgError: false }); - }; + }); private onImageError = (): void => { // If the thumbnail failed to load then try again using the contentUrl diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index eedf5a6046..f3f1a3d9d3 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -38,14 +38,14 @@ interface IState { export default class MLocationBody extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private unmounted = false; private mapId: string; private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; - public constructor(props: IBodyProps) { - super(props); + public constructor(props: IBodyProps, context: React.ContextType) { + super(props, context); // multiple instances of same map might be in document // eg thread and main timeline, reply diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index d777ed9d77..5fad5acd5f 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -147,11 +147,11 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge export default class MPollBody extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private seenEventIds: string[] = []; // Events we have already seen - public constructor(props: IBodyProps) { - super(props); + public constructor(props: IBodyProps, context: React.ContextType) { + super(props, context); this.state = { selected: null, diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 84aa08162a..faa5dbd4f7 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -42,24 +42,20 @@ interface IState { export default class MVideoBody extends React.PureComponent { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private videoRef = React.createRef(); private sizeWatcher?: string; - public constructor(props: IBodyProps) { - super(props); - - this.state = { - fetchingData: false, - decryptedUrl: null, - decryptedThumbnailUrl: null, - decryptedBlob: null, - error: null, - posterLoading: false, - blurhashUrl: null, - }; - } + public state = { + fetchingData: false, + decryptedUrl: null, + decryptedThumbnailUrl: null, + decryptedBlob: null, + error: null, + posterLoading: false, + blurhashUrl: null, + }; private getContentUrl(): string | undefined { const content = this.props.mxEvent.getContent(); diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 2c314d284e..2a4c01f5e6 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -262,6 +262,7 @@ interface IMessageActionBarProps { export default class MessageActionBar extends React.PureComponent { public static contextType = RoomContext; + public declare context: React.ContextType; public componentDidMount(): void { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index db0016de7b..fb700579be 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -90,7 +90,7 @@ export default class MessageEvent extends React.Component implements IMe private evTypes = new Map>(baseEvTypes.entries()); public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index e57326edd7..c5d004b12b 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -83,11 +83,10 @@ interface IState { export default class ReactionsRow extends React.PureComponent { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); - this.context = context; this.state = { myReactions: this.getMyReactions(), diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 1dbd1bd7bf..7c4dcc8b3c 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -46,7 +46,7 @@ export interface IProps { export default class ReactionsRowButton extends React.PureComponent { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public onClick = (): void => { const { mxEvent, myReactionEvent, content } = this.props; diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx index 5b4db10ed6..f9a5c2d66f 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx +++ b/src/components/views/messages/ReactionsRowButtonTooltip.tsx @@ -36,7 +36,7 @@ interface IProps { export default class ReactionsRowButtonTooltip extends React.PureComponent> { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public render(): React.ReactNode { const { content, reactionEvents, mxEvent, children } = this.props; diff --git a/src/components/views/messages/RoomPredecessorTile.tsx b/src/components/views/messages/RoomPredecessorTile.tsx index 3166373fe0..36679d906d 100644 --- a/src/components/views/messages/RoomPredecessorTile.tsx +++ b/src/components/views/messages/RoomPredecessorTile.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { useCallback, useContext } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; @@ -52,7 +52,7 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => const predecessor = useRoomState( roomContext.room, useCallback( - (state) => state.findPredecessor(msc3946ProcessDynamicPredecessor), + (state: RoomState) => state.findPredecessor(msc3946ProcessDynamicPredecessor), [msc3946ProcessDynamicPredecessor], ), ); @@ -63,9 +63,9 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => dis.dispatch({ action: Action.ViewRoom, - event_id: predecessor.eventId, + event_id: predecessor?.eventId, highlighted: true, - room_id: predecessor.roomId, + room_id: predecessor?.roomId, metricsTrigger: "Predecessor", metricsViaKeyboard: e.type !== "click", }); @@ -126,7 +126,7 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => const predecessorPermalink = prevRoom ? createLinkWithRoom(prevRoom, predecessor.roomId, predecessor.eventId) - : createLinkWithoutRoom(predecessor.roomId, predecessor.viaServers, predecessor.eventId); + : createLinkWithoutRoom(predecessor.roomId, predecessor?.viaServers ?? [], predecessor.eventId); const link = ( diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 7246f7db57..5dba3e42b7 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -59,23 +59,19 @@ interface IState { } export default class TextualBody extends React.Component { - private readonly contentRef = createRef(); + private readonly contentRef = createRef(); private unmounted = false; private pills: Element[] = []; private tooltips: Element[] = []; public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; - public constructor(props: IBodyProps) { - super(props); - - this.state = { - links: [], - widgetHidden: false, - }; - } + public state = { + links: [], + widgetHidden: false, + }; public componentDidMount(): void { if (!this.props.editState) { @@ -566,35 +562,38 @@ export default class TextualBody extends React.Component { } const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); - let isNotice = false; - let isEmote = false; + const isNotice = content.msgtype === MsgType.Notice; + const isEmote = content.msgtype === MsgType.Emote; + + const willHaveWrapper = + this.props.replacingEventId || this.props.isSeeingThroughMessageHiddenForModeration || isEmote; // only strip reply if this is the original replying event, edits thereafter do not have the fallback const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent); - isEmote = content.msgtype === MsgType.Emote; - isNotice = content.msgtype === MsgType.Notice; - let body = HtmlUtils.bodyToHtml(content, this.props.highlights, { + + const htmlOpts = { disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"), // Part of Replies fallback support stripReplyFallback: stripReply, - ref: this.contentRef, - returnString: false, - }); + }; + let body = willHaveWrapper + ? HtmlUtils.bodyToSpan(content, this.props.highlights, htmlOpts, this.contentRef, false) + : HtmlUtils.bodyToDiv(content, this.props.highlights, htmlOpts, this.contentRef); if (this.props.replacingEventId) { body = ( - <> +
{body} {this.renderEditedMarker()} - +
); } if (this.props.isSeeingThroughMessageHiddenForModeration) { body = ( - <> +
{body} {this.renderPendingModerationMarker()} - +
); } @@ -625,7 +624,7 @@ export default class TextualBody extends React.Component { if (isEmote) { return ( -
+
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index ae94fd31f9..35351ce531 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -27,6 +27,7 @@ interface IProps { export default class TextualEvent extends React.Component { public static contextType = RoomContext; + public declare context: React.ContextType; public render(): React.ReactNode { const text = TextForEvent.textForEvent( diff --git a/src/components/views/messages/TimelineSeparator.tsx b/src/components/views/messages/TimelineSeparator.tsx index 78e0d1fd65..b3a2b9ccfb 100644 --- a/src/components/views/messages/TimelineSeparator.tsx +++ b/src/components/views/messages/TimelineSeparator.tsx @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; interface Props { label: string; + children?: ReactNode; } export const enum SeparatorKind { diff --git a/src/components/views/pips/WidgetPip.tsx b/src/components/views/pips/WidgetPip.tsx index 9bba2ccc53..a1710d16cc 100644 --- a/src/components/views/pips/WidgetPip.tsx +++ b/src/components/views/pips/WidgetPip.tsx @@ -34,6 +34,7 @@ import { WidgetType } from "../../../widgets/WidgetType"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import WidgetUtils from "../../../utils/WidgetUtils"; import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions"; +import { ButtonEvent } from "../elements/AccessibleButton"; interface Props { widgetId: string; @@ -62,7 +63,7 @@ export const WidgetPip: FC = ({ widgetId, room, viewingRoom, onStartMovin const call = useCallForWidget(widgetId, room.roomId); const onBackClick = useCallback( - (ev) => { + (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -87,7 +88,7 @@ export const WidgetPip: FC = ({ widgetId, room, viewingRoom, onStartMovin ); const onLeaveClick = useCallback( - (ev) => { + (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index aadabeb646..8443da220e 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -41,26 +41,11 @@ interface IProps { onKeyDown?(ev: KeyboardEvent): void; cardState?: any; ref?: Ref; - // Ref for the 'close' button the the card + // Ref for the 'close' button the card closeButtonRef?: Ref; children: ReactNode; } -interface IGroupProps { - className?: string; - title: string; - children: ReactNode; -} - -export const Group: React.FC = ({ className, title, children }) => { - return ( -
-

{title}

- {children} -
- ); -}; - const BaseCard: React.FC = forwardRef( ( { @@ -143,7 +128,7 @@ const BaseCard: React.FC = forwardRef( {header} ) : ( - header ??
+ (header ??
) )} {closeButton}
diff --git a/src/components/views/right_panel/ExtensionsCard.tsx b/src/components/views/right_panel/ExtensionsCard.tsx new file mode 100644 index 0000000000..22ea8bad99 --- /dev/null +++ b/src/components/views/right_panel/ExtensionsCard.tsx @@ -0,0 +1,214 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useEffect, useMemo, useState } from "react"; +import { Room } from "matrix-js-sdk/src/matrix"; +import classNames from "classnames"; +import { Button, Link, Separator, Text } from "@vector-im/compound-web"; +import { Icon as PlusIcon } from "@vector-im/compound-design-tokens/icons/plus.svg"; +import { Icon as ExtensionsIcon } from "@vector-im/compound-design-tokens/icons/extensions.svg"; + +import BaseCard from "./BaseCard"; +import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils"; +import { _t } from "../../../languageHandler"; +import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; +import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; +import UIStore from "../../../stores/UIStore"; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; +import { IApp } from "../../../stores/WidgetStore"; +import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; +import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; +import AccessibleButton from "../elements/AccessibleButton"; +import WidgetAvatar from "../avatars/WidgetAvatar"; +import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; +import EmptyState from "./EmptyState"; + +interface Props { + room: Room; + onClose(): void; +} + +interface IAppRowProps { + app: IApp; + room: Room; +} + +const AppRow: React.FC = ({ app, room }) => { + const name = WidgetUtils.getWidgetName(app); + const [canModifyWidget, setCanModifyWidget] = useState(); + + useEffect(() => { + setCanModifyWidget(WidgetUtils.canUserModifyWidgets(room.client, room.roomId)); + }, [room.client, room.roomId]); + + const onOpenWidgetClick = (): void => { + RightPanelStore.instance.pushCard({ + phase: RightPanelPhases.Widget, + state: { widgetId: app.id }, + }); + }; + + const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top); + const togglePin = isPinned + ? () => { + WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); + } + : () => { + WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); + }; + + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + let contextMenu; + if (menuDisplayed) { + const rect = handle.current?.getBoundingClientRect(); + const rightMargin = rect?.right ?? 0; + const topMargin = rect?.top ?? 0; + contextMenu = ( + + ); + } + + const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top); + + let pinTitle: string; + if (cannotPin) { + pinTitle = _t("right_panel|pinned_messages|limits", { count: MAX_PINNED }); + } else { + pinTitle = isPinned ? _t("action|unpin") : _t("action|pin"); + } + + const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center); + + let openTitle = ""; + if (isPinned) { + openTitle = _t("widget|unpin_to_view_right_panel"); + } else if (isMaximised) { + openTitle = _t("widget|close_to_view_right_panel"); + } + + const classes = classNames("mx_BaseCard_Button mx_ExtensionsCard_Button", { + mx_ExtensionsCard_Button_pinned: isPinned, + }); + + return ( +
+ + + + {name} + + + + {canModifyWidget && ( + + )} + + + + {contextMenu} +
+ ); +}; + +/** + * A right panel card displaying a list of widgets in the room and allowing the user to manage them. + * @param room the room to manage widgets for + * @param onClose callback when the card is closed + */ +const ExtensionsCard: React.FC = ({ room, onClose }) => { + const apps = useWidgets(room); + // Filter out virtual widgets + const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]); + + const onManageIntegrations = (): void => { + const managers = IntegrationManagers.sharedInstance(); + if (!managers.hasManager()) { + managers.openNoManagerDialog(); + } else { + // noinspection JSIgnoredPromiseFromCall + managers.getPrimaryManager()?.open(room); + } + }; + + // The button is in the header to keep it outside the scrollable region + const header = ( + + ); + + let body: JSX.Element; + if (realApps.length < 1) { + body = ( + + ); + } else { + let copyLayoutBtn: JSX.Element | null = null; + if (WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) { + copyLayoutBtn = ( + WidgetLayoutStore.instance.copyLayoutToRoom(room)}> + {_t("widget|set_room_layout")} + + ); + } + + body = ( + <> + + {realApps.map((app) => ( + + ))} + {copyLayoutBtn} + + ); + } + + return ( + + {body} + + ); +}; + +export default ExtensionsCard; diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index f813dd3427..85be2e6d03 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -14,41 +14,62 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useContext, useEffect, useState } from "react"; -import { Room, RoomEvent, RoomStateEvent, MatrixEvent, EventType, RelationType } from "matrix-js-sdk/src/matrix"; +import React, { useCallback, useEffect, useState, JSX } from "react"; +import { + Room, + RoomEvent, + RoomStateEvent, + MatrixEvent, + EventType, + RelationType, + EventTimeline, +} from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { Button, Separator } from "@vector-im/compound-web"; +import classNames from "classnames"; +import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin"; -import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg"; -import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg"; -import { Icon as ReplyIcon } from "../../../../res/img/element-icons/room/message-bar/reply.svg"; import { _t } from "../../../languageHandler"; import BaseCard from "./BaseCard"; import Spinner from "../elements/Spinner"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import PinningUtils from "../../../utils/PinningUtils"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; -import PinnedEventTile from "../rooms/PinnedEventTile"; +import { PinnedEventTile } from "../rooms/PinnedEventTile"; import { useRoomState } from "../../../hooks/useRoomState"; -import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType, useRoomContext } from "../../../contexts/RoomContext"; import { ReadPinsEventId } from "./types"; import Heading from "../typography/Heading"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { filterBoolean } from "../../../utils/arrays"; +import Modal from "../../../Modal"; +import { UnpinAllDialog } from "../dialogs/UnpinAllDialog"; +import EmptyState from "./EmptyState"; -interface IProps { - room: Room; - permalinkCreator: RoomPermalinkCreator; - onClose(): void; -} - +/** + * Get the pinned event IDs from a room. + * @param room + */ function getPinnedEventIds(room?: Room): string[] { - return room?.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned ?? []; + return ( + room + ?.getLiveTimeline() + .getState(EventTimeline.FORWARDS) + ?.getStateEvents(EventType.RoomPinnedEvents, "") + ?.getContent()?.pinned ?? [] + ); } +/** + * Get the pinned event IDs from a room. + * @param room + */ export const usePinnedEvents = (room?: Room): string[] => { const [pinnedEvents, setPinnedEvents] = useState(getPinnedEventIds(room)); + // Update the pinned events when the room state changes + // Filter out events that are not pinned events const update = useCallback( (ev?: MatrixEvent) => { if (ev && ev.getType() !== EventType.RoomPinnedEvents) return; @@ -57,7 +78,7 @@ export const usePinnedEvents = (room?: Room): string[] => { [room], ); - useTypedEventEmitter(room?.currentState, RoomStateEvent.Events, update); + useTypedEventEmitter(room?.getLiveTimeline().getState(EventTimeline.FORWARDS), RoomStateEvent.Events, update); useEffect(() => { setPinnedEvents(getPinnedEventIds(room)); return () => { @@ -67,13 +88,23 @@ export const usePinnedEvents = (room?: Room): string[] => { return pinnedEvents; }; +/** + * Get the read pinned event IDs from a room. + * @param room + */ function getReadPinnedEventIds(room?: Room): Set { return new Set(room?.getAccountData(ReadPinsEventId)?.getContent()?.event_ids ?? []); } +/** + * Get the read pinned event IDs from a room. + * @param room + */ export const useReadPinnedEvents = (room?: Room): Set => { const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); + // Update the read pinned events when the room state changes + // Filter out events that are not read pinned events const update = useCallback( (ev?: MatrixEvent) => { if (ev && ev.getType() !== ReadPinsEventId) return; @@ -92,36 +123,36 @@ export const useReadPinnedEvents = (room?: Room): Set => { return readPinnedEvents; }; -const PinnedMessagesCard: React.FC = ({ room, onClose, permalinkCreator }) => { - const cli = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); - const canUnpin = useRoomState(room, (state) => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli)); - const pinnedEventIds = usePinnedEvents(room); - const readPinnedEvents = useReadPinnedEvents(room); +/** + * Fetch the pinned events + * @param room + * @param pinnedEventIds + */ +function useFetchedPinnedEvents(room: Room, pinnedEventIds: string[]): Array | null { + const cli = useMatrixClientContext(); - useEffect(() => { - if (!cli || cli.isGuest()) return; // nothing to do - const newlyRead = pinnedEventIds.filter((id) => !readPinnedEvents.has(id)); - if (newlyRead.length > 0) { - // clear out any read pinned events which no longer are pinned - cli.setRoomAccountData(room.roomId, ReadPinsEventId, { - event_ids: pinnedEventIds, - }); - } - }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); - - const pinnedEvents = useAsyncMemo( + return useAsyncMemo( () => { const promises = pinnedEventIds.map(async (eventId): Promise => { const timelineSet = room.getUnfilteredTimelineSet(); + // Get the event from the local timeline const localEvent = timelineSet ?.getTimelineForEvent(eventId) ?.getEvents() .find((e) => e.getId() === eventId); + + // Decrypt the event if it's encrypted + // Can happen when the tab is refreshed and the pinned events card is opened directly + if (localEvent?.isEncrypted()) { + await cli.decryptEventIfNeeded(localEvent); + } + + // If the event is available locally, return it if it's pinnable + // Otherwise, return null if (localEvent) return PinningUtils.isPinnable(localEvent) ? localEvent : null; try { - // Fetch the event and latest edit in parallel + // The event is not available locally, so we fetch the event and latest edit in parallel const [ evJson, { @@ -131,10 +162,15 @@ const PinnedMessagesCard: React.FC = ({ room, onClose, permalinkCreator cli.fetchRoomEvent(room.roomId, eventId), cli.relations(room.roomId, eventId, RelationType.Replace, null, { limit: 1 }), ]); + const event = new MatrixEvent(evJson); + + // Decrypt the event if it's encrypted if (event.isEncrypted()) { - await cli.decryptEventIfNeeded(event); // TODO await? + await cli.decryptEventIfNeeded(event); } + + // Handle poll events await room.processPollEvents([event]); const senderUserId = event.getSender(); @@ -158,62 +194,59 @@ const PinnedMessagesCard: React.FC = ({ room, onClose, permalinkCreator [cli, room, pinnedEventIds], null, ); +} + +/** + * List the pinned messages in a room inside a Card. + */ +interface PinnedMessagesCardProps { + /** + * The room to list the pinned messages for. + */ + room: Room; + /** + * Permalink of the room. + */ + permalinkCreator: RoomPermalinkCreator; + /** + * Callback for when the card is closed. + */ + onClose(): void; +} - let content: JSX.Element[] | JSX.Element | undefined; +export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMessagesCardProps): JSX.Element { + const cli = useMatrixClientContext(); + const roomContext = useRoomContext(); + const pinnedEventIds = usePinnedEvents(room); + const readPinnedEvents = useReadPinnedEvents(room); + const pinnedEvents = useFetchedPinnedEvents(room, pinnedEventIds); + + useEffect(() => { + if (!cli || cli.isGuest()) return; // nothing to do + const newlyRead = pinnedEventIds.filter((id) => !readPinnedEvents.has(id)); + if (newlyRead.length > 0) { + // clear out any read pinned events which no longer are pinned + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: pinnedEventIds, + }); + } + }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); + + let content: JSX.Element; if (!pinnedEventIds.length) { content = ( -
-
- {/* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */} -
-
- -
-
- -
-
- -
-
- - - {_t("right_panel|pinned_messages|empty")} - - {_t( - "right_panel|pinned_messages|explainer", - {}, - { - b: (sub) => {sub}, - }, - )} -
-
+ ); } else if (pinnedEvents?.length) { - const onUnpinClicked = async (event: MatrixEvent): Promise => { - const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ""); - if (pinnedEvents?.getContent()?.pinned) { - const pinned = pinnedEvents.getContent().pinned; - const index = pinned.indexOf(event.getId()); - if (index !== -1) { - pinned.splice(index, 1); - await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, ""); - } - } - }; - - // show them in reverse, with latest pinned at the top - content = filterBoolean(pinnedEvents) - .reverse() - .map((ev) => ( - onUnpinClicked(ev) : undefined} - permalinkCreator={permalinkCreator} - /> - )); + content = ( + + ); } else { content = ; } @@ -223,7 +256,7 @@ const PinnedMessagesCard: React.FC = ({ room, onClose, permalinkCreator header={
- {_t("right_panel|pinned_messages|title")} + {_t("right_panel|pinned_messages|header", { count: pinnedEventIds.length })}
} @@ -240,6 +273,79 @@ const PinnedMessagesCard: React.FC = ({ room, onClose, permalinkCreator ); -}; +} + +/** + * The pinned messages in a room. + */ +interface PinnedMessagesProps { + /** + * The pinned events. + */ + events: MatrixEvent[]; + /** + * The room the events are in. + */ + room: Room; + /** + * The permalink creator to use. + */ + permalinkCreator: RoomPermalinkCreator; +} + +/** + * The pinned messages in a room. + */ +function PinnedMessages({ events, room, permalinkCreator }: PinnedMessagesProps): JSX.Element { + const matrixClient = useMatrixClientContext(); -export default PinnedMessagesCard; + /** + * Whether the client can unpin events from the room. + */ + const canUnpin = useRoomState(room, (state) => + state.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient), + ); + + /** + * Opens the unpin all dialog. + */ + const onUnpinAll = useCallback(async (): Promise => { + Modal.createDialog(UnpinAllDialog, { + roomId: room.roomId, + matrixClient, + }); + }, [room, matrixClient]); + + return ( + <> +
+ {events.reverse().map((event, i) => ( + <> + + {/* Add a separator if this isn't the last pinned message */} + {events.length - 1 !== i && ( + + )} + + ))} +
+ {canUnpin && ( +
+ +
+ )} + + ); +} diff --git a/src/components/views/right_panel/RightPanelTabs.tsx b/src/components/views/right_panel/RightPanelTabs.tsx index fc2eeb17fa..300856e28f 100644 --- a/src/components/views/right_panel/RightPanelTabs.tsx +++ b/src/components/views/right_panel/RightPanelTabs.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { useRef } from "react"; import { NavBar, NavItem } from "@vector-im/compound-web"; +import { Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; @@ -24,17 +25,27 @@ import PosthogTrackers from "../../../PosthogTrackers"; import { useDispatcher } from "../../../hooks/useDispatcher"; import dispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; +import SettingsStore from "../../../settings/SettingsStore"; +import { UIComponent, UIFeature } from "../../../settings/UIFeature"; +import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; +import { useIsVideoRoom } from "../../../utils/video-rooms"; function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean { - const tabs = [RightPanelPhases.RoomSummary, RightPanelPhases.RoomMemberList, RightPanelPhases.ThreadPanel]; + const tabs = [ + RightPanelPhases.RoomSummary, + RightPanelPhases.RoomMemberList, + RightPanelPhases.ThreadPanel, + RightPanelPhases.Extensions, + ]; return !!phase && tabs.includes(phase); } type Props = { + room?: Room; phase: RightPanelPhases; }; -export const RightPanelTabs: React.FC = ({ phase }): JSX.Element | null => { +export const RightPanelTabs: React.FC = ({ phase, room }): JSX.Element | null => { const threadsTabRef = useRef(null); useDispatcher(dispatcher, (payload) => { @@ -45,6 +56,8 @@ export const RightPanelTabs: React.FC = ({ phase }): JSX.Element | null = } }); + const isVideoRoom = useIsVideoRoom(room); + if (!shouldShowTabsForPhase(phase)) return null; return ( @@ -81,6 +94,20 @@ export const RightPanelTabs: React.FC = ({ phase }): JSX.Element | null = > {_t("common|threads")} + {SettingsStore.getValue(UIFeature.Widgets) && + !isVideoRoom && + shouldShowComponent(UIComponent.AddIntegrations) && ( + { + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true); + }} + active={phase === RightPanelPhases.Extensions} + > + {_t("common|extensions")} + + )} ); }; diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 86c163b910..c13899f778 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -14,16 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { - ChangeEvent, - SyntheticEvent, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { ChangeEvent, SyntheticEvent, useContext, useEffect, useRef, useState } from "react"; import classNames from "classnames"; import { MenuItem, @@ -55,35 +46,23 @@ import { EventType, JoinRule, Room, RoomStateEvent } from "matrix-js-sdk/src/mat import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; -import BaseCard, { Group } from "./BaseCard"; +import BaseCard from "./BaseCard"; import { _t } from "../../../languageHandler"; import RoomAvatar from "../avatars/RoomAvatar"; -import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import Modal from "../../../Modal"; import ShareDialog from "../dialogs/ShareDialog"; -import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter"; -import WidgetUtils from "../../../utils/WidgetUtils"; -import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; -import SettingsStore from "../../../settings/SettingsStore"; -import WidgetAvatar from "../avatars/WidgetAvatar"; -import WidgetStore, { IApp } from "../../../stores/WidgetStore"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; -import { UIComponent, UIFeature } from "../../../settings/UIFeature"; -import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; -import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; import { useFeatureEnabled } from "../../../hooks/useSettings"; import { usePinnedEvents } from "./PinnedMessagesCard"; -import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import RoomName from "../elements/RoomName"; -import UIStore from "../../../stores/UIStore"; import ExportDialog from "../dialogs/ExportDialog"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import PosthogTrackers from "../../../PosthogTrackers"; -import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { PollHistoryDialog } from "../dialogs/PollHistoryDialog"; import { Flex } from "../../utils/Flex"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; @@ -101,6 +80,7 @@ import { useDispatcher } from "../../../hooks/useDispatcher"; import { Action } from "../../../dispatcher/actions"; import { Key } from "../../../Keyboard"; import { useTransition } from "../../../hooks/useTransition"; +import { useIsVideoRoom } from "../../../utils/video-rooms"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; // :TCHAP: tchap-room-icons @@ -113,182 +93,6 @@ interface IProps { focusRoomSearch?: boolean; } -interface IAppsSectionProps { - room: Room; -} - -export const useWidgets = (room: Room): IApp[] => { - const [apps, setApps] = useState(() => WidgetStore.instance.getApps(room.roomId)); - - const updateApps = useCallback(() => { - // Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings - setApps([...WidgetStore.instance.getApps(room.roomId)]); - }, [room]); - - useEffect(updateApps, [room, updateApps]); - useEventEmitter(WidgetStore.instance, room.roomId, updateApps); - useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps); - - return apps; -}; - -interface IAppRowProps { - app: IApp; - room: Room; -} - -const AppRow: React.FC = ({ app, room }) => { - const name = WidgetUtils.getWidgetName(app); - const dataTitle = WidgetUtils.getWidgetDataTitle(app); - const subtitle = dataTitle && " - " + dataTitle; - const [canModifyWidget, setCanModifyWidget] = useState(); - - useEffect(() => { - setCanModifyWidget(WidgetUtils.canUserModifyWidgets(room.client, room.roomId)); - }, [room.client, room.roomId]); - - const onOpenWidgetClick = (): void => { - RightPanelStore.instance.pushCard({ - phase: RightPanelPhases.Widget, - state: { widgetId: app.id }, - }); - }; - - const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top); - const togglePin = isPinned - ? () => { - WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); - } - : () => { - WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); - }; - - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); - let contextMenu; - if (menuDisplayed) { - const rect = handle.current?.getBoundingClientRect(); - const rightMargin = rect?.right ?? 0; - const topMargin = rect?.top ?? 0; - contextMenu = ( - - ); - } - - const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top); - - let pinTitle: string; - if (cannotPin) { - pinTitle = _t("right_panel|pinned_messages|limits", { count: MAX_PINNED }); - } else { - pinTitle = isPinned ? _t("action|unpin") : _t("action|pin"); - } - - const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center); - const toggleMaximised = isMaximised - ? () => { - WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); - } - : () => { - WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center); - }; - - const maximiseTitle = isMaximised ? _t("action|close") : _t("action|maximise"); - - let openTitle = ""; - if (isPinned) { - openTitle = _t("widget|unpin_to_view_right_panel"); - } else if (isMaximised) { - openTitle = _t("widget|close_to_view_right_panel"); - } - - const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", { - mx_RoomSummaryCard_Button_pinned: isPinned, - mx_RoomSummaryCard_Button_maximised: isMaximised, - }); - - return ( -
- - - {name} - {subtitle} - - - {canModifyWidget && ( - - )} - - - - - {contextMenu} -
- ); -}; - -const AppsSection: React.FC = ({ room }) => { - const apps = useWidgets(room); - // Filter out virtual widgets - const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]); - - const onManageIntegrations = (): void => { - const managers = IntegrationManagers.sharedInstance(); - if (!managers.hasManager()) { - managers.openNoManagerDialog(); - } else { - // noinspection JSIgnoredPromiseFromCall - managers.getPrimaryManager()?.open(room); - } - }; - - let copyLayoutBtn: JSX.Element | null = null; - if (realApps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) { - copyLayoutBtn = ( - WidgetLayoutStore.instance.copyLayoutToRoom(room)}> - {_t("widget|set_room_layout")} - - ); - } - - return ( - - {realApps.map((app) => ( - - ))} - {copyLayoutBtn} - - {realApps.length > 0 ? _t("right_panel|edit_integrations") : _t("right_panel|add_integrations")} - - - ); -}; - const onRoomFilesClick = (): void => { RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true); }; @@ -303,17 +107,24 @@ const onRoomSettingsClick = (ev: Event): void => { }; const RoomTopic: React.FC> = ({ room }): JSX.Element | null => { - const [expanded, setExpanded] = useState(false); + const [expanded, setExpanded] = useState(true); const topic = useTopic(room); const body = topicToHtml(topic?.text, topic?.html); + const canEditTopic = useRoomState(room, (state) => + state.maySendStateEvent(EventType.RoomTopic, room.client.getSafeUserId()), + ); const onEditClick = (e: SyntheticEvent): void => { e.preventDefault(); e.stopPropagation(); defaultDispatcher.dispatch({ action: "open_room_settings" }); }; + if (!body && !canEditTopic) { + return null; + } + if (!body) { return ( > = ({ room }): JSX.Element | null onRoomTopicLinkClick(ev); return; } - setExpanded(!expanded); }} > {content} @@ -367,7 +177,7 @@ const RoomTopic: React.FC> = ({ room }): JSX.Element | null - {expanded && ( + {expanded && canEditTopic && ( @@ -419,10 +229,7 @@ const RoomSummaryCard: React.FC = ({ const isRoomEncrypted = useIsEncrypted(cli, room); const roomContext = useContext(RoomContext); const e2eStatus = roomContext.e2eStatus; - const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms"); - const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); - const isVideoRoom = - videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())); + const isVideoRoom = useIsVideoRoom(room); const roomState = useRoomState(room); const directRoomsList = useAccountData>(room.client, EventType.Direct); @@ -586,18 +393,11 @@ const RoomSummaryCard: React.FC = ({ disabled={!canInviteToState} onSelect={() => inviteToRoom(room)} /> - - + {!isVideoRoom && ( <> - - {pinningEnabled && ( = ({ )} + + + )} + + + + + + {!isVideoRoom && ( + <> + = ({ )} + + = ({ onSelect={onLeaveRoomClick} />
- - {SettingsStore.getValue(UIFeature.Widgets) && - !isVideoRoom && - shouldShowComponent(UIComponent.AddIntegrations) && } ); }; diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index 7dfcd63307..97f5aabc32 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -77,6 +77,7 @@ interface IState { export default class TimelineCard extends React.Component { public static contextType = RoomContext; + public declare context: React.ContextType; private dispatcherRef?: string; private layoutWatcherRef?: string; @@ -84,8 +85,8 @@ export default class TimelineCard extends React.Component { private card = React.createRef(); private readReceiptsSettingWatcher: string | undefined; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { showReadReceipts: SettingsStore.getValue("showReadReceipts", props.room.roomId), layout: SettingsStore.getValue("layout"), diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 6f8fd9790b..9cc50fa928 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -424,6 +424,7 @@ export const UserOptionsSection: React.FC<{ member: Member; canInvite: boolean; isSpace?: boolean; + children?: ReactNode; }> = ({ member, canInvite, isSpace, children }) => { const cli = useContext(MatrixClientContext); @@ -441,19 +442,35 @@ export const UserOptionsSection: React.FC<{ // Only allow the user to ignore the user if its not ourselves // same goes for jumping to read receipt if (!isMe) { - if (member instanceof RoomMember && member.roomId && !isSpace) { - const onReadReceiptButton = function (): void { - const room = cli.getRoom(member.roomId); - dis.dispatch({ - action: Action.ViewRoom, - highlighted: true, - // this could return null, the default prevents a type error - event_id: room?.getEventReadUpTo(member.userId) || undefined, - room_id: member.roomId, - metricsTrigger: undefined, // room doesn't change - }); - }; + const onReadReceiptButton = function (room: Room): void { + dis.dispatch({ + action: Action.ViewRoom, + highlighted: true, + // this could return null, the default prevents a type error + event_id: room.getEventReadUpTo(member.userId) || undefined, + room_id: room.roomId, + metricsTrigger: undefined, // room doesn't change + }); + }; + const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : null; + const readReceiptButtonDisabled = isSpace || !room?.getEventReadUpTo(member.userId); + readReceiptButton = ( + { + ev.preventDefault(); + if (room && !readReceiptButtonDisabled) { + onReadReceiptButton(room); + } + }} + label={_t("user_info|jump_to_rr_button")} + disabled={readReceiptButtonDisabled} + Icon={CheckIcon} + /> + ); + + if (member instanceof RoomMember && member.roomId && !isSpace) { const onInsertPillButton = function (): void { dis.dispatch({ action: Action.ComposerInsert, @@ -462,21 +479,6 @@ export const UserOptionsSection: React.FC<{ }); }; - const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : undefined; - if (room?.getEventReadUpTo(member.userId)) { - readReceiptButton = ( - { - ev.preventDefault(); - onReadReceiptButton(); - }} - label={_t("user_info|jump_to_rr_button")} - Icon={CheckIcon} - /> - ); - } - insertPillButton = ( { + (ev: MatrixEvent) => { if (ev.getType() === "m.ignored_user_list") { setIsIgnored(cli.isUserIgnored(member.userId)); } diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index ca7cdebb21..210e673556 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -19,10 +19,9 @@ import { Room } from "matrix-js-sdk/src/matrix"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import BaseCard from "./BaseCard"; -import WidgetUtils from "../../../utils/WidgetUtils"; +import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils"; import AppTile from "../elements/AppTile"; import { _t } from "../../../languageHandler"; -import { useWidgets } from "./RoomSummaryCard"; import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 3b2f294e5c..a8fe6cdbe9 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -102,7 +102,7 @@ interface IState { export default class AliasSettings extends React.Component { public static contextType = MatrixClientContext; - public context!: ContextType; + public declare context: ContextType; public static defaultProps = { canSetAliases: false, diff --git a/src/components/views/room_settings/RoomProfileSettings.tsx b/src/components/views/room_settings/RoomProfileSettings.tsx index c829a26bc1..e565e1239c 100644 --- a/src/components/views/room_settings/RoomProfileSettings.tsx +++ b/src/components/views/room_settings/RoomProfileSettings.tsx @@ -253,7 +253,7 @@ export default class RoomProfileSettings extends React.Component avatar={ this.state.avatarRemovalPending ? undefined - : this.state.avatarFile ?? this.state.originalAvatarUrl ?? undefined + : (this.state.avatarFile ?? this.state.originalAvatarUrl ?? undefined) } avatarAltText={_t("room_settings|general|avatar_field_label")} disabled={!this.state.canSetAvatar} diff --git a/src/components/views/room_settings/UrlPreviewSettings.tsx b/src/components/views/room_settings/UrlPreviewSettings.tsx index b2b4c553f0..ad0d1bd98e 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.tsx +++ b/src/components/views/room_settings/UrlPreviewSettings.tsx @@ -101,7 +101,7 @@ export default class UrlPreviewSettings extends React.Component { ( ); diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 8eedf0867e..eb7c42ee7c 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -56,9 +56,10 @@ export default class Autocomplete extends React.PureComponent { private containerRef = createRef(); public static contextType = RoomContext; + public declare context: React.ContextType; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { // list of completionResults, each containing completions diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index fabca13a1c..9b33f42914 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -129,7 +129,7 @@ interface IState { class EditMessageComposer extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private readonly editorRef = createRef(); private readonly dispatcherRef: string; @@ -137,8 +137,7 @@ class EditMessageComposer extends React.Component) { - super(props); - this.context = context; // otherwise React will only set it prior to render due to type def above + super(props, context); const isRestored = this.createEditorModel(); const ev = this.props.editState.getEvent(); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 669e57a06a..e0f8ab1161 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -297,7 +297,7 @@ export class UnwrappedEventTile extends React.Component }; public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private unmounted = false; diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx index c6fa28fc7c..20c8357473 100644 --- a/src/components/views/rooms/LegacyRoomHeader.tsx +++ b/src/components/views/rooms/LegacyRoomHeader.tsx @@ -54,7 +54,7 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHa import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings"; import SdkConfig from "../../../SdkConfig"; import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import { useWidgets } from "../right_panel/RoomSummaryCard"; +import { useWidgets } from "../../../utils/WidgetUtils"; import { WidgetType } from "../../../widgets/WidgetType"; import { useCall, useLayout } from "../../../hooks/useCall"; import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers"; @@ -493,11 +493,11 @@ export default class RoomHeader extends React.Component { }; public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private readonly client = this.props.room.client; private readonly featureAskToJoinWatcher: string; - public constructor(props: IProps, context: IState) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room); notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate); diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 85e1f0a988..554b671491 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -84,11 +84,11 @@ export default class MemberList extends React.Component { private mounted = false; public static contextType = SDKContext; - public context!: React.ContextType; + public declare context: React.ContextType; private tiles: Map = new Map(); public constructor(props: IProps, context: React.ContextType) { - super(props); + super(props, context); this.state = this.getMembersState([], []); this.showPresence = context?.memberListStore.isPresenceEnabled() ?? true; this.mounted = true; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index bb4b4c7245..a4a73b495f 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -119,7 +119,7 @@ export class MessageComposer extends React.Component { private _voiceRecording: Optional; public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; public static defaultProps = { compact: false, @@ -127,8 +127,8 @@ export class MessageComposer extends React.Component { isRichTextEnabled: true, }; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); this.state = { diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 77d809d1f8..dc5c14517b 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -298,7 +298,7 @@ interface IPollButtonProps { class PollButton extends React.PureComponent { public static contextType = OverflowMenuContext; - public context!: React.ContextType; + public declare context: React.ContextType; private onCreateClick = (): void => { this.context?.(); // close overflow menu diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx index 581583d1d5..5252e5124d 100644 --- a/src/components/views/rooms/PinnedEventTile.tsx +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -15,112 +15,206 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import { MatrixEvent, EventType, RelationType, Relations } from "matrix-js-sdk/src/matrix"; +import React, { JSX, useCallback, useState } from "react"; +import { EventTimeline, EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { IconButton, Menu, MenuItem, Separator, Text } from "@vector-im/compound-web"; +import { Icon as ViewIcon } from "@vector-im/compound-design-tokens/icons/visibility-on.svg"; +import { Icon as UnpinIcon } from "@vector-im/compound-design-tokens/icons/unpin.svg"; +import { Icon as ForwardIcon } from "@vector-im/compound-design-tokens/icons/forward.svg"; +import { Icon as TriggerIcon } from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"; +import { Icon as DeleteIcon } from "@vector-im/compound-design-tokens/icons/delete.svg"; +import classNames from "classnames"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; -import AccessibleButton from "../elements/AccessibleButton"; import MessageEvent from "../messages/MessageEvent"; import MemberAvatar from "../avatars/MemberAvatar"; import { _t } from "../../../languageHandler"; -import { formatDate } from "../../../DateUtils"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; - -interface IProps { +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { useRoomState } from "../../../hooks/useRoomState"; +import { isContentActionable } from "../../../utils/EventUtils"; +import { getForwardableEvent } from "../../../events"; +import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload"; +import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog"; + +const AVATAR_SIZE = "32px"; + +/** + * Properties for {@link PinnedEventTile}. + */ +interface PinnedEventTileProps { + /** + * The event to display. + */ event: MatrixEvent; + /** + * The permalink creator to use. + */ permalinkCreator: RoomPermalinkCreator; - onUnpinClicked?(): void; + /** + * The room the event is in. + */ + room: Room; } -const AVATAR_SIZE = "24px"; +/** + * A pinned event tile. + */ +export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTileProps): JSX.Element { + const sender = event.getSender(); + if (!sender) { + throw new Error("Pinned event unexpectedly has no sender"); + } -export default class PinnedEventTile extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; + return ( +
+
+ +
+
+
+ + {event.sender?.name || sender} + + +
+ {}} // we need to give this, apparently + permalinkCreator={permalinkCreator} + replacingEventId={event.replacingEventId()} + /> +
+
+ ); +} - private onTileClicked = (): void => { +/** + * Properties for {@link PinMenu}. + */ +interface PinMenuProps extends PinnedEventTileProps {} + +/** + * A popover menu with actions on the pinned event + */ +function PinMenu({ event, room, permalinkCreator }: PinMenuProps): JSX.Element { + const [open, setOpen] = useState(false); + const matrixClient = useMatrixClientContext(); + + /** + * View the event in the timeline. + */ + const onViewInTimeline = useCallback(() => { dis.dispatch({ action: Action.ViewRoom, - event_id: this.props.event.getId(), + event_id: event.getId(), highlighted: true, - room_id: this.props.event.getRoomId(), + room_id: event.getRoomId(), metricsTrigger: undefined, // room doesn't change }); - }; - - // For event types like polls that use relations, we fetch those manually on - // mount and store them here, exposing them through getRelationsForEvent - private relations = new Map>(); - private getRelationsForEvent = ( - eventId: string, - relationType: RelationType | string, - eventType: EventType | string, - ): Relations | undefined => { - if (eventId === this.props.event.getId()) { - return this.relations.get(relationType)?.get(eventType); - } - }; - - public render(): React.ReactNode { - const sender = this.props.event.getSender(); - - if (!sender) { - throw new Error("Pinned event unexpectedly has no sender"); + }, [event]); + + /** + * Whether the client can unpin the event. + * Pin and unpin are using the same permission. + */ + const canUnpin = useRoomState(room, (state) => + state.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient), + ); + + /** + * Unpin the event. + * @param event + */ + const onUnpin = useCallback(async (): Promise => { + const pinnedEvents = room + .getLiveTimeline() + .getState(EventTimeline.FORWARDS) + ?.getStateEvents(EventType.RoomPinnedEvents, ""); + if (pinnedEvents?.getContent()?.pinned) { + const pinned = pinnedEvents.getContent().pinned; + const index = pinned.indexOf(event.getId()); + if (index !== -1) { + pinned.splice(index, 1); + await matrixClient.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, ""); + } } - - let unpinButton: JSX.Element | undefined; - if (this.props.onUnpinClicked) { - unpinButton = ( - - ); + }, [event, room, matrixClient]); + + const contentActionable = isContentActionable(event); + // Get the forwardable event for the given event + const forwardableEvent = contentActionable && getForwardableEvent(event, matrixClient); + /** + * Open the forward dialog. + */ + const onForward = useCallback(() => { + if (forwardableEvent) { + dis.dispatch({ + action: Action.OpenForwardDialog, + event: forwardableEvent, + permalinkCreator: permalinkCreator, + }); } - - return ( -
- - - - {this.props.event.sender?.name || sender} - - - {unpinButton} - -
- {}} // we need to give this, apparently - permalinkCreator={this.props.permalinkCreator} - replacingEventId={this.props.event.replacingEventId()} - /> -
- -
- - {formatDate(new Date(this.props.event.getTs()))} - - - - {_t("common|view_message")} - -
-
- ); - } + }, [forwardableEvent, permalinkCreator]); + + /** + * Whether the client can redact the event. + */ + const canRedact = + room + .getLiveTimeline() + .getState(EventTimeline.FORWARDS) + ?.maySendRedactionForEvent(event, matrixClient.getSafeUserId()) && + event.getType() !== EventType.RoomServerAcl && + event.getType() !== EventType.RoomEncryption; + + /** + * Redact the event. + */ + const onRedact = useCallback( + (): void => + createRedactEventDialog({ + mxEvent: event, + }), + [event], + ); + + return ( + + + + } + > + + {canUnpin && } + {forwardableEvent && } + {canRedact && ( + <> + + + + )} + + ); } diff --git a/src/components/views/rooms/PresenceLabel.tsx b/src/components/views/rooms/PresenceLabel.tsx index bdbc7e23e2..55e6b111d9 100644 --- a/src/components/views/rooms/PresenceLabel.tsx +++ b/src/components/views/rooms/PresenceLabel.tsx @@ -21,7 +21,7 @@ import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { formatDuration } from "../../../DateUtils"; -const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy"); +export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy"); interface IProps { // number of milliseconds ago this user was last active. diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx index 50c625c428..431af5ce6f 100644 --- a/src/components/views/rooms/ReplyPreview.tsx +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -39,6 +39,7 @@ interface IProps { export default class ReplyPreview extends React.Component { public static contextType = RoomContext; + public declare context: React.ContextType; public render(): JSX.Element | null { if (!this.props.replyToEvent) return null; diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 7c7c800119..1b5d02e0c5 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useContext, useMemo, useState } from "react"; import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg"; @@ -25,13 +25,11 @@ import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/ico import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; -import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; +import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import { useRoomName } from "../../../hooks/useRoomName"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; -import { useTopic } from "../../../hooks/room/useTopic"; -import { useAccountData } from "../../../hooks/useAccountData"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMembers"; import { _t } from "../../../languageHandler"; @@ -49,17 +47,20 @@ import { useRoomState } from "../../../hooks/useRoomState"; import RoomAvatar from "../avatars/RoomAvatar"; import { formatCount } from "../../../utils/FormattingUtils"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; -import { Linkify, topicToHtml } from "../../../HtmlUtils"; import PosthogTrackers from "../../../PosthogTrackers"; import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton"; import { RoomKnocksBar } from "./RoomKnocksBar"; -import { isVideoRoom } from "../../../utils/video-rooms"; +import { useIsVideoRoom } from "../../../utils/video-rooms"; import { notificationLevelToIndicator } from "../../../utils/notifications"; import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton"; import { ButtonEvent } from "../elements/AccessibleButton"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement"; import { useIsReleaseAnnouncementOpen } from "../../../hooks/useIsReleaseAnnouncementOpen"; import { ReleaseAnnouncementStore } from "../../../stores/ReleaseAnnouncementStore"; +import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator"; +import { IOOBData } from "../../../stores/ThreepidInviteStore"; +import RoomContext from "../../../contexts/RoomContext"; +import { MainSplitContentType } from "../../structures/RoomView"; import TchapUIFeature from "../../../../../../src/tchap/util/TchapUIFeature"; // :TCHAP: customize-room-header-bar import TchapExternalRoomHeader from "../../../../../../src/tchap/components/views/rooms/TchapExternalRoomHeader"; // :TCHAP: customize-room-header-bar @@ -68,15 +69,16 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; // :TCHAP: cus export default function RoomHeader({ room, additionalButtons, + oobData, }: { room: Room; additionalButtons?: ViewRoomOpts["buttons"]; + oobData?: IOOBData; }): JSX.Element { const client = useMatrixClientContext(); const roomName = useRoomName(room); - const roomTopic = useTopic(room); - const roomState = useRoomState(room); + const joinRule = useRoomState(room, (state) => state.getJoinRule()); const members = useRoomMembers(room, 2500); const memberCount = useRoomMemberCount(room, { throttleWait: 2500 }); @@ -107,28 +109,18 @@ export default function RoomHeader({ const threadNotifications = useRoomThreadNotifications(room); const globalNotificationState = useGlobalNotificationState(); - const directRoomsList = useAccountData>(client, EventType.Direct); - const [isDirectMessage, setDirectMessage] = useState(false); - useEffect(() => { - for (const [, dmRoomList] of Object.entries(directRoomsList)) { - if (dmRoomList.includes(room?.roomId ?? "")) { - setDirectMessage(true); - break; - } - } - }, [room, directRoomsList]); + const dmMember = useDmMember(room); + const isDirectMessage = !!dmMember; const e2eStatus = useEncryptionStatus(client, room); const notificationsEnabled = useFeatureEnabled("feature_notifications"); - const roomTopicBody = useMemo( - () => topicToHtml(roomTopic?.text, roomTopic?.html), - [roomTopic?.html, roomTopic?.text], - ); - const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); - const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]); + const videoClick = useCallback( + (ev: React.MouseEvent) => videoCallClick(ev, callOptions[0]), + [callOptions, videoCallClick], + ); const toggleCallButton = ( @@ -247,6 +239,13 @@ export default function RoomHeader({ const isReleaseAnnouncementOpen = useIsReleaseAnnouncementOpen("newRoomHeader"); + const roomContext = useContext(RoomContext); + const isVideoRoom = useIsVideoRoom(room); + const showChatButton = + isVideoRoom || + roomContext.mainSplitContentType === MainSplitContentType.MaximisedWidget || + roomContext.mainSplitContentType === MainSplitContentType.Call; + return ( <> @@ -268,8 +267,10 @@ export default function RoomHeader({ }} className="mx_RoomHeader_infoWrapper" > - {/* :TCHAP: customize-room-header-bar - RoomAvatar -> DecoratedRoomAvatar - + {/* :TCHAP: customize-room-header-bar - RoomAvatar -> DecoratedRoomAvatar + + + */} {/* end :TCHAP: */} @@ -327,15 +328,6 @@ export default function RoomHeader({ )} */} - {roomTopic && ( - - {roomTopicBody} - - )} @@ -359,24 +351,23 @@ export default function RoomHeader({ })} {isViewingCall && } - {((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && } {hasActiveCallSession && !isConnectedToCall && !isViewingCall ? ( joinCallButton ) : ( <> { /* :TCHAP: customize-room-header-bar - activate video call only if directmessage and if feature is activated on homeserver } - {!isVideoRoom(room) && videoCallButton} + {!isVideoRoom && videoCallButton} */ } {isDirectMessage && TchapUIFeature.isFeatureActiveForHomeserver("feature_video_call") && - !isVideoRoom(room) && videoCallButton} + !isVideoRoom && videoCallButton} {/* end :TCHAP: */} { /* :TCHAP: customize-room-header-bar - activate audio call only if directmessage and if feature is activated on homeserver {!useElementCallExclusively && !isVideoRoom(room) && voiceCallButton} */ } {isDirectMessage && TchapUIFeature.isFeatureActiveForHomeserver("feature_audio_call") && - !useElementCallExclusively && !isVideoRoom(room) && voiceCallButton} + !useElementCallExclusively && !isVideoRoom && voiceCallButton} {/* end :TCHAP: */} )} @@ -393,7 +384,10 @@ export default function RoomHeader({ + {showChatButton && } + {/* :TCHAP: extend-remove-thread-buttons + = ({ room }) => { const membership = useMyRoomMembership(room); const memberCount = useRoomMemberCount(room); - const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); - const isVideoRoom = room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom()); + const isVideoRoom = useIsVideoRoom(room, true); let iconClass: string; let roomType: string; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 8a0de4d7b2..ebb0d89382 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -432,10 +432,10 @@ export default class RoomList extends React.PureComponent { private treeRef = createRef(); public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { sublists: {}, diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx index 472cb4e4eb..0f4c552fdc 100644 --- a/src/components/views/rooms/RoomPreviewBar.tsx +++ b/src/components/views/rooms/RoomPreviewBar.tsx @@ -506,33 +506,39 @@ export default class RoomPreviewBar extends React.Component { break; } case MessageCase.Invite: { + const isDM = this.isDMInvite(); const avatar = ; const inviteMember = this.getInviteMember(); - let inviterElement: JSX.Element; - if (inviteMember) { - inviterElement = ( - - {inviteMember.rawDisplayName} ( - {inviteMember.userId}) - - ); - } else { - inviterElement = {this.props.inviterName}; - } + const userName = ( + + {inviteMember?.rawDisplayName ?? this.props.inviterName} + + ); + const inviterElement = ( + <> + {isDM + ? _t("room|dm_invite_subtitle", {}, { userName }) + : _t("room|invite_subtitle", {}, { userName })} + {inviteMember && ( + <> +
+ {inviteMember.userId} + + )} + + ); - const isDM = this.isDMInvite(); if (isDM) { title = _t("room|dm_invite_title", { user: inviteMember?.name ?? this.props.inviterName, }); - subTitle = [avatar, _t("room|dm_invite_subtitle", {}, { userName: () => inviterElement })]; primaryActionLabel = _t("room|dm_invite_action"); } else { title = _t("room|invite_title", { roomName }); - subTitle = [avatar, _t("room|invite_subtitle", {}, { userName: () => inviterElement })]; primaryActionLabel = _t("action|accept"); } + subTitle = [avatar, inviterElement]; const myUserId = MatrixClientPeg.safeGet().getSafeUserId(); const member = this.props.room?.currentState.getMember(myUserId); diff --git a/src/components/views/rooms/RoomPreviewCard.tsx b/src/components/views/rooms/RoomPreviewCard.tsx index ee31206c38..2fc9a57aa6 100644 --- a/src/components/views/rooms/RoomPreviewCard.tsx +++ b/src/components/views/rooms/RoomPreviewCard.tsx @@ -37,6 +37,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import MemberAvatar from "../avatars/MemberAvatar"; import { BetaPill } from "../beta/BetaCard"; import RoomInfoLine from "./RoomInfoLine"; +import { useIsVideoRoom } from "../../../utils/video-rooms"; interface IProps { room: Room; @@ -51,8 +52,7 @@ interface IProps { const RoomPreviewCard: FC = ({ room, onJoinButtonClicked, onRejectButtonClicked }) => { const cli = useContext(MatrixClientContext); const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms"); - const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); - const isVideoRoom = room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom()); + const isVideoRoom = useIsVideoRoom(room, true); const myMembership = useMyRoomMembership(room); useDispatcher(defaultDispatcher, (payload) => { if (payload.action === Action.JoinRoomError && payload.roomId === room.roomId) { diff --git a/src/components/views/rooms/RoomSearchAuxPanel.tsx b/src/components/views/rooms/RoomSearchAuxPanel.tsx index 0882c4a8bf..1461b12a42 100644 --- a/src/components/views/rooms/RoomSearchAuxPanel.tsx +++ b/src/components/views/rooms/RoomSearchAuxPanel.tsx @@ -23,6 +23,7 @@ import { _t } from "../../../languageHandler"; import { PosthogScreenTracker } from "../../../PosthogTrackers"; import SearchWarning, { WarningKind } from "../elements/SearchWarning"; import { SearchInfo, SearchScope } from "../../../Searching"; +import InlineSpinner from "../elements/InlineSpinner"; interface Props { searchInfo?: SearchInfo; @@ -41,13 +42,15 @@ const RoomSearchAuxPanel: React.FC = ({ searchInfo, isRoomEncrypted, onSe
- {searchInfo - ? _t( - "room|search|summary", - { count: searchInfo.count ?? 0 }, - { query: () => {searchInfo.term} }, - ) - : undefined} + {searchInfo?.count !== undefined ? ( + _t( + "room|search|summary", + { count: searchInfo.count }, + { query: () => {searchInfo.term} }, + ) + ) : ( + + )}
diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 99ae4f7ba4..ab374108e9 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -33,7 +33,7 @@ interface IState { export default class RoomUpgradeWarningBar extends React.PureComponent { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 99b5f0805c..52977901cf 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -44,7 +44,7 @@ interface IProps { export default class SearchResultTile extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; // A map of private callEventGroupers = new Map(); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index c5972ee86a..9e986a181e 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -71,6 +71,9 @@ import { IDiff } from "../../../editor/diff"; import { getBlobSafeMimeType } from "../../../utils/blobs"; import { EMOJI_REGEX } from "../../../HtmlUtils"; +// The prefix used when persisting editor drafts to localstorage. +export const EDITOR_STATE_STORAGE_PREFIX = "mx_cider_state_"; + /** * Build the mentions information based on the editor model (and any related events): * @@ -254,7 +257,7 @@ interface ISendMessageComposerProps extends MatrixClientProps { export class SendMessageComposer extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private readonly prepareToEncrypt?: DebouncedFunc<() => void>; private readonly editorRef = createRef(); @@ -269,7 +272,6 @@ export class SendMessageComposer extends React.Component) { super(props, context); - this.context = context; // otherwise React will only set it prior to render due to type def above if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) { this.prepareToEncrypt = throttle( @@ -605,7 +607,7 @@ export class SendMessageComposer extends React.Component { public static contextType = RoomContext; - public context!: React.ContextType; + public declare context: React.ContextType; private voiceRecordingId: string; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = {}; diff --git a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx index ded0c39129..e35ad34de3 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx @@ -26,6 +26,7 @@ import { useSetCursorPosition } from "../hooks/useSetCursorPosition"; import { ComposerFunctions } from "../types"; import { Editor } from "./Editor"; import { WysiwygAutocomplete } from "./WysiwygAutocomplete"; +import { useSettingValue } from "../../../../../hooks/useSettings"; interface PlainTextComposerProps { disabled?: boolean; @@ -52,6 +53,7 @@ export function PlainTextComposer({ rightComponent, eventRelation, }: PlainTextComposerProps): JSX.Element { + const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji"); const { ref: editorRef, autocompleteRef, @@ -66,14 +68,12 @@ export function PlainTextComposer({ handleCommand, handleMention, handleAtRoomMention, - } = usePlainTextListeners(initialContent, onChange, onSend, eventRelation); - + } = usePlainTextListeners(initialContent, onChange, onSend, eventRelation, isAutoReplaceEmojiEnabled); const composerFunctions = useComposerFunctions(editorRef, setContent); usePlainTextInitialization(initialContent, editorRef); useSetCursorPosition(disabled, editorRef); const { isFocused, onFocus } = useIsFocused(); const computedPlaceholder = (!content && placeholder) || undefined; - return (
{ + const emojiSuggestions = new Map(Array.from(EMOTICON_TO_EMOJI, ([key, value]) => [key, value.unicode])); + return enabled ? emojiSuggestions : new Map(); +} + export const WysiwygComposer = memo(function WysiwygComposer({ disabled = false, onChange, @@ -61,9 +68,14 @@ export const WysiwygComposer = memo(function WysiwygComposer({ const autocompleteRef = useRef(null); const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation); + + const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji"); + const emojiSuggestions = useMemo(() => getEmojiSuggestions(isAutoReplaceEmojiEnabled), [isAutoReplaceEmojiEnabled]); + const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({ initialContent, inputEventProcessor, + emojiSuggestions, }); const { isFocused, onFocus } = useIsFocused(); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 6121f0c877..282ed9d174 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -40,6 +40,8 @@ function isDivElement(target: EventTarget): target is HTMLDivElement { * @param initialContent - the content of the editor when it is first mounted * @param onChange - called whenever there is change in the editor content * @param onSend - called whenever the user sends the message + * @param eventRelation - used to send the event to the correct place eg timeline vs thread + * @param isAutoReplaceEmojiEnabled - whether plain text emoticons should be auto replaced with emojis * @returns * - `ref`: a ref object which the caller must attach to the HTML `div` node for the editor * * `autocompleteRef`: a ref object which the caller must attach to the autocomplete component @@ -53,6 +55,7 @@ export function usePlainTextListeners( onChange?: (content: string) => void, onSend?: () => void, eventRelation?: IEventRelation, + isAutoReplaceEmojiEnabled?: boolean, ): { ref: RefObject; autocompleteRef: React.RefObject; @@ -100,7 +103,8 @@ export function usePlainTextListeners( // For separation of concerns, the suggestion handling is kept in a separate hook but is // nested here because we do need to be able to update the `content` state in this hook // when a user selects a suggestion from the autocomplete menu - const { suggestion, onSelect, handleCommand, handleMention, handleAtRoomMention } = useSuggestion(ref, setText); + const { suggestion, onSelect, handleCommand, handleMention, handleAtRoomMention, handleEmojiReplacement } = + useSuggestion(ref, setText, isAutoReplaceEmojiEnabled); const onInput = useCallback( (event: SyntheticEvent) => { @@ -140,6 +144,10 @@ export function usePlainTextListeners( if (isHandledByAutocomplete) { return; } + // handle accepting of plain text emojicon to emoji replacement + if (event.key == Key.ENTER || event.key == Key.SPACE) { + handleEmojiReplacement(); + } // resume regular flow if (event.key === Key.ENTER) { @@ -161,7 +169,7 @@ export function usePlainTextListeners( } } }, - [autocompleteRef, enterShouldSend, send], + [autocompleteRef, enterShouldSend, send, handleEmojiReplacement], ); return { diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts index b7a7236dda..337849aece 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EMOTICON_TO_EMOJI } from "@matrix-org/emojibase-bindings"; import { AllowedMentionAttributes, MappedSuggestion } from "@matrix-org/matrix-wysiwyg"; import { SyntheticEvent, useState, SetStateAction } from "react"; import { logger } from "matrix-js-sdk/src/logger"; @@ -41,6 +42,7 @@ type SuggestionState = Suggestion | null; * * @param editorRef - a ref to the div that is the composer textbox * @param setText - setter function to set the content of the composer + * @param isAutoReplaceEmojiEnabled - whether plain text emoticons should be auto replaced with emojis * @returns * - `handleMention`: a function that will insert @ or # mentions which are selected from * the autocomplete into the composer, given an href, the text to display, and any additional attributes @@ -53,10 +55,12 @@ type SuggestionState = Suggestion | null; export function useSuggestion( editorRef: React.RefObject, setText: (text?: string) => void, + isAutoReplaceEmojiEnabled?: boolean, ): { handleMention: (href: string, displayName: string, attributes: AllowedMentionAttributes) => void; handleAtRoomMention: (attributes: AllowedMentionAttributes) => void; handleCommand: (text: string) => void; + handleEmojiReplacement: () => void; onSelect: (event: SyntheticEvent) => void; suggestion: MappedSuggestion | null; } { @@ -77,7 +81,7 @@ export function useSuggestion( // We create a `selectionchange` handler here because we need to know when the user has moved the cursor, // we can not depend on input events only - const onSelect = (): void => processSelectionChange(editorRef, setSuggestionData); + const onSelect = (): void => processSelectionChange(editorRef, setSuggestionData, isAutoReplaceEmojiEnabled); const handleMention = (href: string, displayName: string, attributes: AllowedMentionAttributes): void => processMention(href, displayName, attributes, suggestionData, setSuggestionData, setText); @@ -88,11 +92,14 @@ export function useSuggestion( const handleCommand = (replacementText: string): void => processCommand(replacementText, suggestionData, setSuggestionData, setText); + const handleEmojiReplacement = (): void => processEmojiReplacement(suggestionData, setSuggestionData, setText); + return { suggestion: suggestionData?.mappedSuggestion ?? null, handleCommand, handleMention, handleAtRoomMention, + handleEmojiReplacement, onSelect, }; } @@ -103,10 +110,12 @@ export function useSuggestion( * * @param editorRef - ref to the composer * @param setSuggestionData - the setter for the suggestion state + * @param isAutoReplaceEmojiEnabled - whether plain text emoticons should be auto replaced with emojis */ export function processSelectionChange( editorRef: React.RefObject, setSuggestionData: React.Dispatch>, + isAutoReplaceEmojiEnabled?: boolean, ): void { const selection = document.getSelection(); @@ -132,7 +141,12 @@ export function processSelectionChange( const firstTextNode = document.createNodeIterator(editorRef.current, NodeFilter.SHOW_TEXT).nextNode(); const isFirstTextNode = currentNode === firstTextNode; - const foundSuggestion = findSuggestionInText(currentNode.textContent, currentOffset, isFirstTextNode); + const foundSuggestion = findSuggestionInText( + currentNode.textContent, + currentOffset, + isFirstTextNode, + isAutoReplaceEmojiEnabled, + ); // if we have not found a suggestion, return, clearing the suggestion state if (foundSuggestion === null) { @@ -241,6 +255,42 @@ export function processCommand( setSuggestionData(null); } +/** + * Replaces the relevant part of the editor text, replacing the plain text emoitcon with the suggested emoji. + * + * @param suggestionData - representation of the part of the DOM that will be replaced + * @param setSuggestionData - setter function to set the suggestion state + * @param setText - setter function to set the content of the composer + */ +export function processEmojiReplacement( + suggestionData: SuggestionState, + setSuggestionData: React.Dispatch>, + setText: (text?: string) => void, +): void { + // if we do not have a suggestion of the correct type, return early + if (suggestionData === null || suggestionData.mappedSuggestion.type !== `custom`) { + return; + } + const { node, mappedSuggestion } = suggestionData; + const existingContent = node.textContent; + + if (existingContent == null) { + return; + } + + // replace the emoticon with the suggesed emoji + const newContent = + existingContent.slice(0, suggestionData.startOffset) + + mappedSuggestion.text + + existingContent.slice(suggestionData.endOffset); + + node.textContent = newContent; + + document.getSelection()?.setBaseAndExtent(node, newContent.length, node, newContent.length); + setText(newContent); + setSuggestionData(null); +} + /** * Given some text content from a node and the cursor position, find the word that the cursor is currently inside * and then test that word to see if it is a suggestion. Return the `MappedSuggestion` with start and end offsets if @@ -250,12 +300,14 @@ export function processCommand( * @param offset - the current cursor offset position within the node * @param isFirstTextNode - whether or not the node is the first text node in the editor. Used to determine * if a command suggestion is found or not + * @param isAutoReplaceEmojiEnabled - whether plain text emoticons should be auto replaced with emojis * @returns the `MappedSuggestion` along with its start and end offsets if found, otherwise null */ export function findSuggestionInText( text: string, offset: number, isFirstTextNode: boolean, + isAutoReplaceEmojiEnabled?: boolean, ): { mappedSuggestion: MappedSuggestion; startOffset: number; endOffset: number } | null { // Return null early if the offset is outside the content if (offset < 0 || offset > text.length) { @@ -281,7 +333,7 @@ export function findSuggestionInText( // Get the word at the cursor then check if it contains a suggestion or not const wordAtCursor = text.slice(startSliceIndex, endSliceIndex); - const mappedSuggestion = getMappedSuggestion(wordAtCursor); + const mappedSuggestion = getMappedSuggestion(wordAtCursor, isAutoReplaceEmojiEnabled); /** * If we have a word that could be a command, it is not a valid command if: @@ -339,9 +391,17 @@ function shouldIncrementEndIndex(text: string, index: number): boolean { * Given a string, return a `MappedSuggestion` if the string contains a suggestion. Otherwise return null. * * @param text - string to check for a suggestion + * @param isAutoReplaceEmojiEnabled - whether plain text emoticons should be auto replaced with emojis * @returns a `MappedSuggestion` if a suggestion is present, null otherwise */ -export function getMappedSuggestion(text: string): MappedSuggestion | null { +export function getMappedSuggestion(text: string, isAutoReplaceEmojiEnabled?: boolean): MappedSuggestion | null { + if (isAutoReplaceEmojiEnabled) { + const emoji = EMOTICON_TO_EMOJI.get(text.toLocaleLowerCase()); + if (emoji?.unicode) { + return { keyChar: "", text: emoji.unicode, type: "custom" }; + } + } + const firstChar = text.charAt(0); const restOfString = text.slice(1); diff --git a/src/components/views/settings/AddRemoveThreepids.tsx b/src/components/views/settings/AddRemoveThreepids.tsx new file mode 100644 index 0000000000..242afd272c --- /dev/null +++ b/src/components/views/settings/AddRemoveThreepids.tsx @@ -0,0 +1,534 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback, useRef, useState } from "react"; +import { logger } from "matrix-js-sdk/src/logger"; +import { + IRequestMsisdnTokenResponse, + IRequestTokenResponse, + MatrixError, + ThreepidMedium, +} from "matrix-js-sdk/src/matrix"; + +import AddThreepid, { Binding, ThirdPartyIdentifier } from "../../../AddThreepid"; +import { _t, UserFriendlyError } from "../../../languageHandler"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import Modal from "../../../Modal"; +import ErrorDialog, { extractErrorMessageFromError } from "../dialogs/ErrorDialog"; +import Field from "../elements/Field"; +import { looksValid as emailLooksValid } from "../../../email"; +import CountryDropdown from "../auth/CountryDropdown"; +import { PhoneNumberCountryDefinition } from "../../../phonenumber"; +import InlineSpinner from "../elements/InlineSpinner"; + +// Whether we're adding 3pids to the user's account on the homeserver or sharing them on an identity server +type TheepidControlMode = "hs" | "is"; + +interface ExistingThreepidProps { + mode: TheepidControlMode; + threepid: ThirdPartyIdentifier; + onChange: (threepid: ThirdPartyIdentifier) => void; + disabled?: boolean; +} + +const ExistingThreepid: React.FC = ({ mode, threepid, onChange, disabled }) => { + const [isConfirming, setIsConfirming] = useState(false); + const client = useMatrixClientContext(); + const bindTask = useRef(); + + const [isVerifyingBind, setIsVerifyingBind] = useState(false); + const [continueDisabled, setContinueDisabled] = useState(false); + const [verificationCode, setVerificationCode] = useState(""); + + const onRemoveClick = useCallback((e: ButtonEvent) => { + e.stopPropagation(); + e.preventDefault(); + + setIsConfirming(true); + }, []); + + const onCancelClick = useCallback((e: ButtonEvent) => { + e.stopPropagation(); + e.preventDefault(); + + setIsConfirming(false); + }, []); + + const onConfirmRemoveClick = useCallback( + (e: ButtonEvent) => { + e.stopPropagation(); + e.preventDefault(); + + client + .deleteThreePid(threepid.medium, threepid.address) + .then(() => { + return onChange(threepid); + }) + .catch((err) => { + logger.error("Unable to remove contact information: " + err); + Modal.createDialog(ErrorDialog, { + title: _t("settings|general|error_remove_3pid"), + description: err && err.message ? err.message : _t("invite|failed_generic"), + }); + }); + }, + [client, threepid, onChange], + ); + + const changeBinding = useCallback( + async ({ bind, label, errorTitle }: Binding) => { + try { + if (bind) { + bindTask.current = new AddThreepid(client); + setContinueDisabled(true); + if (threepid.medium === ThreepidMedium.Email) { + await bindTask.current.bindEmailAddress(threepid.address); + } else { + // XXX: Sydent will accept a number without country code if you add + // a leading plus sign to a number in E.164 format (which the 3PID + // address is), but this goes against the spec. + // See https://github.com/matrix-org/matrix-doc/issues/2222 + await bindTask.current.bindMsisdn(null as unknown as string, `+${threepid.address}`); + } + setContinueDisabled(false); + setIsVerifyingBind(true); + } else { + await client.unbindThreePid(threepid.medium, threepid.address); + onChange(threepid); + } + } catch (err) { + logger.error(`changeBinding: Unable to ${label} email address ${threepid.address}`, err); + setIsVerifyingBind(false); + setContinueDisabled(false); + bindTask.current = undefined; + Modal.createDialog(ErrorDialog, { + title: errorTitle, + description: extractErrorMessageFromError(err, _t("invite|failed_generic")), + }); + } + }, + [client, threepid, onChange], + ); + + const onRevokeClick = useCallback( + (e: ButtonEvent): void => { + e.stopPropagation(); + e.preventDefault(); + changeBinding({ + bind: false, + label: "revoke", + errorTitle: + threepid.medium === "email" + ? _t("settings|general|error_revoke_email_discovery") + : _t("settings|general|error_revoke_msisdn_discovery"), + }).then(); + }, + [changeBinding, threepid.medium], + ); + + const onShareClick = useCallback( + (e: ButtonEvent): void => { + e.stopPropagation(); + e.preventDefault(); + changeBinding({ + bind: true, + label: "share", + errorTitle: + threepid.medium === "email" + ? _t("settings|general|error_share_email_discovery") + : _t("settings|general|error_share_msisdn_discovery"), + }).then(); + }, + [changeBinding, threepid.medium], + ); + + const onContinueClick = useCallback( + async (e: ButtonEvent) => { + e.stopPropagation(); + e.preventDefault(); + + setContinueDisabled(true); + try { + if (threepid.medium === ThreepidMedium.Email) { + await bindTask.current?.checkEmailLinkClicked(); + } else { + await bindTask.current?.haveMsisdnToken(verificationCode); + } + setIsVerifyingBind(false); + onChange(threepid); + bindTask.current = undefined; + } catch (err) { + logger.error(`Unable to verify threepid:`, err); + + let underlyingError = err; + if (err instanceof UserFriendlyError) { + underlyingError = err.cause; + } + + if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") { + Modal.createDialog(ErrorDialog, { + title: + threepid.medium === "email" + ? _t("settings|general|email_not_verified") + : _t("settings|general|error_msisdn_verification"), + description: + threepid.medium === "email" + ? _t("settings|general|email_verification_instructions") + : extractErrorMessageFromError(err, _t("invite|failed_generic")), + }); + } else { + logger.error("Unable to verify email address: " + err); + Modal.createDialog(ErrorDialog, { + title: _t("settings|general|error_email_verification"), + description: extractErrorMessageFromError(err, _t("invite|failed_generic")), + }); + } + } finally { + setContinueDisabled(false); + } + }, + [verificationCode, onChange, threepid], + ); + + const onVerificationCodeChange = useCallback((e: React.ChangeEvent) => { + setVerificationCode(e.target.value); + }, []); + + if (isConfirming) { + return ( +
+ + {threepid.medium === ThreepidMedium.Email + ? _t("settings|general|remove_email_prompt", { email: threepid.address }) + : _t("settings|general|remove_msisdn_prompt", { phone: threepid.address })} + + + {_t("action|remove")} + + + {_t("action|cancel")} + +
+ ); + } + + if (isVerifyingBind) { + if (threepid.medium === ThreepidMedium.Email) { + return ( +
+ + {_t("settings|general|discovery_email_verification_instructions")} + + + {_t("action|complete")} + +
+ ); + } else { + return ( +
+ + {_t("settings|general|msisdn_verification_instructions")} + +
+ + +
+ ); + } + } + + return ( +
+ {threepid.address} + + {mode === "hs" ? _t("action|remove") : threepid.bound ? _t("action|revoke") : _t("action|share")} + +
+ ); +}; + +function isMsisdnResponse( + resp: IRequestTokenResponse | IRequestMsisdnTokenResponse, +): resp is IRequestMsisdnTokenResponse { + return (resp as IRequestMsisdnTokenResponse).msisdn !== undefined; +} + +const AddThreepidSection: React.FC<{ medium: "email" | "msisdn"; disabled?: boolean; onChange: () => void }> = ({ + medium, + disabled, + onChange, +}) => { + const addTask = useRef(); + const [newThreepidInput, setNewThreepidInput] = useState(""); + const [phoneCountryInput, setPhoneCountryInput] = useState(""); + const [verificationCodeInput, setVerificationCodeInput] = useState(""); + const [isVerifying, setIsVerifying] = useState(false); + const [continueDisabled, setContinueDisabled] = useState(false); + const [sentToMsisdn, setSentToMsisdn] = useState(""); + + const client = useMatrixClientContext(); + + const onPhoneCountryChanged = useCallback((country: PhoneNumberCountryDefinition) => { + setPhoneCountryInput(country.iso2); + }, []); + + const onContinueClick = useCallback( + (e: ButtonEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (!addTask.current) return; + + setContinueDisabled(true); + + const checkPromise = + medium === "email" + ? addTask.current?.checkEmailLinkClicked() + : addTask.current?.haveMsisdnToken(verificationCodeInput); + checkPromise + .then(([finished]) => { + if (finished) { + addTask.current = undefined; + setIsVerifying(false); + setNewThreepidInput(""); + onChange(); + } + setContinueDisabled(false); + }) + .catch((err) => { + logger.error("Unable to verify 3pid: ", err); + + setContinueDisabled(false); + + let underlyingError = err; + if (err instanceof UserFriendlyError) { + underlyingError = err.cause; + } + + if ( + underlyingError instanceof MatrixError && + underlyingError.errcode === "M_THREEPID_AUTH_FAILED" + ) { + Modal.createDialog(ErrorDialog, { + title: + medium === "email" + ? _t("settings|general|email_not_verified") + : _t("settings|general|error_msisdn_verification"), + description: _t("settings|general|email_verification_instructions"), + }); + } else { + Modal.createDialog(ErrorDialog, { + title: + medium == "email" + ? _t("settings|general|error_email_verification") + : _t("settings|general|error_msisdn_verification"), + description: extractErrorMessageFromError(err, _t("invite|failed_generic")), + }); + } + }); + }, + [onChange, medium, verificationCodeInput], + ); + + const onNewThreepidInputChange = useCallback((e: React.ChangeEvent) => { + setNewThreepidInput(e.target.value); + }, []); + + const onAddClick = useCallback( + (e: React.FormEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (!newThreepidInput) return; + + // TODO: Inline field validation + if (medium === "email" && !emailLooksValid(newThreepidInput)) { + Modal.createDialog(ErrorDialog, { + title: _t("settings|general|error_invalid_email"), + description: _t("settings|general|error_invalid_email_detail"), + }); + return; + } + + addTask.current = new AddThreepid(client); + setIsVerifying(true); + setContinueDisabled(true); + + const addPromise = + medium === "email" + ? addTask.current.addEmailAddress(newThreepidInput) + : addTask.current.addMsisdn(phoneCountryInput, newThreepidInput); + + addPromise + .then((resp: IRequestTokenResponse | IRequestMsisdnTokenResponse) => { + setContinueDisabled(false); + if (isMsisdnResponse(resp)) { + setSentToMsisdn(resp.msisdn); + } + }) + .catch((err) => { + logger.error(`Unable to add threepid ${newThreepidInput}`, err); + setIsVerifying(false); + setContinueDisabled(false); + addTask.current = undefined; + Modal.createDialog(ErrorDialog, { + title: medium === "email" ? _t("settings|general|error_add_email") : _t("common|error"), + description: extractErrorMessageFromError(err, _t("invite|failed_generic")), + }); + }); + }, + [client, phoneCountryInput, newThreepidInput, medium], + ); + + const onVerificationCodeInputChange = useCallback((e: React.ChangeEvent) => { + setVerificationCodeInput(e.target.value); + }, []); + + if (isVerifying && medium === "email") { + return ( +
+
{_t("settings|general|add_email_instructions")}
+ + {_t("action|continue")} + +
+ ); + } else if (isVerifying) { + return ( +
+
+ {_t("settings|general|add_msisdn_instructions", { msisdn: sentToMsisdn })} +
+
+
+ + + {_t("action|continue")} + + +
+ ); + } + + const phoneCountry = + medium === "msisdn" ? ( + + ) : undefined; + + return ( +
+ + + {_t("action|add")} + + + ); +}; + +interface AddRemoveThreepidsProps { + // Whether the control is for adding 3pids to the user's homeserver account or sharing them on an IS + mode: TheepidControlMode; + // Whether the control is for emails or phone numbers + medium: ThreepidMedium; + // The current list of third party identifiers + threepids: ThirdPartyIdentifier[]; + // If true, the component is disabled and no third party identifiers can be added or removed + disabled?: boolean; + // Called when changes are made to the list of third party identifiers + onChange: () => void; + // If true, a spinner is shown instead of the component + isLoading: boolean; +} + +export const AddRemoveThreepids: React.FC = ({ + mode, + medium, + threepids, + disabled, + onChange, + isLoading, +}) => { + if (isLoading) { + return ; + } + + const existingEmailElements = threepids.map((e) => { + return ; + }); + + return ( + <> + {existingEmailElements} + {mode === "hs" && } + + ); +}; diff --git a/src/components/views/settings/UserPersonalInfoSettings.tsx b/src/components/views/settings/UserPersonalInfoSettings.tsx index 8e5880a517..63925424aa 100644 --- a/src/components/views/settings/UserPersonalInfoSettings.tsx +++ b/src/components/views/settings/UserPersonalInfoSettings.tsx @@ -18,8 +18,6 @@ import React, { useCallback, useEffect, useState } from "react"; import { ThreepidMedium } from "matrix-js-sdk/src/matrix"; import { Alert } from "@vector-im/compound-web"; -import AccountEmailAddresses from "./account/EmailAddresses"; -import AccountPhoneNumbers from "./account/PhoneNumbers"; import { _t } from "../../../languageHandler"; import InlineSpinner from "../elements/InlineSpinner"; import SettingsSubsection from "./shared/SettingsSubsection"; @@ -27,6 +25,7 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { ThirdPartyIdentifier } from "../../../AddThreepid"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; +import { AddRemoveThreepids } from "./AddRemoveThreepids"; type LoadingState = "loading" | "loaded" | "error"; @@ -64,26 +63,28 @@ export const UserPersonalInfoSettings: React.FC = const client = useMatrixClientContext(); - useEffect(() => { - (async () => { - try { - const threepids = await client.getThreePids(); - setEmails(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Email)); - setPhoneNumbers(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Phone)); - setLoadingState("loaded"); - } catch (e) { - setLoadingState("error"); - } - })(); + const updateThreepids = useCallback(async () => { + try { + const threepids = await client.getThreePids(); + setEmails(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Email)); + setPhoneNumbers(threepids.threepids.filter((a) => a.medium === ThreepidMedium.Phone)); + setLoadingState("loaded"); + } catch (e) { + setLoadingState("error"); + } }, [client]); - const onEmailsChange = useCallback((emails: ThirdPartyIdentifier[]) => { - setEmails(emails); - }, []); + useEffect(() => { + updateThreepids().then(); + }, [updateThreepids]); + + const onEmailsChange = useCallback(() => { + updateThreepids().then(); + }, [updateThreepids]); - const onMsisdnsChange = useCallback((msisdns: ThirdPartyIdentifier[]) => { - setPhoneNumbers(msisdns); - }, []); + const onMsisdnsChange = useCallback(() => { + updateThreepids().then(); + }, [updateThreepids]); if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null; @@ -99,10 +100,13 @@ export const UserPersonalInfoSettings: React.FC = error={_t("settings|general|unable_to_load_emails")} loadingState={loadingState} > - @@ -116,10 +120,13 @@ export const UserPersonalInfoSettings: React.FC = error={_t("settings|general|unable_to_load_msisdns")} loadingState={loadingState} > - diff --git a/src/components/views/settings/UserProfileSettings.tsx b/src/components/views/settings/UserProfileSettings.tsx index a5ff435676..b793f27dd6 100644 --- a/src/components/views/settings/UserProfileSettings.tsx +++ b/src/components/views/settings/UserProfileSettings.tsx @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"; +import React, { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web"; import { Icon as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg"; +import { Icon as SignOutIcon } from "@vector-im/compound-design-tokens/icons/sign-out.svg"; import { _t } from "../../../languageHandler"; import { OwnProfileStore } from "../../../stores/OwnProfileStore"; @@ -31,8 +32,12 @@ import { useId } from "../../../utils/useId"; import CopyableText from "../elements/CopyableText"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import AccessibleButton from "../elements/AccessibleButton"; +import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog"; +import Modal from "../../../Modal"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import { Flex } from "../../utils/Flex"; -const SpinnerToast: React.FC = ({ children }) => ( +const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => ( <> {children} @@ -76,6 +81,25 @@ const ManageAccountButton: React.FC = ({ externalAccou ); +const SignOutButton: React.FC = () => { + const client = useMatrixClientContext(); + + const onClick = useCallback(async () => { + if (await shouldShowLogoutDialog(client)) { + Modal.createDialog(LogoutDialog); + } else { + defaultDispatcher.dispatch({ action: "logout" }); + } + }, [client]); + + return ( + + + {_t("action|sign_out")} + + ); +}; + interface UserProfileSettingsProps { // The URL to redirect the user to in order to manage their account. externalAccountManagementUrl?: string; @@ -219,11 +243,12 @@ const UserProfileSettings: React.FC = ({ )} {userIdentifier && } - {externalAccountManagementUrl && ( -
+ + {externalAccountManagementUrl && ( -
- )} + )} + +
); }; diff --git a/src/components/views/settings/account/EmailAddresses.tsx b/src/components/views/settings/account/EmailAddresses.tsx deleted file mode 100644 index ef98707c1b..0000000000 --- a/src/components/views/settings/account/EmailAddresses.tsx +++ /dev/null @@ -1,303 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { ThreepidMedium, MatrixError } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { _t, UserFriendlyError } from "../../../../languageHandler"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import Field from "../../elements/Field"; -import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; -import * as Email from "../../../../email"; -import AddThreepid, { ThirdPartyIdentifier } from "../../../../AddThreepid"; -import Modal from "../../../../Modal"; -import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog"; - -/* -TODO: Improve the UX for everything in here. -It's very much placeholder, but it gets the job done. The old way of handling -email addresses in user settings was to use dialogs to communicate state, however -due to our dialog system overriding dialogs (causing unmounts) this creates problems -for a sane UX. For instance, the user could easily end up entering an email address -and receive a dialog to verify the address, which then causes the component here -to forget what it was doing and ultimately fail. Dialogs are still used in some -places to communicate errors - these should be replaced with inline validation when -that is available. - */ - -interface IExistingEmailAddressProps { - email: ThirdPartyIdentifier; - onRemoved: (emails: ThirdPartyIdentifier) => void; - /** - * Disallow removal of this email address when truthy - */ - disabled?: boolean; -} - -interface IExistingEmailAddressState { - verifyRemove: boolean; -} - -export class ExistingEmailAddress extends React.Component { - public constructor(props: IExistingEmailAddressProps) { - super(props); - - this.state = { - verifyRemove: false, - }; - } - - private onRemove = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - this.setState({ verifyRemove: true }); - }; - - private onDontRemove = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - this.setState({ verifyRemove: false }); - }; - - private onActuallyRemove = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - MatrixClientPeg.safeGet() - .deleteThreePid(this.props.email.medium, this.props.email.address) - .then(() => { - return this.props.onRemoved(this.props.email); - }) - .catch((err) => { - logger.error("Unable to remove contact information: " + err); - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_remove_3pid"), - description: err && err.message ? err.message : _t("invite|failed_generic"), - }); - }); - }; - - public render(): React.ReactNode { - if (this.state.verifyRemove) { - return ( -
- - {_t("settings|general|remove_email_prompt", { email: this.props.email.address })} - - - {_t("action|remove")} - - - {_t("action|cancel")} - -
- ); - } - - return ( -
- - {this.props.email.address} - - - {_t("action|remove")} - -
- ); - } -} - -interface IProps { - emails: ThirdPartyIdentifier[]; - onEmailsChange: (emails: ThirdPartyIdentifier[]) => void; - /** - * Adding or removing emails is disabled when truthy - */ - disabled?: boolean; -} - -interface IState { - verifying: boolean; - addTask: AddThreepid | null; - continueDisabled: boolean; - newEmailAddress: string; -} - -export default class EmailAddresses extends React.Component { - public constructor(props: IProps) { - super(props); - - this.state = { - verifying: false, - addTask: null, - continueDisabled: false, - newEmailAddress: "", - }; - } - - private onRemoved = (address: ThirdPartyIdentifier): void => { - const emails = this.props.emails.filter((e) => e !== address); - this.props.onEmailsChange(emails); - }; - - private onChangeNewEmailAddress = (e: React.ChangeEvent): void => { - this.setState({ - newEmailAddress: e.target.value, - }); - }; - - private onAddClick = (e: React.FormEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - if (!this.state.newEmailAddress) return; - - const email = this.state.newEmailAddress; - - // TODO: Inline field validation - if (!Email.looksValid(email)) { - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_invalid_email"), - description: _t("settings|general|error_invalid_email_detail"), - }); - return; - } - - const task = new AddThreepid(MatrixClientPeg.safeGet()); - this.setState({ verifying: true, continueDisabled: true, addTask: task }); - - task.addEmailAddress(email) - .then(() => { - this.setState({ continueDisabled: false }); - }) - .catch((err) => { - logger.error("Unable to add email address " + email + " " + err); - this.setState({ verifying: false, continueDisabled: false, addTask: null }); - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_add_email"), - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - }); - }; - - private onContinueClick = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - this.setState({ continueDisabled: true }); - this.state.addTask - ?.checkEmailLinkClicked() - .then(([finished]) => { - let newEmailAddress = this.state.newEmailAddress; - if (finished) { - const email = this.state.newEmailAddress; - const emails = [...this.props.emails, { address: email, medium: ThreepidMedium.Email }]; - this.props.onEmailsChange(emails); - newEmailAddress = ""; - } - this.setState({ - addTask: null, - continueDisabled: false, - verifying: false, - newEmailAddress, - }); - }) - .catch((err) => { - logger.error("Unable to verify email address: ", err); - - this.setState({ continueDisabled: false }); - - let underlyingError = err; - if (err instanceof UserFriendlyError) { - underlyingError = err.cause; - } - - if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") { - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|email_not_verified"), - description: _t("settings|general|email_verification_instructions"), - }); - } else { - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_email_verification"), - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - } - }); - }; - - public render(): React.ReactNode { - const existingEmailElements = this.props.emails.map((e) => { - return ( - - ); - }); - - let addButton = ( - - {_t("action|add")} - - ); - if (this.state.verifying) { - addButton = ( -
-
{_t("settings|general|add_email_instructions")}
- - {_t("action|continue")} - -
- ); - } - - return ( - <> - {existingEmailElements} -
- - {addButton} - - - ); - } -} diff --git a/src/components/views/settings/account/PhoneNumbers.tsx b/src/components/views/settings/account/PhoneNumbers.tsx deleted file mode 100644 index b037643bc0..0000000000 --- a/src/components/views/settings/account/PhoneNumbers.tsx +++ /dev/null @@ -1,342 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { IAuthData, ThreepidMedium } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { _t, UserFriendlyError } from "../../../../languageHandler"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import Field from "../../elements/Field"; -import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; -import AddThreepid, { ThirdPartyIdentifier } from "../../../../AddThreepid"; -import CountryDropdown from "../../auth/CountryDropdown"; -import Modal from "../../../../Modal"; -import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog"; -import { PhoneNumberCountryDefinition } from "../../../../phonenumber"; - -/* -TODO: Improve the UX for everything in here. -This is a copy/paste of EmailAddresses, mostly. - */ - -// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic - -interface IExistingPhoneNumberProps { - msisdn: ThirdPartyIdentifier; - onRemoved: (phoneNumber: ThirdPartyIdentifier) => void; - /** - * Disable removing phone number - */ - disabled?: boolean; -} - -interface IExistingPhoneNumberState { - verifyRemove: boolean; -} - -export class ExistingPhoneNumber extends React.Component { - public constructor(props: IExistingPhoneNumberProps) { - super(props); - - this.state = { - verifyRemove: false, - }; - } - - private onRemove = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - this.setState({ verifyRemove: true }); - }; - - private onDontRemove = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - this.setState({ verifyRemove: false }); - }; - - private onActuallyRemove = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - MatrixClientPeg.safeGet() - .deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address) - .then(() => { - return this.props.onRemoved(this.props.msisdn); - }) - .catch((err) => { - logger.error("Unable to remove contact information: " + err); - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_remove_3pid"), - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - }); - }; - - public render(): React.ReactNode { - if (this.state.verifyRemove) { - return ( -
- - {_t("settings|general|remove_msisdn_prompt", { phone: this.props.msisdn.address })} - - - {_t("action|remove")} - - - {_t("action|cancel")} - -
- ); - } - - return ( -
- - +{this.props.msisdn.address} - - - {_t("action|remove")} - -
- ); - } -} - -interface IProps { - msisdns: ThirdPartyIdentifier[]; - onMsisdnsChange: (phoneNumbers: ThirdPartyIdentifier[]) => void; - /** - * Adding or removing phone numbers is disabled when truthy - */ - disabled?: boolean; -} - -interface IState { - verifying: boolean; - verifyError: string | null; - verifyMsisdn: string; - addTask: AddThreepid | null; - continueDisabled: boolean; - phoneCountry: string; - newPhoneNumber: string; - newPhoneNumberCode: string; -} - -export default class PhoneNumbers extends React.Component { - public constructor(props: IProps) { - super(props); - - this.state = { - verifying: false, - verifyError: null, - verifyMsisdn: "", - addTask: null, - continueDisabled: false, - phoneCountry: "", - newPhoneNumber: "", - newPhoneNumberCode: "", - }; - } - - private onRemoved = (address: ThirdPartyIdentifier): void => { - const msisdns = this.props.msisdns.filter((e) => e !== address); - this.props.onMsisdnsChange(msisdns); - }; - - private onChangeNewPhoneNumber = (e: React.ChangeEvent): void => { - this.setState({ - newPhoneNumber: e.target.value, - }); - }; - - private onChangeNewPhoneNumberCode = (e: React.ChangeEvent): void => { - this.setState({ - newPhoneNumberCode: e.target.value, - }); - }; - - private onAddClick = (e: ButtonEvent | React.FormEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - if (!this.state.newPhoneNumber) return; - - const phoneNumber = this.state.newPhoneNumber; - const phoneCountry = this.state.phoneCountry; - - const task = new AddThreepid(MatrixClientPeg.safeGet()); - this.setState({ verifying: true, continueDisabled: true, addTask: task }); - - task.addMsisdn(phoneCountry, phoneNumber) - .then((response) => { - this.setState({ continueDisabled: false, verifyMsisdn: response.msisdn }); - }) - .catch((err) => { - logger.error("Unable to add phone number " + phoneNumber + " " + err); - this.setState({ verifying: false, continueDisabled: false, addTask: null }); - Modal.createDialog(ErrorDialog, { - title: _t("common|error"), - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - }); - }; - - private onContinueClick = (e: ButtonEvent | React.FormEvent): void => { - e.stopPropagation(); - e.preventDefault(); - - this.setState({ continueDisabled: true }); - const token = this.state.newPhoneNumberCode; - const address = this.state.verifyMsisdn; - this.state.addTask - ?.haveMsisdnToken(token) - .then(([finished]: [success?: boolean, result?: IAuthData | Error | null] = []) => { - let newPhoneNumber = this.state.newPhoneNumber; - if (finished !== false) { - const msisdns = [...this.props.msisdns, { address, medium: ThreepidMedium.Phone }]; - this.props.onMsisdnsChange(msisdns); - newPhoneNumber = ""; - } - this.setState({ - addTask: null, - continueDisabled: false, - verifying: false, - verifyMsisdn: "", - verifyError: null, - newPhoneNumber, - newPhoneNumberCode: "", - }); - }) - .catch((err) => { - logger.error("Unable to verify phone number: " + err); - this.setState({ continueDisabled: false }); - - let underlyingError = err; - if (err instanceof UserFriendlyError) { - underlyingError = err.cause; - } - - if (underlyingError.errcode !== "M_THREEPID_AUTH_FAILED") { - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_msisdn_verification"), - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - } else { - this.setState({ verifyError: _t("settings|general|incorrect_msisdn_verification") }); - } - }); - }; - - private onCountryChanged = (country: PhoneNumberCountryDefinition): void => { - this.setState({ phoneCountry: country.iso2 }); - }; - - public render(): React.ReactNode { - const existingPhoneElements = this.props.msisdns.map((p) => { - return ( - - ); - }); - - let addVerifySection = ( - - {_t("action|add")} - - ); - if (this.state.verifying) { - const msisdn = this.state.verifyMsisdn; - addVerifySection = ( -
-
- {_t("settings|general|add_msisdn_instructions", { msisdn: msisdn })} -
- {this.state.verifyError} -
-
- - - {_t("action|continue")} - - -
- ); - } - - const phoneCountry = ( - - ); - - return ( - <> - {existingPhoneElements} -
-
- -
-
- {addVerifySection} - - ); - } -} diff --git a/src/components/views/settings/discovery/DiscoverySettings.tsx b/src/components/views/settings/discovery/DiscoverySettings.tsx index 8b1a20ac2e..4eec56e41f 100644 --- a/src/components/views/settings/discovery/DiscoverySettings.tsx +++ b/src/components/views/settings/discovery/DiscoverySettings.tsx @@ -19,8 +19,6 @@ import { SERVICE_TYPES, ThreepidMedium } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { Alert } from "@vector-im/compound-web"; -import DiscoveryEmailAddresses from "../discovery/EmailAddresses"; -import DiscoveryPhoneNumbers from "../discovery/PhoneNumbers"; import { getThreepidsWithBindStatus } from "../../../../boundThreepids"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; import { ThirdPartyIdentifier } from "../../../../AddThreepid"; @@ -36,6 +34,7 @@ import { abbreviateUrl } from "../../../../utils/UrlUtils"; import { useDispatcher } from "../../../../hooks/useDispatcher"; import defaultDispatcher from "../../../../dispatcher/dispatcher"; import { ActionPayload } from "../../../../dispatcher/payloads"; +import { AddRemoveThreepids } from "../AddRemoveThreepids"; type RequiredPolicyInfo = | { @@ -56,9 +55,9 @@ type RequiredPolicyInfo = export const DiscoverySettings: React.FC = () => { const client = useMatrixClientContext(); + const [isLoadingThreepids, setIsLoadingThreepids] = useState(true); const [emails, setEmails] = useState([]); const [phoneNumbers, setPhoneNumbers] = useState([]); - const [loadingState, setLoadingState] = useState<"loading" | "loaded" | "error">("loading"); const [idServerName, setIdServerName] = useState(abbreviateUrl(client.getIdentityServerUrl())); const [canMake3pidChanges, setCanMake3pidChanges] = useState(false); @@ -71,9 +70,11 @@ export const DiscoverySettings: React.FC = () => { const [hasTerms, setHasTerms] = useState(false); const getThreepidState = useCallback(async () => { + setIsLoadingThreepids(true); const threepids = await getThreepidsWithBindStatus(client); setEmails(threepids.filter((a) => a.medium === ThreepidMedium.Email)); setPhoneNumbers(threepids.filter((a) => a.medium === ThreepidMedium.Phone)); + setIsLoadingThreepids(false); }, [client]); useDispatcher( @@ -133,11 +134,7 @@ export const DiscoverySettings: React.FC = () => { ); logger.warn(e); } - - setLoadingState("loaded"); - } catch (e) { - setLoadingState("error"); - } + } catch (e) {} })(); }, [client, getThreepidState]); @@ -163,23 +160,44 @@ export const DiscoverySettings: React.FC = () => { ); } - const threepidSection = idServerName ? ( - <> - - - - ) : null; + let threepidSection; + if (idServerName) { + threepidSection = ( + <> + + + + + + + + ); + } return ( - + {threepidSection} {/* has its own heading as it includes the current identity server */} diff --git a/src/components/views/settings/discovery/EmailAddresses.tsx b/src/components/views/settings/discovery/EmailAddresses.tsx deleted file mode 100644 index 58f1eaac7f..0000000000 --- a/src/components/views/settings/discovery/EmailAddresses.tsx +++ /dev/null @@ -1,251 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixError } from "matrix-js-sdk/src/matrix"; - -import { _t, UserFriendlyError } from "../../../../languageHandler"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import Modal from "../../../../Modal"; -import AddThreepid, { Binding, ThirdPartyIdentifier } from "../../../../AddThreepid"; -import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog"; -import SettingsSubsection from "../shared/SettingsSubsection"; -import InlineSpinner from "../../elements/InlineSpinner"; -import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; - -/* -TODO: Improve the UX for everything in here. -It's very much placeholder, but it gets the job done. The old way of handling -email addresses in user settings was to use dialogs to communicate state, however -due to our dialog system overriding dialogs (causing unmounts) this creates problems -for a sane UX. For instance, the user could easily end up entering an email address -and receive a dialog to verify the address, which then causes the component here -to forget what it was doing and ultimately fail. Dialogs are still used in some -places to communicate errors - these should be replaced with inline validation when -that is available. -*/ - -/* -TODO: Reduce all the copying between account vs. discovery components. -*/ - -interface IEmailAddressProps { - email: ThirdPartyIdentifier; - disabled?: boolean; -} - -interface IEmailAddressState { - verifying: boolean; - addTask: AddThreepid | null; - continueDisabled: boolean; - bound?: boolean; -} - -export class EmailAddress extends React.Component { - public constructor(props: IEmailAddressProps) { - super(props); - - const { bound } = props.email; - - this.state = { - verifying: false, - addTask: null, - continueDisabled: false, - bound, - }; - } - - public componentDidUpdate(prevProps: Readonly): void { - if (this.props.email !== prevProps.email) { - const { bound } = this.props.email; - this.setState({ bound }); - } - } - - private async changeBinding({ bind, label, errorTitle }: Binding): Promise { - const { medium, address } = this.props.email; - - try { - if (bind) { - const task = new AddThreepid(MatrixClientPeg.safeGet()); - this.setState({ - verifying: true, - continueDisabled: true, - addTask: task, - }); - await task.bindEmailAddress(address); - this.setState({ - continueDisabled: false, - }); - } else { - await MatrixClientPeg.safeGet().unbindThreePid(medium, address); - } - this.setState({ bound: bind }); - } catch (err) { - logger.error(`changeBinding: Unable to ${label} email address ${address}`, err); - this.setState({ - verifying: false, - continueDisabled: false, - addTask: null, - }); - Modal.createDialog(ErrorDialog, { - title: errorTitle, - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - } - } - - private onRevokeClick = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - this.changeBinding({ - bind: false, - label: "revoke", - errorTitle: _t("settings|general|error_revoke_email_discovery"), - }); - }; - - private onShareClick = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - this.changeBinding({ - bind: true, - label: "share", - errorTitle: _t("settings|general|error_share_email_discovery"), - }); - }; - - private onContinueClick = async (e: ButtonEvent): Promise => { - e.stopPropagation(); - e.preventDefault(); - - // Prevent the continue button from being pressed multiple times while we're working - this.setState({ continueDisabled: true }); - try { - await this.state.addTask?.checkEmailLinkClicked(); - this.setState({ - addTask: null, - verifying: false, - }); - } catch (err) { - logger.error(`Unable to verify email address:`, err); - - let underlyingError = err; - if (err instanceof UserFriendlyError) { - underlyingError = err.cause; - } - - if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") { - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|email_not_verified"), - description: _t("settings|general|email_verification_instructions"), - }); - } else { - logger.error("Unable to verify email address: " + err); - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_email_verification"), - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - } - } finally { - // Re-enable the continue button so the user can retry - this.setState({ continueDisabled: false }); - } - }; - - public render(): React.ReactNode { - const { address } = this.props.email; - const { verifying, bound } = this.state; - - let status; - if (verifying) { - status = ( - - {_t("settings|general|discovery_email_verification_instructions")} - - {_t("action|complete")} - - - ); - } else if (bound) { - status = ( - - {_t("action|revoke")} - - ); - } else { - status = ( - - {_t("action|share")} - - ); - } - - return ( -
- {address} - {status} -
- ); - } -} -interface IProps { - emails: ThirdPartyIdentifier[]; - isLoading?: boolean; - disabled?: boolean; -} - -export default class EmailAddresses extends React.Component { - public render(): React.ReactNode { - let content; - if (this.props.isLoading) { - content = ; - } else if (this.props.emails.length > 0) { - content = this.props.emails.map((e) => { - return ; - }); - } - - const hasEmails = !!this.props.emails.length; - - return ( - - {content} - - ); - } -} diff --git a/src/components/views/settings/discovery/PhoneNumbers.tsx b/src/components/views/settings/discovery/PhoneNumbers.tsx deleted file mode 100644 index a462273314..0000000000 --- a/src/components/views/settings/discovery/PhoneNumbers.tsx +++ /dev/null @@ -1,263 +0,0 @@ -/* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixError } from "matrix-js-sdk/src/matrix"; - -import { _t, UserFriendlyError } from "../../../../languageHandler"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import Modal from "../../../../Modal"; -import AddThreepid, { Binding, ThirdPartyIdentifier } from "../../../../AddThreepid"; -import ErrorDialog, { extractErrorMessageFromError } from "../../dialogs/ErrorDialog"; -import Field from "../../elements/Field"; -import SettingsSubsection from "../shared/SettingsSubsection"; -import InlineSpinner from "../../elements/InlineSpinner"; -import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; - -/* -TODO: Improve the UX for everything in here. -This is a copy/paste of EmailAddresses, mostly. - */ - -// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic - -interface IPhoneNumberProps { - msisdn: ThirdPartyIdentifier; - disabled?: boolean; -} - -interface IPhoneNumberState { - verifying: boolean; - verificationCode: string; - addTask: AddThreepid | null; - continueDisabled: boolean; - bound?: boolean; - verifyError: string | null; -} - -export class PhoneNumber extends React.Component { - public constructor(props: IPhoneNumberProps) { - super(props); - - const { bound } = props.msisdn; - - this.state = { - verifying: false, - verificationCode: "", - addTask: null, - continueDisabled: false, - bound, - verifyError: null, - }; - } - - public componentDidUpdate(prevProps: Readonly): void { - if (this.props.msisdn !== prevProps.msisdn) { - const { bound } = this.props.msisdn; - this.setState({ bound }); - } - } - - private async changeBinding({ bind, label, errorTitle }: Binding): Promise { - const { medium, address } = this.props.msisdn; - - try { - if (bind) { - const task = new AddThreepid(MatrixClientPeg.safeGet()); - this.setState({ - verifying: true, - continueDisabled: true, - addTask: task, - }); - // XXX: Sydent will accept a number without country code if you add - // a leading plus sign to a number in E.164 format (which the 3PID - // address is), but this goes against the spec. - // See https://github.com/matrix-org/matrix-doc/issues/2222 - // @ts-ignore - await task.bindMsisdn(null, `+${address}`); - this.setState({ - continueDisabled: false, - }); - } else { - await MatrixClientPeg.safeGet().unbindThreePid(medium, address); - } - this.setState({ bound: bind }); - } catch (err) { - logger.error(`changeBinding: Unable to ${label} phone number ${address}`, err); - this.setState({ - verifying: false, - continueDisabled: false, - addTask: null, - }); - Modal.createDialog(ErrorDialog, { - title: errorTitle, - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - } - } - - private onRevokeClick = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - this.changeBinding({ - bind: false, - label: "revoke", - errorTitle: _t("settings|general|error_revoke_msisdn_discovery"), - }); - }; - - private onShareClick = (e: ButtonEvent): void => { - e.stopPropagation(); - e.preventDefault(); - this.changeBinding({ - bind: true, - label: "share", - errorTitle: _t("settings|general|error_share_msisdn_discovery"), - }); - }; - - private onVerificationCodeChange = (e: React.ChangeEvent): void => { - this.setState({ - verificationCode: e.target.value, - }); - }; - - private onContinueClick = async (e: ButtonEvent | React.FormEvent): Promise => { - e.stopPropagation(); - e.preventDefault(); - - this.setState({ continueDisabled: true }); - const token = this.state.verificationCode; - try { - await this.state.addTask?.haveMsisdnToken(token); - this.setState({ - addTask: null, - continueDisabled: false, - verifying: false, - verifyError: null, - verificationCode: "", - }); - } catch (err) { - logger.error("Unable to verify phone number:", err); - - let underlyingError = err; - if (err instanceof UserFriendlyError) { - underlyingError = err.cause; - } - - this.setState({ continueDisabled: false }); - if (underlyingError instanceof MatrixError && underlyingError.errcode !== "M_THREEPID_AUTH_FAILED") { - Modal.createDialog(ErrorDialog, { - title: _t("settings|general|error_msisdn_verification"), - description: extractErrorMessageFromError(err, _t("invite|failed_generic")), - }); - } else { - this.setState({ verifyError: _t("settings|general|incorrect_msisdn_verification") }); - } - } - }; - - public render(): React.ReactNode { - const { address } = this.props.msisdn; - const { verifying, bound } = this.state; - - let status; - if (verifying) { - status = ( - - - {_t("settings|general|msisdn_verification_instructions")} -
- {this.state.verifyError} -
-
- - -
- ); - } else if (bound) { - status = ( - - {_t("action|revoke")} - - ); - } else { - status = ( - - {_t("action|share")} - - ); - } - - return ( -
- +{address} - {status} -
- ); - } -} - -interface IProps { - msisdns: ThirdPartyIdentifier[]; - isLoading?: boolean; - disabled?: boolean; -} - -export default class PhoneNumbers extends React.Component { - public render(): React.ReactNode { - let content; - if (this.props.isLoading) { - content = ; - } else if (this.props.msisdns.length > 0) { - content = this.props.msisdns.map((e) => { - return ; - }); - } - - const description = (!content && _t("settings|general|discovery_msisdn_empty")) || undefined; - - return ( - - {content} - - ); - } -} diff --git a/src/components/views/settings/notifications/NotificationPusherSettings.tsx b/src/components/views/settings/notifications/NotificationPusherSettings.tsx index 7ba8021818..9da85e0b22 100644 --- a/src/components/views/settings/notifications/NotificationPusherSettings.tsx +++ b/src/components/views/settings/notifications/NotificationPusherSettings.tsx @@ -37,7 +37,7 @@ function generalTabButton(content: string): JSX.Element { onClick={() => { dispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: UserTab.General, + initialTabId: UserTab.Account, }); }} > diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx index cfe6b3ccd3..1f44f06323 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx @@ -36,7 +36,7 @@ interface IProps { export default class BridgeSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private renderBridgeCard(event: MatrixEvent, room: Room | null): ReactNode { const content = event.getContent(); diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 828056dfec..a5954945a6 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -42,7 +42,7 @@ interface IState { export default class GeneralRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public context!: ContextType; + public declare context: ContextType; public constructor(props: IProps, context: ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx index 1c536ed6d6..1b0dbfdf1a 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx @@ -50,7 +50,7 @@ export default class NotificationsSettingsTab extends React.Component(); public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 2197cad3df..3105ba961a 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -89,7 +89,7 @@ interface IBannedUserProps { export class BannedUser extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; private onUnbanClick = (): void => { this.context.unban(this.props.member.roomId, this.props.member.userId).catch((err) => { @@ -137,7 +137,7 @@ interface IProps { export default class RolesRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public componentDidMount(): void { this.context.on(RoomStateEvent.Update, this.onRoomStateUpdate); diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 5b991be0d5..d6d661d896 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -67,7 +67,7 @@ interface IState { export default class SecurityRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public constructor(props: IProps, context: React.ContextType) { super(props, context); diff --git a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx new file mode 100644 index 0000000000..a08005c006 --- /dev/null +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -0,0 +1,210 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback, useContext, useEffect } from "react"; +import { HTTPError } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { UserFriendlyError, _t } from "../../../../../languageHandler"; +import UserProfileSettings from "../../UserProfileSettings"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; +import Modal from "../../../../../Modal"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/ErrorDialog"; +import ChangePassword from "../../ChangePassword"; +import SettingsTab from "../SettingsTab"; +import { SettingsSection } from "../../shared/SettingsSection"; +import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import { SDKContext } from "../../../../../contexts/SDKContext"; +import UserPersonalInfoSettings from "../../UserPersonalInfoSettings"; +import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; + +interface IProps { + closeSettingsFn: () => void; +} + +interface AccountSectionProps { + canChangePassword: boolean; + onPasswordChangeError: (e: Error) => void; + onPasswordChanged: () => void; +} + +const AccountSection: React.FC = ({ + canChangePassword, + onPasswordChangeError, + onPasswordChanged, +}) => { + if (!canChangePassword) return <>; + + return ( + <> + + {_t("settings|general|password_change_section")} + + + + ); +}; + +interface ManagementSectionProps { + onDeactivateClicked: () => void; +} + +const ManagementSection: React.FC = ({ onDeactivateClicked }) => { + return ( + + + + {_t("settings|general|deactivate_section")} + + + + ); +}; + +const AccountUserSettingsTab: React.FC = ({ closeSettingsFn }) => { + const [externalAccountManagementUrl, setExternalAccountManagementUrl] = React.useState(); + const [canMake3pidChanges, setCanMake3pidChanges] = React.useState(false); + const [canSetDisplayName, setCanSetDisplayName] = React.useState(false); + const [canSetAvatar, setCanSetAvatar] = React.useState(false); + const [canChangePassword, setCanChangePassword] = React.useState(false); + + const cli = useMatrixClientContext(); + const sdkContext = useContext(SDKContext); + + useEffect(() => { + (async () => { + const capabilities = (await cli.getCapabilities()) ?? {}; + const changePasswordCap = capabilities["m.change_password"]; + + // You can change your password so long as the capability isn't explicitly disabled. The implicit + // behaviour is you can change your password when the capability is missing or has not-false as + // the enabled flag value. + const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false; + + await sdkContext.oidcClientStore.readyPromise; // wait for the store to be ready + const externalAccountManagementUrl = sdkContext.oidcClientStore.accountManagementEndpoint; + // https://spec.matrix.org/v1.7/client-server-api/#m3pid_changes-capability + // We support as far back as v1.1 which doesn't have m.3pid_changes + // so the behaviour for when it is missing has to be assume true + const canMake3pidChanges = + !capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true; + + const canSetDisplayName = + !capabilities["m.set_displayname"] || capabilities["m.set_displayname"].enabled === true; + const canSetAvatar = !capabilities["m.set_avatar_url"] || capabilities["m.set_avatar_url"].enabled === true; + + setCanMake3pidChanges(canMake3pidChanges); + setCanSetDisplayName(canSetDisplayName); + setCanSetAvatar(canSetAvatar); + setExternalAccountManagementUrl(externalAccountManagementUrl); + setCanChangePassword(canChangePassword); + })(); + }, [cli, sdkContext.oidcClientStore]); + + const onPasswordChangeError = useCallback((err: Error): void => { + logger.error("Failed to change password: " + err); + + let underlyingError = err; + if (err instanceof UserFriendlyError && err.cause instanceof Error) { + underlyingError = err.cause; + } + + const errorMessage = extractErrorMessageFromError( + err, + _t("settings|general|error_password_change_unknown", { + stringifiedError: String(err), + }), + ); + + let errorMessageToDisplay = errorMessage; + if (underlyingError instanceof HTTPError && underlyingError.httpStatus === 403) { + errorMessageToDisplay = _t("settings|general|error_password_change_403"); + } else if (underlyingError instanceof HTTPError) { + errorMessageToDisplay = _t("settings|general|error_password_change_http", { + errorMessage, + httpStatus: underlyingError.httpStatus, + }); + } + + // TODO: Figure out a design that doesn't involve replacing the current dialog + Modal.createDialog(ErrorDialog, { + title: _t("settings|general|error_password_change_title"), + description: errorMessageToDisplay, + }); + }, []); + + const onPasswordChanged = useCallback((): void => { + const description = _t("settings|general|password_change_success"); + // TODO: Figure out a design that doesn't involve replacing the current dialog + Modal.createDialog(ErrorDialog, { + title: _t("common|success"), + description, + }); + }, []); + + const onDeactivateClicked = useCallback((): void => { + Modal.createDialog(DeactivateAccountDialog, { + onFinished: (success) => { + if (success) closeSettingsFn(); + }, + }); + }, [closeSettingsFn]); + + let accountManagementSection: JSX.Element | undefined; + const isAccountManagedExternally = Boolean(externalAccountManagementUrl); + if (SettingsStore.getValue(UIFeature.Deactivate) && !isAccountManagedExternally) { + accountManagementSection = ; + } + + return ( + + + + + + + {accountManagementSection} + + ); +}; + +export default AccountUserSettingsTab; diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index e351e46a91..f57ca58493 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -33,7 +33,6 @@ import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; import { SDKContext } from "../../../../../contexts/SDKContext"; -import TchapUIFeature from '../../../../../../../../src/tchap/util/TchapUIFeature'; // :TCHAP: hide-discovery-email-phone-settings import UserPersonalInfoSettings from "../../UserPersonalInfoSettings"; interface IProps { @@ -202,7 +201,9 @@ export default class GeneralUserSettingsTab extends React.Component - + {/* :TCHAP: hide-discovery-email-phone-settings-updated */} + {/* */} + {/* end :TCHAP: */} {this.renderAccountSection()} {accountManagementSection} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 2bf2c0f604..867b986c82 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -40,10 +40,10 @@ interface IState { export default class HelpUserSettingsTab extends React.Component { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { appVersion: null, diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 439cc2122f..0e213de969 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -70,12 +70,8 @@ const LanguageSection: React.FC = () => { return (
{_t("settings|general|application_language")} - -
+ +
{_t("settings|general|application_language_reload_hint")}
diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 758a48e1c5..c3d21205ff 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -59,10 +59,10 @@ const mapDeviceKindToHandlerValue = (deviceKind: MediaDeviceKindEnum): string | export default class VoiceUserSettingsTab extends React.Component<{}, IState> { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; - public constructor(props: {}) { - super(props); + public constructor(props: {}, context: React.ContextType) { + super(props, context); this.state = { mediaDevices: null, diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 63024c458b..cb22c7f864 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -45,7 +45,7 @@ const QuickSettingsButton: React.FC<{ useSettingValue>("Spaces.enabledMetaSpaces"); const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); - const developerModeEnabled = useSettingValue("developerMode"); + const developerModeEnabled = useSettingValue("developerMode"); let contextMenu: JSX.Element | undefined; if (menuDisplayed && handle.current) { diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 253e7bf881..fc9511f52a 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -38,7 +38,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; @@ -198,8 +197,6 @@ interface IItemState { } export class SpaceItem extends React.PureComponent { - public static contextType = MatrixClientContext; - private buttonRef = createRef(); public constructor(props: IItemProps) { diff --git a/src/components/views/toasts/GenericExpiringToast.tsx b/src/components/views/toasts/GenericExpiringToast.tsx index 150aaff400..fea826dfa3 100644 --- a/src/components/views/toasts/GenericExpiringToast.tsx +++ b/src/components/views/toasts/GenericExpiringToast.tsx @@ -31,9 +31,9 @@ const SECOND = 1000; const GenericExpiringToast: React.FC = ({ description, - acceptLabel, + primaryLabel, dismissLabel, - onAccept, + onPrimaryClick, onDismiss, toastKey, numSeconds, @@ -52,10 +52,10 @@ const GenericExpiringToast: React.FC = ({ return ( ); }; diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index c40eaab8e8..70c6c1a7dd 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -14,31 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; +import React, { ComponentType, ReactNode } from "react"; +import { Button } from "@vector-im/compound-web"; -import AccessibleButton from "../elements/AccessibleButton"; import { XOR } from "../../../@types/common"; export interface IProps { description: ReactNode; detail?: ReactNode; - acceptLabel: string; + primaryLabel: string; + PrimaryIcon?: ComponentType>; - onAccept(): void; + onPrimaryClick(): void; } interface IPropsExtended extends IProps { - rejectLabel: string; - onReject(): void; + secondaryLabel: string; + SecondaryIcon?: ComponentType>; + destructive?: "primary" | "secondary"; + onSecondaryClick(): void; } const GenericToast: React.FC> = ({ description, detail, - acceptLabel, - rejectLabel, - onAccept, - onReject, + primaryLabel, + PrimaryIcon, + secondaryLabel, + SecondaryIcon, + destructive, + onPrimaryClick, + onSecondaryClick, }) => { const detailContent = detail ?
{detail}
: null; @@ -49,14 +55,24 @@ const GenericToast: React.FC> = ({ {detailContent}
- {onReject && rejectLabel && ( - - {rejectLabel} - + {onSecondaryClick && secondaryLabel && ( + )} - - {acceptLabel} - +
); diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 283473bf51..c7ac14f804 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -185,14 +185,14 @@ export default class VerificationRequestToast extends React.PureComponent ); } diff --git a/src/components/views/voip/CallDuration.tsx b/src/components/views/voip/CallDuration.tsx index 03e26a819f..287b059c96 100644 --- a/src/components/views/voip/CallDuration.tsx +++ b/src/components/views/voip/CallDuration.tsx @@ -15,8 +15,7 @@ limitations under the License. */ import React, { FC, useState, useEffect, memo } from "react"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; import { formatPreciseDuration } from "../../../DateUtils"; diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index dc7e9e2464..34ef11f5df 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -193,7 +193,7 @@ export default class VideoFeed extends React.PureComponent { }); let micIcon; - if (feed.purpose !== SDPStreamMetadataPurpose.Screenshare && !primary && !pipMode) { + if (feed.purpose !== SDPStreamMetadataPurpose.Screenshare && !pipMode) { micIcon =
; } diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index 25e8489658..dc3846d905 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MatrixClient, ResizeMethod } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, parseErrorResponse, ResizeMethod } from "matrix-js-sdk/src/matrix"; import { MediaEventContent } from "matrix-js-sdk/src/types"; import { Optional } from "matrix-events-sdk"; @@ -144,12 +144,16 @@ export class Media { * Downloads the source media. * @returns {Promise} Resolves to the server's response for chaining. */ - public downloadSource(): Promise { + public async downloadSource(): Promise { const src = this.srcHttp; if (!src) { throw new UserFriendlyError("error|download_media"); } - return fetch(src); + const res = await fetch(src); + if (!res.ok) { + throw parseErrorResponse(res, await res.text()); + } + return res; } } diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index c3eccf718c..70f23c7573 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -242,9 +242,11 @@ function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: ( if ((n as Element).hasAttribute("data-mx-maths")) { const delims = SdkConfig.get().latex_maths_delims; const delimLeft = - n.nodeName === "SPAN" ? delims?.inline?.left ?? "\\(" : delims?.display?.left ?? "\\["; + n.nodeName === "SPAN" ? (delims?.inline?.left ?? "\\(") : (delims?.display?.left ?? "\\["); const delimRight = - n.nodeName === "SPAN" ? delims?.inline?.right ?? "\\)" : delims?.display?.right ?? "\\]"; + n.nodeName === "SPAN" + ? (delims?.inline?.right ?? "\\)") + : (delims?.display?.right ?? "\\]"); const tex = (n as Element).getAttribute("data-mx-maths"); return pc.plainWithEmoji(`${delimLeft}${tex}${delimRight}`); diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 8391183a65..14cb413a14 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -37,7 +37,7 @@ export function mdSerialize(model: EditorModel): string { case Type.AtRoomPill: return html + part.text; case Type.RoomPill: { - const url = makeGenericPermalink(part.resourceId); + const url = makeGenericPermalink(part.resourceId, true); // Escape square brackets and backslashes // Here we use the resourceId for compatibility with non-rich text clients // See https://github.com/vector-im/element-web/issues/16660 @@ -45,7 +45,7 @@ export function mdSerialize(model: EditorModel): string { return html + `[${title}](${url})`; } case Type.UserPill: { - const url = makeGenericPermalink(part.resourceId); + const url = makeGenericPermalink(part.resourceId, true); // Escape square brackets and backslashes; convert newlines to HTML const title = part.text.replace(/[[\\\]]/g, (c) => "\\" + c).replace(/\n/g, "
"); return html + `[${title}](${url})`; diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index b065855626..8f9a2b1768 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -22,7 +22,7 @@ import { useFeatureEnabled } from "../useSettings"; import SdkConfig from "../../SdkConfig"; import { useEventEmitter, useEventEmitterState } from "../useEventEmitter"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; -import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard"; +import { useWidgets } from "../../utils/WidgetUtils"; import { WidgetType } from "../../widgets/WidgetType"; import { useCall, useConnectionState, useParticipantCount } from "../useCall"; import { useRoomMemberCount } from "../useRoomMembers"; diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts index d59910d503..ad25f61465 100644 --- a/src/hooks/useAccountData.ts +++ b/src/hooks/useAccountData.ts @@ -26,9 +26,9 @@ export const useAccountData = (cli: MatrixClient, eventType: strin const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType))); const handler = useCallback( - (event) => { + (event: MatrixEvent) => { if (event.getType() !== eventType) return; - setValue(event.getContent()); + setValue(event.getContent()); }, [eventType], ); diff --git a/src/hooks/usePermalinkTargetRoom.ts b/src/hooks/usePermalinkTargetRoom.ts index d6d1dd0959..2413c93a3b 100644 --- a/src/hooks/usePermalinkTargetRoom.ts +++ b/src/hooks/usePermalinkTargetRoom.ts @@ -62,9 +62,9 @@ const findRoom = (roomIdOrAlias: string): Room | null => { const client = MatrixClientPeg.safeGet(); return roomIdOrAlias[0] === "#" - ? client.getRooms().find((r) => { + ? (client.getRooms().find((r) => { return r.getCanonicalAlias() === roomIdOrAlias || r.getAltAliases().includes(roomIdOrAlias); - }) ?? null + }) ?? null) : client.getRoom(roomIdOrAlias); }; diff --git a/src/hooks/useUserOnboardingContext.ts b/src/hooks/useUserOnboardingContext.ts index ab6e213069..38cc8d7347 100644 --- a/src/hooks/useUserOnboardingContext.ts +++ b/src/hooks/useUserOnboardingContext.ts @@ -18,15 +18,17 @@ import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Notifier } from "../Notifier"; +import { Notifier, NotifierEvent } from "../Notifier"; import DMRoomMap from "../utils/DMRoomMap"; import { useMatrixClientContext } from "../contexts/MatrixClientContext"; +import { useSettingValue } from "./useSettings"; +import { useEventEmitter } from "./useEventEmitter"; export interface UserOnboardingContext { hasAvatar: boolean; hasDevices: boolean; hasDmRooms: boolean; - hasNotificationsEnabled: boolean; + showNotificationsPrompt: boolean; hasSecureStorage: boolean, // :TCHAP: onboarding-add-secure-backup hasCheckedUserGuide: boolean, // :TCHAP: onboarding-add-tchap-guide } @@ -84,6 +86,18 @@ function useUserOnboardingContextValue(defaultValue: T, callback: (cli: Matri return value; } +function useShowNotificationsPrompt(): boolean { + const [value, setValue] = useState(Notifier.shouldShowPrompt()); + useEventEmitter(Notifier, NotifierEvent.NotificationHiddenChange, () => { + setValue(Notifier.shouldShowPrompt()); + }); + const setting = useSettingValue("notificationsEnabled"); + useEffect(() => { + setValue(Notifier.shouldShowPrompt()); + }, [setting]); + return value; +} + export function useUserOnboardingContext(): UserOnboardingContext { const hasAvatar = useUserOnboardingContextValue(false, async (cli) => { const profile = await cli.getProfileInfo(cli.getUserId()!); @@ -98,9 +112,6 @@ export function useUserOnboardingContext(): UserOnboardingContext { const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {}; return Boolean(Object.keys(dmRooms).length); }); - const hasNotificationsEnabled = useUserOnboardingContextValue(false, async () => { - return Notifier.isPossible(); - }); /** :TCHAP: onboarding-add-secure-backup */ const hasSecureStorage = useUserOnboardingContextValue(false, async (cli) => { const hasKey = await cli.secretStorage.hasKey() @@ -115,8 +126,10 @@ export function useUserOnboardingContext(): UserOnboardingContext { }); /** end :TCHAP: onboarding-add-tchap-guide */ + const showNotificationsPrompt = useShowNotificationsPrompt(); + return useMemo( - () => ({ hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled, hasSecureStorage, hasCheckedUserGuide }), - [hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled, hasSecureStorage, hasCheckedUserGuide], + () => ({ hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt, hasSecureStorage, hasCheckedUserGuide }), + [hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt, hasSecureStorage, hasCheckedUserGuide], ); } diff --git a/src/hooks/useUserOnboardingTasks.ts b/src/hooks/useUserOnboardingTasks.ts index af7ed08073..4948f68e24 100644 --- a/src/hooks/useUserOnboardingTasks.ts +++ b/src/hooks/useUserOnboardingTasks.ts @@ -129,7 +129,7 @@ const tasks: UserOnboardingTask[] = [ PosthogTrackers.trackInteraction("WebUserOnboardingTaskSetupProfile", ev); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: UserTab.General, + initialTabId: UserTab.Account, }); }, }, @@ -138,14 +138,18 @@ const tasks: UserOnboardingTask[] = [ id: "permission-notifications", title: _t("onboarding|enable_notifications"), description: _t("onboarding|enable_notifications_description"), - completed: (ctx: UserOnboardingContext) => ctx.hasNotificationsEnabled, + completed: (ctx: UserOnboardingContext) => !ctx.showNotificationsPrompt, action: { label: _t("onboarding|enable_notifications_action"), onClick: (ev: ButtonEvent) => { PosthogTrackers.trackInteraction("WebUserOnboardingTaskEnableNotifications", ev); - Notifier.setEnabled(true); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Notifications, + }); + Notifier.setPromptHidden(true); }, - hideOnComplete: true, + hideOnComplete: !Notifier.isPossible(), }, },*/ { /** :TCHAP: onboarding-add-tchap-guide */ diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 25353f30df..879d381c4b 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1789,12 +1789,9 @@ }, "right_panel": { "add_integrations": "Přidat widgety, propojení a boty", - "edit_integrations": "Upravujte widgety, propojení a boty", "export_chat_button": "Exportovat chat", "files_button": "Soubory", "pinned_messages": { - "empty": "Zatím není nic připnuto", - "explainer": "Pokud máte oprávnění, otevřete nabídku na libovolné zprávě a výběrem možnosti Připnout je sem vložte.", "limits": { "other": "Můžete připnout až %(count)s widgetů" }, @@ -2455,7 +2452,6 @@ "error_share_msisdn_discovery": "Nepovedlo se nasdílet telefonní číslo", "identity_server_no_token": "Nebyl nalezen žádný přístupový token identity", "identity_server_not_set": "Server identit není nastaven", - "incorrect_msisdn_verification": "Nesprávný ověřovací kód", "language_section": "Jazyk a region", "msisdn_in_use": "Toto telefonní číslo je již používáno", "msisdn_label": "Telefonní číslo", diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 5097bb2440..26abbe22f2 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1775,12 +1775,9 @@ }, "right_panel": { "add_integrations": "Widgets, Brücken und Bots hinzufügen", - "edit_integrations": "Widgets, Brücken und Bots bearbeiten", "export_chat_button": "Unterhaltung exportieren", "files_button": "Dateien", "pinned_messages": { - "empty": "Es ist nichts angepinnt. Noch nicht.", - "explainer": "Sofern du die Berechtigung hast, öffne das Menü einer Nachricht und wähle Anheften, ⁣ um sie hier aufzubewahren.", "limits": { "other": "Du kannst nur %(count)s Widgets anheften" }, @@ -2434,7 +2431,6 @@ "error_share_msisdn_discovery": "Teilen der Telefonnummer nicht möglich", "identity_server_no_token": "Kein Identitäts-Zugangs-Token gefunden", "identity_server_not_set": "Kein Identitäts-Server festgelegt", - "incorrect_msisdn_verification": "Falscher Verifizierungscode", "language_section": "Sprache und Region", "msisdn_in_use": "Diese Telefonnummer wird bereits verwendet", "msisdn_label": "Telefonnummer", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 2e75cc9689..e41d10084b 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -1424,12 +1424,9 @@ }, "right_panel": { "add_integrations": "Προσθήκη μικροεφαρμογών, γεφυρών & bots", - "edit_integrations": "Επεξεργασία μικροεφαρμογών, γεφυρών & bots", "export_chat_button": "Εξαγωγή συνομιλίας", "files_button": "Αρχεία", "pinned_messages": { - "empty": "Δεν έχει καρφιτσωθεί κάτι ακόμα", - "explainer": "Εάν έχετε δικαιώματα, ανοίξτε το μενού σε οποιοδήποτε μήνυμα και επιλέξτε Καρφίτσωμα για να τα κολλήσετε εδώ.", "limits": { "other": "Μπορείτε να καρφιτσώσετε μόνο έως %(count)s μικρεοεφαρμογές" }, @@ -1966,7 +1963,6 @@ "error_revoke_msisdn_discovery": "Αδυναμία ανάκληση της κοινής χρήσης για τον αριθμό τηλεφώνου", "error_share_email_discovery": "Δεν είναι δυνατή η κοινή χρήση της διεύθυνσης email", "error_share_msisdn_discovery": "Αδυναμία κοινής χρήσης του αριθμού τηλεφώνου", - "incorrect_msisdn_verification": "Λανθασμένος κωδικός επαλήθευσης", "language_section": "Γλώσσα και περιοχή", "msisdn_in_use": "Αυτός ο αριθμός τηλεφώνου είναι ήδη σε χρήση", "msisdn_label": "Αριθμός Τηλεφώνου", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4d06f7e07b..aae7b090c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -474,6 +474,7 @@ "encrypted": "Encrypted", "encryption_enabled": "Encryption enabled", "error": "Error", + "extensions": "Extensions", "faq": "FAQ", "favourites": "Favourites", "feedback": "Feedback", @@ -1657,8 +1658,8 @@ "download_brand_desktop": "Download %(brand)s Desktop", "download_f_droid": "Get it on F-Droid", "download_google_play": "Get it on Google Play", - "enable_notifications": "Turn on notifications", - "enable_notifications_action": "Enable notifications", + "enable_notifications": "Turn on desktop notifications", + "enable_notifications_action": "Open settings", "enable_notifications_description": "Don’t miss a reply or important message", "explore_rooms": "Explore Public Rooms", "find_community_members": "Find and invite your community members", @@ -1835,21 +1836,34 @@ "restore_failed_error": "Unable to restore backup" }, "right_panel": { - "add_integrations": "Add widgets, bridges & bots", + "add_integrations": "Add extensions", "add_topic": "Add topic", - "edit_integrations": "Edit widgets, bridges & bots", "export_chat_button": "Export chat", + "extensions_empty_description": "Select “%(addIntegrations)s” to browse and add extensions to this room", + "extensions_empty_title": "Boost productivity with more tools, widgets and bots", "files_button": "Files", "info": "Info", "pinned_messages": { - "empty": "Nothing pinned, yet", - "explainer": "If you have permissions, open the menu on any message and select Pin to stick them here.", + "empty_description": "Select a message and choose “%(pinAction)s” to it include here.", + "empty_title": "Pin important messages so that they can be easily discovered", + "header": { + "one": "1 Pinned message", + "other": "%(count)s Pinned messages", + "zero": "Pinned message" + }, "limits": { "other": "You can only pin up to %(count)s widgets" }, - "title": "Pinned messages" + "menu": "Open menu", + "title": "Pinned messages", + "unpin_all": { + "button": "Unpin all messages", + "content": "Make sure that you really want to remove all pinned messages. This action can’t be undone.", + "title": "Unpin all messages?" + }, + "view": "View in timeline" }, - "pinned_messages_button": "Pinned", + "pinned_messages_button": "Pinned messages", "poll": { "active_heading": "Active polls", "empty_active": "There are no active polls in this room", @@ -1874,7 +1888,7 @@ "view_in_timeline": "View poll in timeline", "view_poll": "View poll" }, - "polls_button": "Poll history", + "polls_button": "Polls", "room_summary_card": { "title": "Room info" }, @@ -1994,7 +2008,7 @@ "invite_reject_ignore": "Reject & Ignore user", "invite_sent_to_email": "This invite was sent to %(email)s", "invite_sent_to_email_room": "This invite to %(roomName)s was sent to %(email)s", - "invite_subtitle": " invited you", + "invite_subtitle": "Invited by ", "invite_this_room": "Invite to this room", "invite_title": "Do you want to join %(roomName)s?", "inviter_unknown": "Unknown", @@ -2425,6 +2439,10 @@ } }, "settings": { + "account": { + "dialog_title": "Settings: Account", + "title": "Account" + }, "all_rooms_home": "Show all rooms in Home", "all_rooms_home_description": "All rooms you're in will appear in Home.", "always_show_message_timestamps": "Always show message timestamps", @@ -2500,7 +2518,6 @@ "deactivate_confirm_erase_label": "Hide my messages from new joiners", "deactivate_section": "Deactivate Account", "deactivate_warning": "Deactivating your account is a permanent action — be careful!", - "dialog_title": "Settings: General", "discovery_email_empty": "Discovery options will appear once you have added an email.", "discovery_email_verification_instructions": "Verify the link in your inbox", "discovery_msisdn_empty": "Discovery options will appear once you have added a phone number.", @@ -2532,7 +2549,6 @@ "error_share_msisdn_discovery": "Unable to share phone number", "identity_server_no_token": "No identity access token found", "identity_server_not_set": "Identity server not set", - "incorrect_msisdn_verification": "Incorrect verification code", "language_section": "Language", "msisdn_in_use": "This phone number is already in use", "msisdn_label": "Phone Number", @@ -3267,6 +3283,8 @@ "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Decrypting", "download_action_downloading": "Downloading", + "download_failed": "Download failed", + "download_failed_description": "An error occurred while downloading this file", "edits": { "tooltip_label": "Edited at %(date)s. Click to view edits.", "tooltip_sub": "Click to view edits", @@ -3699,6 +3717,10 @@ "truncated_list_n_more": { "other": "And %(count)s more..." }, + "unsupported_browser": { + "description": "If you continue, some features may stop working and there is a risk that you may lose data in the future. Update your browser to continue using %(brand)s.", + "title": "%(brand)s does not support this browser" + }, "unsupported_server_description": "This server is using an older version of Matrix. Upgrade to Matrix %(version)s to use %(brand)s without errors.", "unsupported_server_title": "Your server is unsupported", "update": { @@ -4068,7 +4090,7 @@ "title": "Allow this widget to verify your identity" }, "popout": "Popout widget", - "set_room_layout": "Set my room layout for everyone", + "set_room_layout": "Set layout for everyone", "shared_data_avatar": "Your profile picture URL", "shared_data_device_id": "Your device ID", "shared_data_lang": "Your language", diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index a7609450cf..75afc3a4f3 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1287,12 +1287,9 @@ }, "right_panel": { "add_integrations": "Aldonu fenestraĵojn, pontojn, kaj robotojn", - "edit_integrations": "Redakti fenestraĵojn, pontojn, kaj robotojn", "export_chat_button": "Eksporti babilejon", "files_button": "Dosieroj", "pinned_messages": { - "empty": "Ankoraŭ nenio fiksita", - "explainer": "Se vi havas la bezonajn permesojn, malfermu la menuon sur ajna mesaĝo, kaj klaku al Fiksi por meti ĝin ĉi tien.", "limits": { "other": "Vi povas fiksi maksimume %(count)s fenestraĵojn" }, @@ -1755,7 +1752,6 @@ "error_revoke_msisdn_discovery": "Ne povas senvalidigi havigadon je telefonnumero", "error_share_email_discovery": "Ne povas havigi vian retpoŝtadreson", "error_share_msisdn_discovery": "Ne povas havigi telefonnumeron", - "incorrect_msisdn_verification": "Malĝusta kontrola kodo", "language_section": "Lingvo kaj regiono", "msisdn_in_use": "Tiu ĉi telefonnumero jam estas uzata", "msisdn_label": "Telefonnumero", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index bec39f8174..0895426524 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -1648,12 +1648,9 @@ }, "right_panel": { "add_integrations": "Añadir accesorios, puentes y bots", - "edit_integrations": "Editar accesorios, puentes y bots", "export_chat_button": "Exportar conversación", "files_button": "Archivos", "pinned_messages": { - "empty": "Ningún mensaje fijado… todavía", - "explainer": "Si tienes permisos, abre el menú de cualquier mensaje y selecciona Fijar para colocarlo aquí.", "limits": { "other": "Solo puedes anclar hasta %(count)s accesorios" }, @@ -2249,7 +2246,6 @@ "error_share_email_discovery": "No se logró compartir la dirección de correo electrónico", "error_share_msisdn_discovery": "No se logró compartir el número de teléfono", "identity_server_not_set": "Servidor de identidad no configurado", - "incorrect_msisdn_verification": "Verificación de código incorrecta", "language_section": "Idioma y región", "msisdn_in_use": "Este número de teléfono ya está en uso", "msisdn_label": "Número de teléfono", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 96e163f0ae..e1be11559b 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1772,12 +1772,9 @@ }, "right_panel": { "add_integrations": "Lisa vidinaid, võrgusildu ja roboteid", - "edit_integrations": "Muuda vidinaid, võrgusildu ja roboteid", "export_chat_button": "Ekspordi vestlus", "files_button": "Failid", "pinned_messages": { - "empty": "Klammerdatud sõnumeid veel pole", - "explainer": "Kui sul on vastavad õigused olemas, siis ava sõnumi juuresolev menüü ning püsisõnumi tekitamiseks vali Klammerda.", "limits": { "other": "Sa saad kinnitada kuni %(count)s vidinat" }, @@ -2417,7 +2414,6 @@ "error_share_msisdn_discovery": "Telefoninumbri jagamine ei õnnestunud", "identity_server_no_token": "Ei leidu tunnusluba isikutuvastusserveri jaoks", "identity_server_not_set": "Isikutuvastusserver on määramata", - "incorrect_msisdn_verification": "Vigane verifikatsioonikood", "language_section": "Keel ja piirkond", "msisdn_in_use": "See telefoninumber on juba kasutusel", "msisdn_label": "Telefoninumber", diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 0120a7e76d..99d60e0474 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -1168,7 +1168,6 @@ }, "right_panel": { "add_integrations": "افزودن ابزارک‌ها، پل‌ها و ربات‌ها", - "edit_integrations": "ویرایش ابزارک ها ، پل ها و ربات ها", "pinned_messages": { "limits": { "other": "فقط می توانید تا %(count)s ابزارک را پین کنید" @@ -1546,7 +1545,6 @@ "error_share_email_discovery": "به اشتراک‌گذاری آدرس ایمیل ممکن نیست", "error_share_msisdn_discovery": "امکان به اشتراک‌گذاری شماره تلفن وجود ندارد", "identity_server_not_set": "سرور هویت تنظیم نشده است", - "incorrect_msisdn_verification": "کد فعال‌سازی اشتباه است", "language_section": "زبان و جغرافیا", "msisdn_in_use": "این شماره تلفن در حال استفاده است", "msisdn_label": "شماره تلفن", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index f44567a25d..0deae167ab 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1544,11 +1544,9 @@ }, "right_panel": { "add_integrations": "Lisää sovelmia, siltoja ja botteja", - "edit_integrations": "Muokkaa sovelmia, siltoja ja botteja", "export_chat_button": "Vie keskustelu", "files_button": "Tiedostot", "pinned_messages": { - "empty": "Ei mitään kiinnitetty, ei vielä", "limits": { "other": "Voit kiinnittää enintään %(count)s sovelmaa" }, @@ -2135,7 +2133,6 @@ "error_share_email_discovery": "Sähköpostiosoitetta ei voi jakaa", "error_share_msisdn_discovery": "Puhelinnumeroa ei voi jakaa", "identity_server_not_set": "Identiteettipalvelinta ei ole asetettu", - "incorrect_msisdn_verification": "Virheellinen varmennuskoodi", "language_section": "Kieli ja alue", "msisdn_in_use": "Puhelinnumero on jo käytössä", "msisdn_label": "Puhelinnumero", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 55fc6ad1d5..715374f340 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1826,12 +1826,9 @@ }, "right_panel": { "add_integrations": "Ajouter des widgets, passerelles et robots", - "edit_integrations": "Modifier les widgets, passerelles et robots", "export_chat_button": "Exporter la conversation", "files_button": "Fichiers", "pinned_messages": { - "empty": "Rien d’épinglé, pour l’instant", - "explainer": "Si vous avez les permissions, ouvrez le menu de n’importe quel message et sélectionnez Épingler pour les afficher ici.", "limits": { "other": "Vous ne pouvez épingler que jusqu’à %(count)s widgets" }, @@ -2485,7 +2482,6 @@ "error_share_msisdn_discovery": "Impossible de partager le numéro de téléphone", "identity_server_no_token": "Aucun jeton d’accès d’identité trouvé", "identity_server_not_set": "Serveur d'identité non défini", - "incorrect_msisdn_verification": "Code de vérification incorrect", "language_section": "Langue et région", "msisdn_in_use": "Ce numéro de téléphone est déjà utilisé", "msisdn_label": "Numéro de téléphone", diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 7f8fa3cb2d..6a508ef4b2 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1533,12 +1533,9 @@ }, "right_panel": { "add_integrations": "Engade widgets, pontes e bots", - "edit_integrations": "Editar widgets, pontes e bots", "export_chat_button": "Exportar chat", "files_button": "Ficheiros", "pinned_messages": { - "empty": "Nada fixado, por agora", - "explainer": "Se tes permisos, abre o menú en calquera mensaxe e elixe Fixar para pegalos aquí.", "limits": { "other": "Só podes fixar ata %(count)s widgets" }, @@ -2082,7 +2079,6 @@ "error_revoke_msisdn_discovery": "Non se puido revogar a compartición do número de teléfono", "error_share_email_discovery": "Non se puido compartir co enderezo de email", "error_share_msisdn_discovery": "Non se puido compartir o número de teléfono", - "incorrect_msisdn_verification": "Código de verificación incorrecto", "language_section": "Idioma e rexión", "msisdn_in_use": "Xa se está a usar este teléfono", "msisdn_label": "Número de teléfono", diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index 8c67ee5dac..9737f7d516 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -1230,11 +1230,9 @@ }, "right_panel": { "add_integrations": "הוסף יישומונים, גשרים ובוטים", - "edit_integrations": "ערוך ישומונים, גשרים ובוטים", "export_chat_button": "ייצוא צ'אט", "files_button": "קבצים", "pinned_messages": { - "empty": "אין הודעות נעוצות, לבינתיים", "limits": { "other": "אתה יכול להצמיד עד%(count)s ווידג'טים בלבד" }, @@ -1673,7 +1671,6 @@ "error_revoke_msisdn_discovery": "לא ניתן לבטל את השיתוף למספר טלפון", "error_share_email_discovery": "לא ניתן לשתף את כתובת הדוא\"ל", "error_share_msisdn_discovery": "לא ניתן לשתף מספר טלפון", - "incorrect_msisdn_verification": "קוד אימות שגוי", "language_section": "שפה ואיזור", "msisdn_in_use": "מספר הטלפון הזה כבר בשימוש", "msisdn_label": "מספר טלפון", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 6b90b22d1e..06a09abd22 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1742,12 +1742,9 @@ }, "right_panel": { "add_integrations": "Kisalkalmazások, hidak, és botok hozzáadása", - "edit_integrations": "Kisalkalmazások, hidak és botok szerkesztése", "export_chat_button": "Beszélgetés exportálása", "files_button": "Fájlok", "pinned_messages": { - "empty": "Még semmi sincs kitűzve", - "explainer": "Ha van hozzá jogosultsága, nyissa meg a menüt bármelyik üzenetben és válassza a Kitűzés menüpontot a kitűzéshez.", "limits": { "other": "Csak %(count)s kisalkalmazást tud kitűzni" }, @@ -2376,7 +2373,6 @@ "error_share_msisdn_discovery": "A telefonszámot nem sikerült megosztani", "identity_server_no_token": "Nem található személyazonosság-hozzáférési kulcs", "identity_server_not_set": "Az azonosítási kiszolgáló nincs megadva", - "incorrect_msisdn_verification": "Hibás azonosítási kód", "language_section": "Nyelv és régió", "msisdn_in_use": "Ez a telefonszám már használatban van", "msisdn_label": "Telefonszám", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index c3b8c462e2..360170f2b0 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -1754,12 +1754,9 @@ }, "right_panel": { "add_integrations": "Tambahkan widget, jembatan & bot", - "edit_integrations": "Edit widget, jembatan & bot", "export_chat_button": "Ekspor obrolan", "files_button": "File", "pinned_messages": { - "empty": "Belum ada yang dipasangi pin", - "explainer": "Jika Anda memiliki izin, buka menunya di pesan apa saja dan pilih Pin untuk menempelkannya di sini.", "limits": { "other": "Anda hanya dapat memasang pin sampai %(count)s widget" }, @@ -2409,7 +2406,6 @@ "error_share_msisdn_discovery": "Tidak dapat membagikan nomor telepon", "identity_server_no_token": "Tidak ada token akses identitas yang ditemukan", "identity_server_not_set": "Server identitas tidak diatur", - "incorrect_msisdn_verification": "Kode verifikasi tidak benar", "language_section": "Bahasa dan wilayah", "msisdn_in_use": "Nomor telepon ini telah dipakai", "msisdn_label": "Nomor Telepon", diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 3688882ada..71d292ca52 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -1467,11 +1467,9 @@ }, "right_panel": { "add_integrations": "Bæta við viðmótshlutum, brúm og vélmennum", - "edit_integrations": "Breyta viðmótshlutum, brúm og vélmennum", "export_chat_button": "Flytja út spjall", "files_button": "Skrár", "pinned_messages": { - "empty": "Ekkert fest, ennþá", "limits": { "other": "Þú getur bara fest allt að %(count)s viðmótshluta" }, @@ -1969,7 +1967,6 @@ "error_revoke_msisdn_discovery": "Ekki er hægt að afturkalla að deila símanúmeri", "error_share_email_discovery": "Get ekki deilt tölvupóstfangi", "error_share_msisdn_discovery": "Ekki er hægt að deila símanúmeri", - "incorrect_msisdn_verification": "Rangur sannvottunarkóði", "language_section": "Tungumál og landsvæði", "msisdn_in_use": "Þetta símanúmer er nú þegar í notkun", "msisdn_label": "Símanúmer", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 59d807a957..b978a3dfbf 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -1788,12 +1788,9 @@ }, "right_panel": { "add_integrations": "Aggiungi widget, bridge e bot", - "edit_integrations": "Modifica widget, bridge e bot", "export_chat_button": "Esporta conversazione", "files_button": "File", "pinned_messages": { - "empty": "Non c'è ancora nulla di ancorato", - "explainer": "Se ne hai il permesso, apri il menu di qualsiasi messaggio e seleziona Fissa per ancorarlo qui.", "limits": { "other": "Puoi ancorare al massimo %(count)s widget" }, @@ -2451,7 +2448,6 @@ "error_share_msisdn_discovery": "Impossibile condividere il numero di telefono", "identity_server_no_token": "Nessun token di accesso d'identità trovato", "identity_server_not_set": "Server d'identità non impostato", - "incorrect_msisdn_verification": "Codice di verifica sbagliato", "language_section": "Lingua e regione", "msisdn_in_use": "Questo numero di telefono è già in uso", "msisdn_label": "Numero di telefono", diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 69aec14f82..4af4a7cee0 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -1661,12 +1661,9 @@ }, "right_panel": { "add_integrations": "ウィジェット、ブリッジ、ボットの追加", - "edit_integrations": "ウィジェット、ブリッジ、ボットを編集", "export_chat_button": "チャットをエクスポート", "files_button": "ファイル", "pinned_messages": { - "empty": "固定メッセージはありません", - "explainer": "権限がある場合は、メッセージのメニューを開いて固定を選択すると、ここにメッセージが表示されます。", "limits": { "other": "ウィジェットのピン留めは%(count)s件までです" }, @@ -2240,7 +2237,6 @@ "error_revoke_msisdn_discovery": "電話番号の共有を取り消せません", "error_share_email_discovery": "メールアドレスを共有できません", "error_share_msisdn_discovery": "電話番号を共有できません", - "incorrect_msisdn_verification": "認証コードが誤っています", "language_section": "言語と地域", "msisdn_in_use": "この電話番号は既に使用されています", "msisdn_label": "電話番号", diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index a7f36be946..ab333ebf40 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -1450,12 +1450,9 @@ }, "right_panel": { "add_integrations": "ເພີ່ມວິດເຈັດ, ຂົວ ແລະບັອດ", - "edit_integrations": "ແກ້ໄຂວິດເຈັດ, ຂົວ ແລະບັອດ", "export_chat_button": "ສົ່ງການສົນທະນາອອກ", "files_button": "ໄຟລ໌", "pinned_messages": { - "empty": "ບໍ່ມີຫຍັງຖືກປັກໝຸດ,", - "explainer": "ຖ້າຫາກທ່ານມີການອະນຸຍາດ, ເປີດເມນູໃນຂໍ້ຄວາມໃດຫນຶ່ງ ແລະ ເລືອກ Pin ເພື່ອຕິດໃຫ້ເຂົາເຈົ້າຢູ່ທີ່ນີ້.", "limits": { "other": "ທ່ານສາມາດປັກໝຸດໄດ້ເຖິງ %(count)s widget ເທົ່ານັ້ນ" }, @@ -1995,7 +1992,6 @@ "error_revoke_msisdn_discovery": "ບໍ່ສາມາດຖອນການແບ່ງປັນສຳລັບເບີໂທລະສັບໄດ້", "error_share_email_discovery": "ບໍ່ສາມາດແບ່ງປັນທີ່ຢູ່ອີເມວໄດ້", "error_share_msisdn_discovery": "ບໍ່ສາມາດແບ່ງປັນເບີໂທລະສັບໄດ້", - "incorrect_msisdn_verification": "ລະຫັດຢືນຢັນບໍ່ຖືກຕ້ອງ", "language_section": "ພາສາ ແລະ ພາກພື້ນ", "msisdn_in_use": "ເບີໂທນີ້ຖືກໃຊ້ແລ້ວ", "msisdn_label": "ເບີໂທລະສັບ", diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index e8cc6bc7eb..9a300d5165 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -1081,12 +1081,9 @@ }, "right_panel": { "add_integrations": "Pridėti valdiklius, tiltus ir botus", - "edit_integrations": "Redaguoti valdiklius, tiltus ir botus", "export_chat_button": "Eksportuoti pokalbį", "files_button": "Failai", "pinned_messages": { - "empty": "Kol kas nieko neprisegta", - "explainer": "Jei turite leidimus, atidarykite bet kurios žinutės meniu ir pasirinkite Prisegti, kad juos čia priklijuotumėte.", "limits": { "other": "Galite prisegti tik iki %(count)s valdiklių" }, @@ -1569,7 +1566,6 @@ "error_revoke_msisdn_discovery": "Neina atšaukti telefono numerio bendrinimo", "error_share_email_discovery": "Nepavyko pasidalinti el. pašto adresu", "error_share_msisdn_discovery": "Neina bendrinti telefono numerio", - "incorrect_msisdn_verification": "Neteisingas patvirtinimo kodas", "language_section": "Kalba ir regionas", "msisdn_in_use": "Šis telefono numeris jau naudojamas", "msisdn_label": "Telefono Numeris", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 1927341c57..10c9e419f4 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1513,12 +1513,9 @@ }, "right_panel": { "add_integrations": "Widgets, bruggen & bots toevoegen", - "edit_integrations": "Widgets, bruggen & bots bewerken", "export_chat_button": "Chat exporteren", "files_button": "Bestanden", "pinned_messages": { - "empty": "Nog niks vastgeprikt", - "explainer": "Als je de rechten hebt, open dan het menu op elk bericht en selecteer Vastprikken om ze hier te zetten.", "limits": { "other": "Je kunt maar %(count)s widgets vastzetten" }, @@ -2077,7 +2074,6 @@ "error_revoke_msisdn_discovery": "Kan delen voor dit telefoonnummer niet intrekken", "error_share_email_discovery": "Kan e-mailadres niet delen", "error_share_msisdn_discovery": "Kan telefoonnummer niet delen", - "incorrect_msisdn_verification": "Onjuiste verificatiecode", "language_section": "Taal en regio", "msisdn_in_use": "Dit telefoonnummer is al in gebruik", "msisdn_label": "Telefoonnummer", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 90ea7ea988..ad9d34ab0c 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -474,6 +474,7 @@ "encrypted": "Szyfrowane", "encryption_enabled": "Włączono szyfrowanie", "error": "Błąd", + "extensions": "Rozszerzenia", "faq": "Najczęściej zadawane pytania", "favourites": "Ulubione", "feedback": "Opinia użytkownika", @@ -1628,7 +1629,7 @@ "level_none": "Brak", "level_notification": "Powiadomienie", "level_unsent": "Niewysłane", - "mark_all_read": "Oznacz wszystko jako przeczytane", + "mark_all_read": "Oznacz wszystkie jako przeczytane", "mentions_and_keywords": "@wzmianki & słowa kluczowe", "mentions_and_keywords_description": "Otrzymuj powiadomienia tylko z wzmiankami i słowami kluczowymi zgodnie z Twoimi
ustawieniami", "mentions_keywords": "Wzmianki i słowa kluczowe", @@ -1654,8 +1655,8 @@ "download_brand_desktop": "Pobierz %(brand)s Desktop", "download_f_droid": "Pobierz w F-Droid", "download_google_play": "Pobierz w Google Play", - "enable_notifications": "Włącz powiadomienia", - "enable_notifications_action": "Włącz powiadomienia", + "enable_notifications": "Włącz powiadomienia na pulpicie", + "enable_notifications_action": "Otwórz ustawienia", "enable_notifications_description": "Nie przegap odpowiedzi lub ważnej wiadomości", "explore_rooms": "Przeglądaj pokoje publiczne", "find_community_members": "Znajdź i zaproś członków swojej społeczności", @@ -1832,21 +1833,20 @@ "restore_failed_error": "Przywrócenie kopii zapasowej jest niemożliwe" }, "right_panel": { - "add_integrations": "Dodaj widżety, mostki i boty", + "add_integrations": "Dodaj rozszerzenia", "add_topic": "Dodaj temat", - "edit_integrations": "Edytuj widżety, mostki i boty", "export_chat_button": "Eksportuj czat", + "extensions_empty_description": "Wybierz “%(addIntegrations)s”, aby przeglądać i dodawać rozszerzenia do tego pokoju", + "extensions_empty_title": "Zwiększ produktywność dzięki większej liczbie narzędzi, widżetów i botów", "files_button": "Pliki", "info": "Info", "pinned_messages": { - "empty": "Nie przypięto tu jeszcze niczego", - "explainer": "Jeżeli masz uprawnienia, przejdź do menu dowolnej wiadomości i wybierz Przypnij, aby przyczepić ją tutaj.", "limits": { "other": "Możesz przypiąć do %(count)s widżetów" }, "title": "Przypięte wiadomości" }, - "pinned_messages_button": "Przypięte", + "pinned_messages_button": "Przypięte wiadomości", "poll": { "active_heading": "Aktywne ankiety", "empty_active": "Brak aktywnych ankiet w tym pokoju", @@ -1871,7 +1871,7 @@ "view_in_timeline": "Wyświetl ankietę na osi czasu", "view_poll": "Wyświetl ankietę" }, - "polls_button": "Historia ankiet", + "polls_button": "Ankiety", "room_summary_card": { "title": "Informacje pokoju" }, @@ -1953,6 +1953,8 @@ "few": "%(count)s osoby proszą o dołączenie", "many": "%(count)s osób prosi o dołączenie" }, + "release_announcement_description": "Ciesz się prostszym, bardziej przystosowanym nagłówkiem pokoju.", + "release_announcement_header": "Nowy design!", "room_is_public": "Ten pokój jest publiczny", "show_widgets_button": "Pokaż widżety", "video_call_button_ec": "Rozmowa wideo (%(brand)s)", @@ -1990,7 +1992,7 @@ "invite_reject_ignore": "Odrzuć i zignoruj użytkownika", "invite_sent_to_email": "To zaproszenie zostało wysłane do %(email)s", "invite_sent_to_email_room": "To zaproszenie do %(roomName)s zostało wysłane do %(email)s", - "invite_subtitle": " zaprosił Cię", + "invite_subtitle": "Zaproszony przez ", "invite_this_room": "Zaproś do tego pokoju", "invite_title": "Czy chcesz dołączyć do %(roomName)s?", "inviter_unknown": "Nieznany", @@ -2004,7 +2006,7 @@ "joining": "Dołączanie…", "joining_room": "Dołączanie do pokoju…", "joining_space": "Dołączanie do przestrzeni…", - "jump_read_marker": "Przeskocz do pierwszej nieprzeczytanej wiadomości.", + "jump_read_marker": "Skocz do pierwszej nieprzeczytanej wiadomości.", "jump_to_bottom_button": "Przewiń do najnowszych wiadomości", "jump_to_date": "Przeskocz do daty", "jump_to_date_beginning": "Początek pokoju", @@ -2422,6 +2424,10 @@ } }, "settings": { + "account": { + "dialog_title": "Ustawienia: Konto", + "title": "Konto" + }, "all_rooms_home": "Pokaż wszystkie pokoje na ekranie głównym", "all_rooms_home_description": "Wszystkie pokoje w których jesteś zostaną pokazane na ekranie głównym.", "always_show_message_timestamps": "Zawsze pokazuj znaczniki czasu wiadomości", @@ -2497,7 +2503,6 @@ "deactivate_confirm_erase_label": "Ukryj moje wiadomości dla nowych osób", "deactivate_section": "Dezaktywuj konto", "deactivate_warning": "Dezaktywacja konta jest akcją trwałą — bądź ostrożny!", - "dialog_title": "Ustawienia: Ogólne", "discovery_email_empty": "Opcje odkrywania pojawią się, gdy dodasz adres e-mail.", "discovery_email_verification_instructions": "Zweryfikuj link w swojej skrzynce odbiorczej", "discovery_msisdn_empty": "Opcje odkrywania pojawią się, gdy dodasz numer telefonu.", @@ -2529,7 +2534,6 @@ "error_share_msisdn_discovery": "Nie udało się udostępnić numeru telefonu", "identity_server_no_token": "Nie znaleziono tokena dostępu tożsamości", "identity_server_not_set": "Serwer tożsamości nie jest ustawiony", - "incorrect_msisdn_verification": "Nieprawidłowy kod weryfikujący", "language_section": "Język", "msisdn_in_use": "Ten numer telefonu jest już zajęty", "msisdn_label": "Numer telefonu", @@ -3197,6 +3201,8 @@ "one": "%(count)s odpowiedź", "other": "%(count)s odpowiedzi" }, + "empty_description": "Użyj „%(replyInThread)s” po najechaniu kursorem na wiadomość", + "empty_title": "Wątki pomagają utrzymać tematykę rozmów i łatwo za nimi podążyć.", "error_start_thread_existing_relation": "Nie można utworzyć wątku z wydarzenia z istniejącą relacją", "mark_all_read": "Oznacz wszystkie jako przeczytane", "my_threads": "Moje wątki", @@ -3262,6 +3268,8 @@ "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Rozszyfrowuję", "download_action_downloading": "Pobieranie", + "download_failed": "Błąd pobierania", + "download_failed_description": "Wystąpił błąd podczas pobierania tego pliku", "edits": { "tooltip_label": "Edytowano w %(date)s. Kliknij, aby zobaczyć zmiany.", "tooltip_sub": "Kliknij, aby wyświetlić edycje", @@ -3693,6 +3701,10 @@ "truncated_list_n_more": { "other": "I %(count)s więcej…" }, + "unsupported_browser": { + "description": "Jeśli kontynuujesz, niektóre funkcje mogą przestać działać, jak i istnieje ryzyko utraty danych w przyszłości. Zaktualizuj przeglądarkę, aby nadal używać %(brand)s.", + "title": "%(brand)s nie wspiera tej przeglądarki" + }, "unsupported_server_description": "Ten serwer używa starszej wersji Matrix. Zaktualizuj do Matrix%(version)s, aby używać %(brand)s bez błędów.", "unsupported_server_title": "Twój serwer nie jest wspierany", "update": { @@ -3768,6 +3780,7 @@ "error_revoke_3pid_invite_title": "Nie udało się odwołać zaproszenia", "hide_sessions": "Ukryj sesje", "hide_verified_sessions": "Ukryj zweryfikowane sesje", + "ignore_button": "Ignoruj", "ignore_confirm_description": "Wszystkie wiadomości i zaproszenia od tego użytkownika zostaną ukryte. Czy na pewno chcesz zignorować?", "ignore_confirm_title": "Ignoruj %(user)s", "invited_by": "Zaproszony przez %(sender)s", @@ -3795,19 +3808,21 @@ "no_recent_messages_description": "Spróbuj przewinąć się w górę na osi czasu, aby sprawdzić, czy nie ma wcześniejszych.", "no_recent_messages_title": "Nie znaleziono ostatnich wiadomości od %(user)s" }, - "redact_button": "Usuń ostatnie wiadomości", + "redact_button": "Usuń wiadomości", "revoke_invite": "Odwołaj zaproszenie", "room_encrypted": "Wiadomości w tym pokoju są szyfrowane end-to-end.", "room_encrypted_detail": "Twoje wiadomości są zabezpieczone i tylko Ty i Twój odbiorca posiadacie unikalne klucze, aby je odblokować.", "room_unencrypted": "Wiadomości w tym pokoju nie są szyfrowane end-to-end.", "room_unencrypted_detail": "W pokojach szyfrowanych Twoje wiadomości są zabezpieczone i tylko Ty i Twój odbiorca posiadacie unikalne klucze, aby je odblokować.", - "share_button": "Udostępnij link użytkownika", + "send_message": "Wyślij wiadomość", + "share_button": "Udostępnij profil", "unban_button_room": "Odbanuj z przestrzeni", "unban_button_space": "Odbanuj z przestrzeni", "unban_room_confirm_title": "Odbanuj z %(roomName)s", "unban_space_everything": "Odbanuj ich z wszystkiego co mogę", "unban_space_specific": "Odbanuj ich z określonych rzeczy, które mogę", "unban_space_warning": "Nie będą w stanie uzyskać dostępu gdziekolwiek, gdzie nie jesteś administratorem.", + "unignore_button": "Przestań ignorować", "verify_button": "Weryfikuj użytkownika", "verify_explainer": "Dla dodatkowego bezpieczeństwa, zweryfikuj tego użytkownika za pomocą jednorazowego kodu na obu waszych urządzeniach." }, @@ -3882,8 +3897,8 @@ "disable_camera": "Wyłącz kamerę", "disable_microphone": "Wycisz mikrofon", "disabled_no_one_here": "Nie ma tu nikogo, do kogo można zadzwonić", - "disabled_no_perms_start_video_call": "Nie posiadasz uprawnień do rozpoczęcia rozmowy wideo", - "disabled_no_perms_start_voice_call": "Nie posiadasz uprawnień do rozpoczęcia rozmowy głosowej", + "disabled_no_perms_start_video_call": "Nie masz uprawnień do rozpoczęcia rozmowy wideo", + "disabled_no_perms_start_voice_call": "Nie masz uprawnień do rozpoczęcia rozmowy głosowej", "disabled_ongoing_call": "Rozmowa w toku", "element_call": "Element Call", "enable_camera": "Włącz kamerę", @@ -4059,7 +4074,7 @@ "title": "Zezwól temu widżetowi na weryfikacje Twojej tożsamości" }, "popout": "Wyskakujący widżet", - "set_room_layout": "Ustaw mój układ pokoju dla wszystkich", + "set_room_layout": "Ustaw układ dla wszystkich", "shared_data_avatar": "URL Twojego zdjęcia profilowego", "shared_data_device_id": "Twoje ID urządzenia", "shared_data_lang": "Twój język", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 8dc2d3567c..54c74671aa 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -1193,12 +1193,9 @@ }, "right_panel": { "add_integrations": "Adicionar widgets, integrações e bots", - "edit_integrations": "Editar widgets, integrações e bots", "export_chat_button": "Exportar conversa", "files_button": "Arquivos", "pinned_messages": { - "empty": "Nada fixado ainda", - "explainer": "Caso você tenha a permissão para isso, abra o menu em qualquer mensagem e selecione Fixar para fixá-la aqui.", "limits": { "other": "Você pode fixar até %(count)s widgets" }, @@ -1665,7 +1662,6 @@ "error_revoke_msisdn_discovery": "Não foi possível revogar o compartilhamento do número de celular", "error_share_email_discovery": "Não foi possível compartilhar o endereço de e-mail", "error_share_msisdn_discovery": "Não foi possível compartilhar o número de celular", - "incorrect_msisdn_verification": "Código de confirmação incorreto", "language_section": "Idioma e região", "msisdn_in_use": "Este número de telefone já está em uso", "msisdn_label": "Número de telefone", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 45d229ca61..da18850d71 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1770,12 +1770,9 @@ }, "right_panel": { "add_integrations": "Добавить виджеты, мосты и ботов", - "edit_integrations": "Редактировать виджеты, мосты и ботов", "export_chat_button": "Экспорт чата", "files_button": "Файлы", "pinned_messages": { - "empty": "Пока ничего не закреплено", - "explainer": "Если у вас есть разрешения, откройте меню на любом сообщении и выберите Закрепить, чтобы поместить их сюда.", "limits": { "other": "Вы можете закрепить не более %(count)s виджетов" }, @@ -2434,7 +2431,6 @@ "error_share_msisdn_discovery": "Не удается предоставить общий доступ к номеру телефона", "identity_server_no_token": "Не найден токен доступа для идентификации", "identity_server_not_set": "Сервер идентификации не установлен", - "incorrect_msisdn_verification": "Неверный код подтверждения", "language_section": "Язык и регион", "msisdn_in_use": "Этот номер телефона уже используется", "msisdn_label": "Номер телефона", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 47a00bb12e..43ce04b06d 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1776,12 +1776,9 @@ }, "right_panel": { "add_integrations": "Pridať widgety, premostenia a boty", - "edit_integrations": "Upraviť widgety, premostenia a boty", "export_chat_button": "Exportovať konverzáciu", "files_button": "Súbory", "pinned_messages": { - "empty": "Zatiaľ nie je nič pripnuté", - "explainer": "Ak máte oprávnenia, otvorte ponuku pri ľubovoľnej správe a výberom položky Pripnúť ich sem prilepíte.", "limits": { "other": "Môžete pripnúť iba %(count)s widgetov" }, @@ -2438,7 +2435,6 @@ "error_share_msisdn_discovery": "Nepodarilo sa zdieľanie telefónneho čísla", "identity_server_no_token": "Nenašiel sa prístupový token totožnosti", "identity_server_not_set": "Server totožnosti nie je nastavený", - "incorrect_msisdn_verification": "Nesprávny overovací kód", "language_section": "Jazyk a región", "msisdn_in_use": "Toto telefónne číslo sa už používa", "msisdn_label": "Telefónne číslo", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 5c67bb8d5b..1d544cd7fb 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1677,12 +1677,9 @@ }, "right_panel": { "add_integrations": "Shtoni widget-e, ura & robotë", - "edit_integrations": "Përpunoni widget-e, ura & robotë", "export_chat_button": "Eksportoni fjalosje", "files_button": "Kartela", "pinned_messages": { - "empty": "Ende pa fiksuar gjë", - "explainer": "Nëse keni leje, hapni menunë për çfarëdo mesazhi dhe përzgjidhni Fiksoje, për ta ngjitur këtu.", "limits": { "other": "Mundeni të fiksoni deri në %(count)s widget-e" }, @@ -2301,7 +2298,6 @@ "error_share_msisdn_discovery": "S’arrihet të ndahet numër telefoni", "identity_server_no_token": "S’u gjet token hyrjeje identiteti", "identity_server_not_set": "Shërbyes identitetesh i paujdisur", - "incorrect_msisdn_verification": "Kod verifikimi i pasaktë", "language_section": "Gjuhë dhe rajon", "msisdn_in_use": "Ky numër telefoni është tashmë në përdorim", "msisdn_label": "Numër Telefoni", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 6774e882ef..1095c8dec1 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -1788,12 +1788,9 @@ }, "right_panel": { "add_integrations": "Lägg till widgets, bryggor och bottar", - "edit_integrations": "Redigera widgets, bryggor och bottar", "export_chat_button": "Exportera chatt", "files_button": "Filer", "pinned_messages": { - "empty": "Inget fäst än", - "explainer": "Om du har behörighet, öppna menyn på ett meddelande och välj Fäst för att fösta dem här.", "limits": { "other": "Du kan bara fästa upp till %(count)s widgets" }, @@ -2450,7 +2447,6 @@ "error_share_msisdn_discovery": "Kunde inte dela telefonnummer", "identity_server_no_token": "Ingen identitetsåtkomsttoken hittades", "identity_server_not_set": "Identitetsserver inte inställd", - "incorrect_msisdn_verification": "Fel verifieringskod", "language_section": "Språk och region", "msisdn_in_use": "Detta telefonnummer används redan", "msisdn_label": "Telefonnummer", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 932b7e8689..66642f5e62 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -1732,12 +1732,9 @@ }, "right_panel": { "add_integrations": "Додати віджети, мости та ботів", - "edit_integrations": "Редагувати віджети, мости та ботів", "export_chat_button": "Експортувати бесіду", "files_button": "Файли", "pinned_messages": { - "empty": "Наразі нічого не закріплено", - "explainer": "Якщо маєте дозвіл, відкрийте меню будь-якого повідомлення й натисніть Закріпити, щоб додати його сюди.", "limits": { "other": "Закріпити можна до %(count)s віджетів" }, @@ -2375,7 +2372,6 @@ "error_share_msisdn_discovery": "Не вдалося надіслати телефонний номер", "identity_server_no_token": "Токен доступу до ідентифікації не знайдено", "identity_server_not_set": "Сервер ідентифікації не налаштовано", - "incorrect_msisdn_verification": "Неправильний код перевірки", "language_section": "Мова та регіон", "msisdn_in_use": "Цей телефонний номер вже використовується", "msisdn_label": "Телефонний номер", diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index 359c0e3182..aa240c20bf 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -1581,11 +1581,8 @@ }, "right_panel": { "add_integrations": "Thêm các widget, bridge và bot", - "edit_integrations": "Chỉnh sửa tiện ích widget, cầu nối và bot", "export_chat_button": "Xuất trò chuyện", "pinned_messages": { - "empty": "Chưa có gì được ghim", - "explainer": "Nếu bạn có quyền, hãy mở menu trên bất kỳ tin nhắn nào và chọn Ghim Pin để dán chúng vào đây.", "limits": { "other": "Bạn chỉ có thể ghim tối đa %(count)s widget" }, @@ -2175,7 +2172,6 @@ "error_share_msisdn_discovery": "Không thể chia sẻ số điện thoại", "identity_server_no_token": "Không tìm thấy mã thông báo danh tính", "identity_server_not_set": "Máy chủ định danh chưa được đặt", - "incorrect_msisdn_verification": "Mã xác minh không chính xác", "language_section": "Ngôn ngữ và khu vực", "msisdn_in_use": "Số điện thoại này đã được sử dụng", "msisdn_label": "Số điện thoại", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 3b2269e9a5..984d87f67d 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -1628,12 +1628,9 @@ }, "right_panel": { "add_integrations": "添加挂件、桥接和机器人", - "edit_integrations": "编辑挂件、桥接和机器人", "export_chat_button": "导出聊天", "files_button": "文件", "pinned_messages": { - "empty": "尚无固定任何东西", - "explainer": "如果你拥有权限,请打开任何消息的菜单并选择固定将它们粘贴至此。", "limits": { "other": "你仅能固定 %(count)s 个挂件" }, @@ -2203,7 +2200,6 @@ "error_share_msisdn_discovery": "无法共享电话号码", "identity_server_no_token": "找不到身份访问令牌", "identity_server_not_set": "身份服务器未设置", - "incorrect_msisdn_verification": "验证码错误", "language_section": "语言与地区", "msisdn_in_use": "此电话号码已被使用", "msisdn_label": "电话号码", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 8715591fcb..d7d3a74784 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1735,12 +1735,9 @@ }, "right_panel": { "add_integrations": "新增小工具、橋接與聊天機器人", - "edit_integrations": "編輯小工具、橋接與聊天機器人", "export_chat_button": "匯出聊天", "files_button": "檔案", "pinned_messages": { - "empty": "尚未釘選任何東西", - "explainer": "如果您有權限,請開啟任何訊息的選單,並選取釘選以將它們固定到這裡。", "limits": { "other": "您最多只能釘選 %(count)s 個小工具" }, @@ -2377,7 +2374,6 @@ "error_share_msisdn_discovery": "無法分享電話號碼", "identity_server_no_token": "找不到身分存取權杖", "identity_server_not_set": "身分伺服器未設定", - "incorrect_msisdn_verification": "驗證碼錯誤", "language_section": "語言與區域", "msisdn_in_use": "這個電話號碼已被使用", "msisdn_label": "電話號碼", diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index e0ae78085b..345a5984f6 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -53,7 +53,7 @@ const FALLBACK_LOCALE = "en"; counterpart.setFallbackLocale(FALLBACK_LOCALE); export interface ErrorOptions { - // Because we're mixing the subsitution variables and `cause` into the same object + // Because we're mixing the substitution variables and `cause` into the same object // below, we want them to always explicitly say whether there is an underlying error // or not to avoid typos of "cause" slipping through unnoticed. cause: unknown | undefined; @@ -78,16 +78,15 @@ export interface ErrorOptions { export class UserFriendlyError extends Error { public readonly translatedMessage: string; - public constructor(message: TranslationKey, substitutionVariablesAndCause?: IVariables & ErrorOptions) { - const errorOptions = { - cause: substitutionVariablesAndCause?.cause, - }; + public constructor( + message: TranslationKey, + substitutionVariablesAndCause?: Omit | ErrorOptions, + ) { // Prevent "Could not find /%\(cause\)s/g in x" logs to the console by removing it from the list - const substitutionVariables = { ...substitutionVariablesAndCause }; - delete substitutionVariables["cause"]; + const { cause, ...substitutionVariables } = substitutionVariablesAndCause ?? {}; + const errorOptions = { cause }; - // Create the error with the English version of the message that we want to show - // up in the logs + // Create the error with the English version of the message that we want to show up in the logs const englishTranslatedMessage = _t(message, { ...substitutionVariables, locale: "en" }); super(englishTranslatedMessage, errorOptions); diff --git a/src/models/Call.ts b/src/models/Call.ts index 7c42719563..658cb78048 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -30,14 +30,13 @@ import { randomString } from "matrix-js-sdk/src/randomstring"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue"; import { IWidgetApiRequest } from "matrix-widget-api"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSession, MatrixRTCSessionEvent } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; -// eslint-disable-next-line no-restricted-imports -import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; -// eslint-disable-next-line no-restricted-imports -import { ICallNotifyContent } from "matrix-js-sdk/src/matrixrtc/types"; +import { + MatrixRTCSession, + MatrixRTCSessionEvent, + CallMembership, + MatrixRTCSessionManagerEvents, + ICallNotifyContent, +} from "matrix-js-sdk/src/matrixrtc"; import type EventEmitter from "events"; import type { ClientWidgetApi, IWidgetData } from "matrix-widget-api"; @@ -127,8 +126,8 @@ interface CallEventHandlerMap { * A group call accessed through a widget. */ export abstract class Call extends TypedEventEmitter { - protected readonly widgetUid = WidgetUtils.getWidgetUid(this.widget); - protected readonly room = this.client.getRoom(this.roomId)!; + protected readonly widgetUid: string; + protected readonly room: Room; /** * The time after which device member state should be considered expired. @@ -185,6 +184,8 @@ export abstract class Call extends TypedEventEmitter { + ev.preventDefault(); + await this.messaging!.transport.reply(ev.detail, {}); // ack + }); if (!this.widget.data?.skipLobby) { // If we do not skip the lobby we need to wait until the widget has diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index cf31bca246..5acdf5b4ab 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -918,7 +918,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { controller: new UIFeatureController(UIFeature.URLPreviews), }, "urlPreviewsEnabled_e2ee": { - supportedLevels: [SettingLevel.ROOM_DEVICE, SettingLevel.ROOM_ACCOUNT], + supportedLevels: [SettingLevel.ROOM_DEVICE], displayName: { "room-account": _td("settings|inline_url_previews_room_account"), }, diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 6e3e9e3e1f..a63958f594 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -17,6 +17,7 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; import { ReactNode } from "react"; +import { ClientEvent, SyncState } from "matrix-js-sdk/src/matrix"; import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler"; import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler"; @@ -36,6 +37,7 @@ import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayl import { Action } from "../dispatcher/actions"; import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler"; import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; +import { MatrixClientPeg } from "../MatrixClientPeg"; // Convert the settings to easier to manage objects for the handlers const defaultSettings: Record = {}; @@ -637,10 +639,61 @@ export default class SettingsStore { return null; } + /** + * Migrate the setting for URL previews in e2e rooms from room account + * data to the room device level. + * + * @param isFreshLogin True if the user has just logged in, false if a previous session is being restored. + */ + private static async migrateURLPreviewsE2EE(isFreshLogin: boolean): Promise { + const MIGRATION_DONE_FLAG = "url_previews_e2ee_migration_done"; + if (localStorage.getItem(MIGRATION_DONE_FLAG)) return; + if (isFreshLogin) return; + + const client = MatrixClientPeg.safeGet(); + + const doMigration = async (): Promise => { + logger.info("Performing one-time settings migration of URL previews in E2EE rooms"); + + const roomAccounthandler = LEVEL_HANDLERS[SettingLevel.ROOM_ACCOUNT]; + + for (const room of client.getRooms()) { + // We need to use the handler directly because this setting is no longer supported + // at this level at all + const val = roomAccounthandler.getValue("urlPreviewsEnabled_e2ee", room.roomId); + + if (val !== undefined) { + await SettingsStore.setValue("urlPreviewsEnabled_e2ee", room.roomId, SettingLevel.ROOM_DEVICE, val); + } + } + + localStorage.setItem(MIGRATION_DONE_FLAG, "true"); + }; + + const onSync = (state: SyncState): void => { + if (state === SyncState.Prepared) { + client.removeListener(ClientEvent.Sync, onSync); + + doMigration().catch((e) => { + logger.error("Failed to migrate URL previews in E2EE rooms:", e); + }); + } + }; + + client.on(ClientEvent.Sync, onSync); + } + /** * Runs or queues any setting migrations needed. */ - public static runMigrations(): void { + public static runMigrations(isFreshLogin: boolean): void { + // This can be removed once enough users have run a version of Element with + // this migration. A couple of months after its release should be sufficient + // (so around October 2024). + // The consequences of missing the migration are only that URL previews will + // be disabled in E2EE rooms. + SettingsStore.migrateURLPreviewsE2EE(isFreshLogin); + // Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and // add a comment to note when it can be removed. return; diff --git a/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts b/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts index bcc72055f4..fb640fe313 100644 --- a/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts +++ b/src/settings/handlers/AbstractLocalStorageSettingsHandler.ts @@ -35,8 +35,7 @@ export default abstract class AbstractLocalStorageSettingsHandler extends Settin } }; - // Expose the clear event for Lifecycle to call, the storage listener only fires for changes from other tabs - public static clear(): void { + private static clear(): void { AbstractLocalStorageSettingsHandler.itemCache.clear(); AbstractLocalStorageSettingsHandler.objectCache.clear(); } @@ -108,4 +107,8 @@ export default abstract class AbstractLocalStorageSettingsHandler extends Settin public isSupported(): boolean { return localStorage !== undefined && localStorage !== null; } + + public reset(): void { + AbstractLocalStorageSettingsHandler.clear(); + } } diff --git a/src/stores/CallStore.ts b/src/stores/CallStore.ts index 0187f3d5aa..4ec2d57e91 100644 --- a/src/stores/CallStore.ts +++ b/src/stores/CallStore.ts @@ -16,10 +16,7 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { MatrixRTCSession, MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc"; import type { GroupCall, Room } from "matrix-js-sdk/src/matrix"; import defaultDispatcher from "../dispatcher/dispatcher"; diff --git a/src/stores/LifecycleStore.ts b/src/stores/LifecycleStore.ts index decef11881..d000289f36 100644 --- a/src/stores/LifecycleStore.ts +++ b/src/stores/LifecycleStore.ts @@ -114,8 +114,8 @@ async function checkServerVersions(): Promise { version: MINIMUM_MATRIX_VERSION, brand: SdkConfig.get().brand, }), - acceptLabel: _t("action|ok"), - onAccept: () => { + primaryLabel: _t("action|ok"), + onPrimaryClick: () => { ToastStore.sharedInstance().dismissToast(toastKey); }, }, diff --git a/src/stores/right-panel/RightPanelStorePhases.ts b/src/stores/right-panel/RightPanelStorePhases.ts index 2353d3fe43..4cae676830 100644 --- a/src/stores/right-panel/RightPanelStorePhases.ts +++ b/src/stores/right-panel/RightPanelStorePhases.ts @@ -28,6 +28,7 @@ export enum RightPanelPhases { Widget = "Widget", PinnedMessages = "PinnedMessages", Timeline = "Timeline", + Extensions = "Extensions", Room3pidMemberInfo = "Room3pidMemberInfo", diff --git a/src/stores/room-list/previews/PollStartEventPreview.ts b/src/stores/room-list/previews/PollStartEventPreview.ts index 793a823071..728256101d 100644 --- a/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/src/stores/room-list/previews/PollStartEventPreview.ts @@ -26,7 +26,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; export class PollStartEventPreview implements IPreview { public static contextType = MatrixClientContext; - public context!: React.ContextType; + public declare context: React.ContextType; public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts index 36b6e2b6fc..593f181843 100644 --- a/src/stores/widgets/ElementWidgetActions.ts +++ b/src/stores/widgets/ElementWidgetActions.ts @@ -21,10 +21,6 @@ export enum ElementWidgetActions { JoinCall = "io.element.join", HangupCall = "im.vector.hangup", CallParticipants = "io.element.participants", - MuteAudio = "io.element.mute_audio", - UnmuteAudio = "io.element.unmute_audio", - MuteVideo = "io.element.mute_video", - UnmuteVideo = "io.element.unmute_video", StartLiveStream = "im.vector.start_live_stream", // Actions for switching layouts @@ -32,11 +28,28 @@ export enum ElementWidgetActions { SpotlightLayout = "io.element.spotlight_layout", OpenIntegrationManager = "integration_manager_open", - /** * @deprecated Use MSC2931 instead */ ViewRoom = "io.element.view_room", + + // This action type is used as a `fromWidget` and a `toWidget` action. + // fromWidget: updates the client about the current device mute state + // toWidget: the client requests a specific device mute configuration + // The reply will always be the resulting configuration + // It is possible to sent an empty configuration to retrieve the current values or + // just one of the fields to update that particular value + // An undefined field means that EC will keep the mute state as is. + // -> this will allow the client to only get the current state + // + // The data of the widget action request and the response are: + // { + // audio_enabled?: boolean, + // video_enabled?: boolean + // } + // NOTE: this is currently unused. Its only here to make EW aware + // of this action so it does not throw errors. + DeviceMute = "io.element.device_mute", } export interface IHangupCallApiRequest extends IWidgetApiRequest { diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index cce1ca18a6..6bb3e887da 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -19,6 +19,7 @@ import { EventDirection, IOpenIDCredentials, IOpenIDUpdate, + ISendDelayedEventDetails, ISendEventDetails, ITurnServer, IReadEventRelationsResult, @@ -33,6 +34,7 @@ import { WidgetKind, ISearchUserDirectoryResult, IGetMediaConfigResult, + UpdateDelayedEventAction, } from "matrix-widget-api"; import { ClientEvent, @@ -43,6 +45,7 @@ import { Room, Direction, THREAD_RELATION_TYPE, + SendDelayedEventResponse, StateEvents, TimelineEvents, } from "matrix-js-sdk/src/matrix"; @@ -128,6 +131,8 @@ export class StopGapWidgetDriver extends WidgetDriver { this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen); this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers); this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`); + this.allowedCapabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent); + this.allowedCapabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent); this.allowedCapabilities.add( WidgetEventCapability.forRoomEvent(EventDirection.Send, "org.matrix.rageshake_request").raw, @@ -160,7 +165,7 @@ export class StopGapWidgetDriver extends WidgetDriver { `_${clientUserId}_${clientDeviceId}`, ).raw, ); - // MSC3779 version, with no leading underscore + // Version with no leading underscore, for room versions whose auth rules allow it this.allowedCapabilities.add( WidgetEventCapability.forStateEvent( EventDirection.Send, @@ -271,20 +276,20 @@ export class StopGapWidgetDriver extends WidgetDriver { public async sendEvent( eventType: K, content: StateEvents[K], - stateKey?: string, - targetRoomId?: string, + stateKey: string | null, + targetRoomId: string | null, ): Promise; public async sendEvent( eventType: K, content: TimelineEvents[K], stateKey: null, - targetRoomId?: string, + targetRoomId: string | null, ): Promise; public async sendEvent( eventType: string, content: IContent, - stateKey?: string | null, - targetRoomId?: string, + stateKey: string | null = null, + targetRoomId: string | null = null, ): Promise { const client = MatrixClientPeg.get(); const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId(); @@ -328,6 +333,94 @@ export class StopGapWidgetDriver extends WidgetDriver { return { roomId, eventId: r.event_id }; } + /** + * @experimental Part of MSC4140 & MSC4157 + * @see {@link WidgetDriver#sendDelayedEvent} + */ + public async sendDelayedEvent( + delay: number | null, + parentDelayId: string | null, + eventType: K, + content: StateEvents[K], + stateKey: string | null, + targetRoomId: string | null, + ): Promise; + /** + * @experimental Part of MSC4140 & MSC4157 + */ + public async sendDelayedEvent( + delay: number | null, + parentDelayId: string | null, + eventType: K, + content: TimelineEvents[K], + stateKey: null, + targetRoomId: string | null, + ): Promise; + public async sendDelayedEvent( + delay: number | null, + parentDelayId: string | null, + eventType: string, + content: IContent, + stateKey: string | null = null, + targetRoomId: string | null = null, + ): Promise { + const client = MatrixClientPeg.get(); + const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId(); + + if (!client || !roomId) throw new Error("Not in a room or not attached to a client"); + + let delayOpts; + if (delay !== null) { + delayOpts = { + delay, + ...(parentDelayId !== null && { parent_delay_id: parentDelayId }), + }; + } else if (parentDelayId !== null) { + delayOpts = { + parent_delay_id: parentDelayId, + }; + } else { + throw new Error("Must provide at least one of delay or parentDelayId"); + } + + let r: SendDelayedEventResponse | null; + if (stateKey !== null) { + // state event + r = await client._unstable_sendDelayedStateEvent( + roomId, + delayOpts, + eventType as keyof StateEvents, + content as StateEvents[keyof StateEvents], + stateKey, + ); + } else { + // message event + r = await client._unstable_sendDelayedEvent( + roomId, + delayOpts, + null, + eventType as keyof TimelineEvents, + content as TimelineEvents[keyof TimelineEvents], + ); + } + + return { + roomId, + delayId: r.delay_id, + }; + } + + /** + * @experimental Part of MSC4140 & MSC4157 + */ + public async updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise { + const client = MatrixClientPeg.get(); + + if (!client) throw new Error("Not in a room or not attached to a client"); + + await client._unstable_updateDelayedEvent(delayId, action); + } + public async sendToDevice( eventType: string, encrypted: boolean, diff --git a/src/toasts/AnalyticsToast.tsx b/src/toasts/AnalyticsToast.tsx index 59c7124d57..b1eadd963d 100644 --- a/src/toasts/AnalyticsToast.tsx +++ b/src/toasts/AnalyticsToast.tsx @@ -87,10 +87,10 @@ export const showToast = (): void => { // them to opt in again. props = { description: _t("analytics|consent_migration"), - acceptLabel: _t("analytics|accept_button"), - onAccept, - rejectLabel: _t("action|learn_more"), - onReject: onLearnMorePreviouslyOptedIn, + primaryLabel: _t("analytics|accept_button"), + onPrimaryClick: onAccept, + secondaryLabel: _t("action|learn_more"), + onSecondaryClick: onLearnMorePreviouslyOptedIn, }; } else if (legacyAnalyticsOptIn === null || legacyAnalyticsOptIn === undefined) { // The user had no analytics setting previously set, so we just need to prompt to opt-in, rather than @@ -102,10 +102,10 @@ export const showToast = (): void => { ); props = { description: _t("analytics|learn_more", {}, { LearnMoreLink: learnMoreLink }), - acceptLabel: _t("action|yes"), - onAccept, - rejectLabel: _t("action|no"), - onReject, + primaryLabel: _t("action|yes"), + onPrimaryClick: onAccept, + secondaryLabel: _t("action|no"), + onSecondaryClick: onReject, }; } else { // false diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index 1019d717e9..878ce7b306 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -44,10 +44,10 @@ export const showToast = (deviceIds: Set): void => { icon: "verification_warning", props: { description: _t("encryption|verification|unverified_sessions_toast_description"), - acceptLabel: _t("action|review"), - onAccept, - rejectLabel: _t("encryption|verification|unverified_sessions_toast_reject"), - onReject, + primaryLabel: _t("action|review"), + onPrimaryClick: onAccept, + secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"), + onSecondaryClick: onReject, }, component: GenericToast, priority: 50, diff --git a/src/toasts/DesktopNotificationsToast.ts b/src/toasts/DesktopNotificationsToast.ts index ba8340ca3a..676d79842d 100644 --- a/src/toasts/DesktopNotificationsToast.ts +++ b/src/toasts/DesktopNotificationsToast.ts @@ -20,9 +20,11 @@ import GenericToast from "../components/views/toasts/GenericToast"; import ToastStore from "../stores/ToastStore"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { getLocalNotificationAccountDataEventType } from "../utils/notifications"; +import SettingsStore from "../settings/SettingsStore"; +import { SettingLevel } from "../settings/SettingLevel"; -const onAccept = (): void => { - Notifier.setEnabled(true); +const onAccept = async (): Promise => { + await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, true); const cli = MatrixClientPeg.safeGet(); const eventType = getLocalNotificationAccountDataEventType(cli.deviceId!); cli.setAccountData(eventType, { @@ -44,10 +46,10 @@ export const showToast = (fromMessageSend: boolean): void => { : _t("notifications|enable_prompt_toast_title"), props: { description: _t("notifications|enable_prompt_toast_description"), - acceptLabel: _t("action|enable"), - onAccept, - rejectLabel: _t("action|dismiss"), - onReject, + primaryLabel: _t("action|enable"), + onPrimaryClick: onAccept, + secondaryLabel: _t("action|dismiss"), + onSecondaryClick: onReject, }, component: GenericToast, priority: 30, diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 2546a76d08..d42042c1cb 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { useCallback, useEffect, useState } from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { Button, Tooltip } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; @@ -35,7 +35,7 @@ import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall"; import AccessibleButton, { ButtonEvent } from "../components/views/elements/AccessibleButton"; import { useDispatcher } from "../hooks/useDispatcher"; import { ActionPayload } from "../dispatcher/payloads"; -import { Call } from "../models/Call"; +import { Call, CallEvent } from "../models/Call"; import LegacyCallHandler, { AudioID } from "../LegacyCallHandler"; import { useEventEmitter } from "../hooks/useEventEmitter"; import { CallStore, CallStoreEvent } from "../stores/CallStore"; @@ -111,6 +111,16 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { [dismissToast, notifyEvent], ); + // Dismiss if antother device from this user joins. + const onParticipantChange = useCallback( + (participants: Map>, prevParticipants: Map>) => { + if (Array.from(participants.keys()).some((p) => p.userId == room?.client.getUserId())) { + dismissToast(); + } + }, + [dismissToast, room?.client], + ); + // Dismiss on timeout. useEffect(() => { const timeout = setTimeout(dismissToast, MAX_RING_TIME_MS); @@ -158,6 +168,7 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { ); useEventEmitter(CallStore.instance, CallStoreEvent.Call, onCall); + useEventEmitter(call ?? undefined, CallEvent.Participants, onParticipantChange); return ( <> diff --git a/src/toasts/MobileGuideToast.ts b/src/toasts/MobileGuideToast.ts index 3288f5a95e..5d4ecd2bb1 100644 --- a/src/toasts/MobileGuideToast.ts +++ b/src/toasts/MobileGuideToast.ts @@ -45,10 +45,10 @@ export const showToast = (): void => { title: _t("mobile_guide|toast_title"), props: { description: _t("mobile_guide|toast_description", { brand }), - acceptLabel: _t("mobile_guide|toast_accept"), - onAccept, - rejectLabel: _t("action|dismiss"), - onReject, + primaryLabel: _t("mobile_guide|toast_accept"), + onPrimaryClick: onAccept, + secondaryLabel: _t("action|dismiss"), + onSecondaryClick: onReject, }, component: GenericToast, priority: 99, diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx index 8e297270fe..a2ea19217a 100644 --- a/src/toasts/ServerLimitToast.tsx +++ b/src/toasts/ServerLimitToast.tsx @@ -47,8 +47,8 @@ export const showToast = ( {errorText} {contactText} ), - acceptLabel: _t("action|ok"), - onAccept: () => { + primaryLabel: _t("action|ok"), + onPrimaryClick: () => { hideToast(); if (onHideToast) onHideToast(); }, diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 72b82d208c..900962df37 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -118,10 +118,11 @@ export const showToast = (kind: Kind): void => { icon: getIcon(kind), props: { description: getDescription(kind), - acceptLabel: getSetupCaption(kind), - onAccept, - rejectLabel: _t("encryption|verification|unverified_sessions_toast_reject"), - onReject, + primaryLabel: getSetupCaption(kind), + onPrimaryClick: onAccept, + secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"), + onSecondaryClick: onReject, + destructive: "secondary", }, component: GenericToast, priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40, diff --git a/src/toasts/UnverifiedSessionToast.tsx b/src/toasts/UnverifiedSessionToast.tsx index e7a38edeea..a934ae9bda 100644 --- a/src/toasts/UnverifiedSessionToast.tsx +++ b/src/toasts/UnverifiedSessionToast.tsx @@ -59,10 +59,11 @@ export const showToast = async (deviceId: string): Promise => { props: { description: device.display_name, detail: , - acceptLabel: _t("encryption|verification|unverified_session_toast_accept"), - onAccept, - rejectLabel: _t("action|no"), - onReject, + primaryLabel: _t("encryption|verification|unverified_session_toast_accept"), + onPrimaryClick: onAccept, + secondaryLabel: _t("action|no"), + onSecondaryClick: onReject, + destructive: "secondary", }, component: GenericToast, priority: 80, diff --git a/src/toasts/UpdateToast.tsx b/src/toasts/UpdateToast.tsx index 4a4f66602b..5e08fb56ab 100644 --- a/src/toasts/UpdateToast.tsx +++ b/src/toasts/UpdateToast.tsx @@ -83,10 +83,10 @@ export const showToast = (version: string, newVersion: string, releaseNotes?: st title: _t("update|toast_title", { brand }), props: { description: _t("update|toast_description", { brand }), - acceptLabel, - onAccept, - rejectLabel: _t("action|dismiss"), - onReject, + primaryLabel: acceptLabel, + onPrimaryClick: onAccept, + secondaryLabel: _t("action|dismiss"), + onSecondaryClick: onReject, }, component: GenericToast, priority: 20, diff --git a/src/utils/MegolmExportEncryption.ts b/src/utils/MegolmExportEncryption.ts index de6ffea5f1..fc0958c179 100644 --- a/src/utils/MegolmExportEncryption.ts +++ b/src/utils/MegolmExportEncryption.ts @@ -20,7 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../languageHandler"; import SdkConfig from "../SdkConfig"; -const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; +const subtleCrypto = window.crypto.subtle; /** * Make an Error object which has a friendlyText property which is already diff --git a/src/utils/MessageDiffUtils.tsx b/src/utils/MessageDiffUtils.tsx index ff0c42e1a1..6cc6a59e77 100644 --- a/src/utils/MessageDiffUtils.tsx +++ b/src/utils/MessageDiffUtils.tsx @@ -22,7 +22,7 @@ import { IContent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { unescape } from "lodash"; -import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils"; +import { bodyToHtml, checkBlockNode, EventRenderOpts } from "../HtmlUtils"; function textToHtml(text: string): string { const container = document.createElement("div"); @@ -31,9 +31,8 @@ function textToHtml(text: string): string { } function getSanitizedHtmlBody(content: IContent): string { - const opts: IOptsReturnString = { + const opts: EventRenderOpts = { stripReplyFallback: true, - returnString: true, }; if (content.format === "org.matrix.custom.html") { return bodyToHtml(content, null, opts); diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 3272a14e4e..40712f633a 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { useCallback, useEffect, useState } from "react"; import { base32 } from "rfc4648"; import { IWidget, IWidgetData } from "matrix-widget-api"; import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; @@ -32,8 +33,10 @@ import { WidgetType } from "../widgets/WidgetType"; import { Jitsi } from "../widgets/Jitsi"; import { objectClone } from "./objects"; import { _t } from "../languageHandler"; -import { IApp, isAppWidget } from "../stores/WidgetStore"; +import WidgetStore, { IApp, isAppWidget } from "../stores/WidgetStore"; import { parseUrl } from "./UrlUtils"; +import { useEventEmitter } from "../hooks/useEventEmitter"; +import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; // How long we wait for the state event echo to come back from the server // before waitFor[Room/User]Widget rejects its promise @@ -562,3 +565,22 @@ export default class WidgetUtils { return false; } } + +/** + * Hook to get the widgets for a room and update when they change + * @param room the room to get widgets for + */ +export const useWidgets = (room: Room): IApp[] => { + const [apps, setApps] = useState(() => WidgetStore.instance.getApps(room.roomId)); + + const updateApps = useCallback(() => { + // Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings + setApps([...WidgetStore.instance.getApps(room.roomId)]); + }, [room]); + + useEffect(updateApps, [room, updateApps]); + useEventEmitter(WidgetStore.instance, room.roomId, updateApps); + useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps); + + return apps; +}; diff --git a/src/utils/device/parseUserAgent.ts b/src/utils/device/parseUserAgent.ts index 724ef617da..2d36b1209d 100644 --- a/src/utils/device/parseUserAgent.ts +++ b/src/utils/device/parseUserAgent.ts @@ -42,15 +42,15 @@ const getDeviceType = ( browser: UAParser.IBrowser, operatingSystem: UAParser.IOS, ): DeviceType => { + if (device.type === "mobile" || operatingSystem.name?.includes("Android") || userAgent.indexOf(IOS_KEYWORD) > -1) { + return DeviceType.Mobile; + } if (browser.name === "Electron") { return DeviceType.Desktop; } if (!!browser.name) { return DeviceType.Web; } - if (device.type === "mobile" || operatingSystem.name?.includes("Android") || userAgent.indexOf(IOS_KEYWORD) > -1) { - return DeviceType.Mobile; - } return DeviceType.Unknown; }; diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index 537494b26b..1c2526b49e 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -274,26 +274,48 @@ export class RoomPermalinkCreator { }; } -export function makeGenericPermalink(entityId: string): string { - return getPermalinkConstructor().forEntity(entityId); +/** + * Creates a permalink for an Entity. If isPill is set it uses a spec-compliant + * prefix for the permalink, instead of permalink_prefix + * @param {string} entityId The entity to link to. + * @param {boolean} isPill Link should be pillifyable. + * @returns {string|null} The transformed permalink or null if unable. + */ +export function makeGenericPermalink(entityId: string, isPill = false): string { + return getPermalinkConstructor(isPill).forEntity(entityId); } -export function makeUserPermalink(userId: string): string { - return getPermalinkConstructor().forUser(userId); +/** + * Creates a permalink for a User. If isPill is set it uses a spec-compliant + * prefix for the permalink, instead of permalink_prefix + * @param {string} userId The user to link to. + * @param {boolean} isPill Link should be pillifyable. + * @returns {string|null} The transformed permalink or null if unable. + */ +export function makeUserPermalink(userId: string, isPill = false): string { + return getPermalinkConstructor(isPill).forUser(userId); } -export function makeRoomPermalink(matrixClient: MatrixClient, roomId: string): string { +/** + * Creates a permalink for a room. If isPill is set it uses a spec-compliant + * prefix for the permalink, instead of permalink_prefix + * @param {MatrixClient} matrixClient The MatrixClient to use + * @param {string} roomId The user to link to. + * @param {boolean} isPill Link should be pillifyable. + * @returns {string|null} The transformed permalink or null if unable. + */ +export function makeRoomPermalink(matrixClient: MatrixClient, roomId: string, isPill = false): string { if (!roomId) { throw new Error("can't permalink a falsy roomId"); } // If the roomId isn't actually a room ID, don't try to list the servers. // Aliases are already routable, and don't need extra information. - if (roomId[0] !== "!") return getPermalinkConstructor().forRoom(roomId, []); + if (roomId[0] !== "!") return getPermalinkConstructor(isPill).forRoom(roomId, []); const room = matrixClient.getRoom(roomId); if (!room) { - return getPermalinkConstructor().forRoom(roomId, []); + return getPermalinkConstructor(isPill).forRoom(roomId, []); } const permalinkCreator = new RoomPermalinkCreator(room); permalinkCreator.load(); @@ -414,9 +436,15 @@ export function getPrimaryPermalinkEntity(permalink: string): string | null { return null; } -function getPermalinkConstructor(): PermalinkConstructor { +/** + * Returns the correct PermalinkConstructor based on permalink_prefix + * and isPill + * @param {boolean} isPill Should constructed links be pillifyable. + * @returns {string|null} The transformed permalink or null if unable. + */ +function getPermalinkConstructor(isPill = false): PermalinkConstructor { const elementPrefix = SdkConfig.get("permalink_prefix"); - if (elementPrefix && elementPrefix !== matrixtoBaseUrl) { + if (elementPrefix && elementPrefix !== matrixtoBaseUrl && !isPill) { return new ElementPermalinkConstructor(elementPrefix); } diff --git a/src/utils/tokens/pickling.ts b/src/utils/tokens/pickling.ts index 9e096bedef..f1739145a0 100644 --- a/src/utils/tokens/pickling.ts +++ b/src/utils/tokens/pickling.ts @@ -17,7 +17,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/matrix"; +// Note: we don't import the base64 utils from `matrix-js-sdk/src/matrix` because this file +// is used by Element Web's service worker, and importing `matrix` brings in ~1mb of stuff +// we don't need. Instead, we ignore the import restriction and only bring in what we actually +// need. +// Note: `base64` is not public in the js-sdk, so if it changes/breaks, that's on us. We should +// be okay with our frequent tests, locked versioning, etc though. We'll pick up problems well +// before release. +// eslint-disable-next-line no-restricted-imports +import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/base64"; import { logger } from "matrix-js-sdk/src/logger"; /** diff --git a/src/utils/video-rooms.ts b/src/utils/video-rooms.ts index 4e17a39662..eae0ca0cd0 100644 --- a/src/utils/video-rooms.ts +++ b/src/utils/video-rooms.ts @@ -16,6 +16,26 @@ limitations under the License. import type { Room } from "matrix-js-sdk/src/matrix"; import SettingsStore from "../settings/SettingsStore"; +import { useFeatureEnabled } from "../hooks/useSettings"; + +function checkIsVideoRoom(room: Room, elementCallVideoRoomsEnabled: boolean): boolean { + return room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom()); +} export const isVideoRoom = (room: Room): boolean => - room.isElementVideoRoom() || (SettingsStore.getValue("feature_element_call_video_rooms") && room.isCallRoom()); + checkIsVideoRoom(room, SettingsStore.getValue("feature_element_call_video_rooms")); + +/** + * Returns whether the given room is a video room based on the current feature flags. + * @param room The room to check. + * @param skipVideoRoomsEnabledCheck If true, the check for the video rooms feature flag is skipped, + * useful for suggesting to the user to enable the labs flag. + */ +export const useIsVideoRoom = (room?: Room, skipVideoRoomsEnabledCheck = false): boolean => { + const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms"); + const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); // react to updates as isVideoRoom reads the value itself + + if (!room) return false; + if (!videoRoomsEnabled && !skipVideoRoomsEnabledCheck) return false; + return checkIsVideoRoom(room, elementCallVideoRoomsEnabled); +}; diff --git a/test/HtmlUtils-test.tsx b/test/HtmlUtils-test.tsx index ae12a71780..0de866d452 100644 --- a/test/HtmlUtils-test.tsx +++ b/test/HtmlUtils-test.tsx @@ -19,7 +19,7 @@ import { mocked } from "jest-mock"; import { render, screen } from "@testing-library/react"; import { IContent } from "matrix-js-sdk/src/matrix"; -import { bodyToHtml, formatEmojis, topicToHtml } from "../src/HtmlUtils"; +import { bodyToSpan, formatEmojis, topicToHtml } from "../src/HtmlUtils"; import SettingsStore from "../src/settings/SettingsStore"; jest.mock("../src/settings/SettingsStore"); @@ -66,7 +66,7 @@ describe("topicToHtml", () => { describe("bodyToHtml", () => { function getHtml(content: IContent, highlights?: string[]): string { - return (bodyToHtml(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html; + return (bodyToSpan(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html; } it("should apply highlights to HTML messages", () => { @@ -108,14 +108,14 @@ describe("bodyToHtml", () => { }); it("generates big emoji for emoji made of multiple characters", () => { - const { asFragment } = render(bodyToHtml({ body: "👨‍👩‍👧‍👦 ↔️ 🇮🇸", msgtype: "m.text" }, [], {}) as ReactElement); + const { asFragment } = render(bodyToSpan({ body: "👨‍👩‍👧‍👦 ↔️ 🇮🇸", msgtype: "m.text" }, [], {}) as ReactElement); expect(asFragment()).toMatchSnapshot(); }); it("should generate big emoji for an emoji-only reply to a message", () => { const { asFragment } = render( - bodyToHtml( + bodyToSpan( { "body": "> <@sender1:server> Test\n\n🥰", "format": "org.matrix.custom.html", @@ -139,7 +139,7 @@ describe("bodyToHtml", () => { }); it("does not mistake characters in text presentation mode for emoji", () => { - const { asFragment } = render(bodyToHtml({ body: "↔ ❗︎", msgtype: "m.text" }, [], {}) as ReactElement); + const { asFragment } = render(bodyToSpan({ body: "↔ ❗︎", msgtype: "m.text" }, [], {}) as ReactElement); expect(asFragment()).toMatchSnapshot(); }); diff --git a/test/Notifier-test.ts b/test/Notifier-test.ts index a47b6cf5a4..7390415e58 100644 --- a/test/Notifier-test.ts +++ b/test/Notifier-test.ts @@ -26,6 +26,7 @@ import { SyncState, } from "matrix-js-sdk/src/matrix"; import { waitFor } from "@testing-library/react"; +import { CallMembership, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; import BasePlatform from "../src/BasePlatform"; import Notifier from "../src/Notifier"; @@ -139,6 +140,11 @@ describe("Notifier", () => { getRoom: jest.fn(), getPushActionsForEvent: jest.fn(), supportsThreads: jest.fn().mockReturnValue(false), + matrixRTC: { + on: jest.fn(), + off: jest.fn(), + getRoomSession: jest.fn(), + }, }); mockClient.pushRules = { @@ -455,6 +461,35 @@ describe("Notifier", () => { expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled(); }); + it("should not show toast when group call is already connected", () => { + setGroupCallsEnabled(true); + const spyCallMemberships = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom").mockReturnValue([ + new CallMembership( + mkEvent({ + event: true, + room: testRoom.roomId, + user: userId, + type: EventType.GroupCallMemberPrefix, + content: {}, + }), + { + call_id: "123", + application: "m.call", + focus_active: { type: "livekit" }, + foci_preferred: [], + device_id: "DEVICE", + }, + ), + ]); + + const roomSession = MatrixRTCSession.roomSessionForRoom(mockClient, testRoom); + + mockClient.matrixRTC.getRoomSession.mockReturnValue(roomSession); + emitCallNotifyEvent(); + expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled(); + spyCallMemberships.mockRestore(); + }); + it("should not show toast when calling with non-group call event", () => { setGroupCallsEnabled(true); diff --git a/test/SupportedBrowser-test.ts b/test/SupportedBrowser-test.ts new file mode 100644 index 0000000000..f5cccf221b --- /dev/null +++ b/test/SupportedBrowser-test.ts @@ -0,0 +1,123 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; + +import { checkBrowserSupport, LOCAL_STORAGE_KEY } from "../src/SupportedBrowser"; +import ToastStore from "../src/stores/ToastStore"; +import GenericToast from "../src/components/views/toasts/GenericToast"; + +jest.mock("matrix-js-sdk/src/logger"); + +describe("SupportedBrowser", () => { + beforeEach(() => { + jest.resetAllMocks(); + localStorage.clear(); + }); + + const testUserAgentFactory = + (expectedWarning?: string) => + async (userAgent: string): Promise => { + const toastSpy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); + const warnLogSpy = jest.spyOn(logger, "warn"); + Object.defineProperty(window, "navigator", { value: { userAgent: userAgent }, writable: true }); + checkBrowserSupport(); + if (expectedWarning) { + expect(warnLogSpy).toHaveBeenCalledWith(expectedWarning, expect.any(String)); + expect(toastSpy).toHaveBeenCalled(); + } else { + expect(warnLogSpy).not.toHaveBeenCalled(); + expect(toastSpy).not.toHaveBeenCalled(); + } + }; + + it.each([ + // Safari on iOS + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", + // Firefox on iOS + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/128.0 Mobile/15E148 Safari/605.1.15", + // Opera on Samsung + "Mozilla/5.0 (Linux; Android 10; SM-G970F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.64 Mobile Safari/537.36 OPR/76.2.4027.73374", + ])("should warn for mobile browsers", testUserAgentFactory("Browser unsupported, unsupported device type")); + + it.each([ + // Chrome on Chrome OS + "Mozilla/5.0 (X11; CrOS x86_64 15633.69.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.212 Safari/537.36", + // Opera on Windows + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 OPR/113.0.0.0", + // Vivaldi on Linux + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Vivaldi/6.8.3381.48", + // IE11 on Windows 10 + "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko", + // Firefox 115 on macOS + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_4_5; rv:115.0) Gecko/20000101 Firefox/115.0", + ])( + "should warn for unsupported desktop browsers", + testUserAgentFactory("Browser unsupported, unsupported user agent"), + ); + + it.each([ + // Safari 17.5 on macOS Sonoma + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15", + // Firefox 127 on macOS Sonoma + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0", + // Edge 126 on Windows + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/126.0.2592.113", + // Edge 126 on macOS + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/126.0.2592.113", + // Firefox 128 on Windows + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0", + // Firefox 128 on Linux + "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0", + // Chrome 127 on Windows + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", + ])("should not warn for supported browsers", testUserAgentFactory()); + + it.each([ + // Element Nightly on macOS + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2024072501 Chrome/126.0.6478.127 Electron/31.2.1 Safari/537.36", + ])("should not warn for Element Desktop", testUserAgentFactory()); + + it.each(["AppleTV11,1/11.1"])( + "should handle unknown user agent sanely", + testUserAgentFactory("Browser unsupported, unknown client"), + ); + + it("should not warn for unsupported browser if user accepted already", async () => { + const toastSpy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); + const warnLogSpy = jest.spyOn(logger, "warn"); + const userAgent = + "Mozilla/5.0 (X11; CrOS x86_64 15633.69.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.212 Safari/537.36"; + Object.defineProperty(window, "navigator", { value: { userAgent: userAgent }, writable: true }); + + checkBrowserSupport(); + expect(warnLogSpy).toHaveBeenCalledWith("Browser unsupported, unsupported user agent", expect.any(String)); + expect(toastSpy).toHaveBeenCalledWith( + expect.objectContaining({ + component: GenericToast, + title: "Element does not support this browser", + }), + ); + + localStorage.setItem(LOCAL_STORAGE_KEY, String(true)); + toastSpy.mockClear(); + warnLogSpy.mockClear(); + + checkBrowserSupport(); + expect(warnLogSpy).toHaveBeenCalledWith("Browser unsupported, but user has previously accepted"); + expect(toastSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/test/TestSdkContext.ts b/test/TestSdkContext.ts index 5aad5bcfa5..154881db53 100644 --- a/test/TestSdkContext.ts +++ b/test/TestSdkContext.ts @@ -35,18 +35,18 @@ import { * replace individual stores. This is useful for tests which need to mock out stores. */ export class TestSdkContext extends SdkContextClass { - public _RightPanelStore?: RightPanelStore; - public _RoomNotificationStateStore?: RoomNotificationStateStore; - public _RoomViewStore?: RoomViewStore; - public _WidgetPermissionStore?: WidgetPermissionStore; - public _WidgetLayoutStore?: WidgetLayoutStore; - public _WidgetStore?: WidgetStore; - public _PosthogAnalytics?: PosthogAnalytics; - public _SlidingSyncManager?: SlidingSyncManager; - public _SpaceStore?: SpaceStoreClass; - public _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; - public _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; - public _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; + public declare _RightPanelStore?: RightPanelStore; + public declare _RoomNotificationStateStore?: RoomNotificationStateStore; + public declare _RoomViewStore?: RoomViewStore; + public declare _WidgetPermissionStore?: WidgetPermissionStore; + public declare _WidgetLayoutStore?: WidgetLayoutStore; + public declare _WidgetStore?: WidgetStore; + public declare _PosthogAnalytics?: PosthogAnalytics; + public declare _SlidingSyncManager?: SlidingSyncManager; + public declare _SpaceStore?: SpaceStoreClass; + public declare _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; + public declare _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; + public declare _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; constructor() { super(); diff --git a/test/__snapshots__/HtmlUtils-test.tsx.snap b/test/__snapshots__/HtmlUtils-test.tsx.snap index c69eaa7d95..1a6e6dfe48 100644 --- a/test/__snapshots__/HtmlUtils-test.tsx.snap +++ b/test/__snapshots__/HtmlUtils-test.tsx.snap @@ -3,7 +3,7 @@ exports[`bodyToHtml does not mistake characters in text presentation mode for emoji 1`] = ` ↔ ❗︎ @@ -22,7 +22,7 @@ exports[`bodyToHtml feature_latex_maths should render inline katex 1`] = `"hello exports[`bodyToHtml generates big emoji for emoji made of multiple characters 1`] = ` ({ completeAuthorizationCodeGrant: jest.fn(), @@ -598,6 +599,41 @@ describe("", () => { expect(screen.getByText(`Welcome ${userId}`)).toBeInTheDocument(); }); + describe("clean up drafts", () => { + const roomId = "!room:server.org"; + const unknownRoomId = "!room2:server.org"; + const room = new Room(roomId, mockClient, userId); + const timestamp = 2345678901234; + beforeEach(() => { + localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content"); + localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content"); + mockClient.getRoom.mockImplementation((id) => [room].find((room) => room.roomId === id) || null); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it("should clean up drafts", async () => { + Date.now = jest.fn(() => timestamp); + localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content"); + localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content"); + await getComponentAndWaitForReady(); + mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing); + // let things settle + await flushPromises(); + expect(localStorage.getItem(`mx_cider_state_${roomId}`)).not.toBeNull(); + expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).toBeNull(); + }); + + it("should not clean up drafts before expiry", async () => { + // Set the last cleanup to the recent past + localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content"); + localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(timestamp - 100)); + await getComponentAndWaitForReady(); + mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing); + expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).not.toBeNull(); + }); + }); + describe("onAction()", () => { beforeEach(() => { jest.spyOn(defaultDispatcher, "dispatch").mockClear(); @@ -1380,7 +1416,6 @@ describe("", () => { it("while we are checking the sync store", async () => { const rendered = getComponent({}); - await flushPromises(); expect(rendered.getByTestId("spinner")).toBeInTheDocument(); // now a third session starts diff --git a/test/components/structures/UserMenu-test.tsx b/test/components/structures/UserMenu-test.tsx index 24b75a87d1..6e490ad531 100644 --- a/test/components/structures/UserMenu-test.tsx +++ b/test/components/structures/UserMenu-test.tsx @@ -128,7 +128,7 @@ describe("", () => { const spy = jest.spyOn(defaultDispatcher, "dispatch"); screen.getByRole("button", { name: /User menu/i }).click(); - screen.getByRole("menuitem", { name: /Sign out/i }).click(); + (await screen.findByRole("menuitem", { name: /Sign out/i })).click(); await waitFor(() => { expect(spy).toHaveBeenCalledWith({ action: "logout" }); }); @@ -152,7 +152,7 @@ describe("", () => { const spy = jest.spyOn(defaultDispatcher, "dispatch"); screen.getByRole("button", { name: /User menu/i }).click(); - screen.getByRole("menuitem", { name: /Sign out/i }).click(); + (await screen.findByRole("menuitem", { name: /Sign out/i })).click(); await waitFor(() => { expect(spy).toHaveBeenCalledWith({ action: "logout" }); }); @@ -178,7 +178,7 @@ describe("", () => { const spy = jest.spyOn(Modal, "createDialog"); screen.getByRole("button", { name: /User menu/i }).click(); - screen.getByRole("menuitem", { name: /Sign out/i }).click(); + (await screen.findByRole("menuitem", { name: /Sign out/i })).click(); await waitFor(() => { expect(spy).toHaveBeenCalledWith(LogoutDialog); diff --git a/test/components/views/avatars/WithPresenceIndicator-test.tsx b/test/components/views/avatars/WithPresenceIndicator-test.tsx new file mode 100644 index 0000000000..27088a0399 --- /dev/null +++ b/test/components/views/avatars/WithPresenceIndicator-test.tsx @@ -0,0 +1,110 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { render, waitFor } from "@testing-library/react"; +import { mocked } from "jest-mock"; +import { MatrixClient, PendingEventOrdering, Room, RoomMember, User } from "matrix-js-sdk/src/matrix"; +import React from "react"; +import userEvent from "@testing-library/user-event"; + +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { stubClient } from "../../../test-utils"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import WithPresenceIndicator from "../../../../src/components/views/avatars/WithPresenceIndicator"; +import { isPresenceEnabled } from "../../../../src/utils/presence"; + +jest.mock("../../../../src/utils/presence"); + +jest.mock("../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({ + getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([1, 2]), +})); + +describe("WithPresenceIndicator", () => { + const ROOM_ID = "roomId"; + + let mockClient: MatrixClient; + let room: Room; + + function renderComponent() { + return render( + + + , + ); + } + + beforeEach(() => { + stubClient(); + mockClient = mocked(MatrixClientPeg.safeGet()); + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + const dmRoomMap = { + getUserIdForRoomId: jest.fn(), + } as unknown as DMRoomMap; + jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("renders only child if presence is disabled", async () => { + mocked(isPresenceEnabled).mockReturnValue(false); + const { container } = renderComponent(); + + expect(container.children).toHaveLength(1); + expect(container.children[0].tagName).toBe("SPAN"); + }); + + it.each([ + ["online", "Online"], + ["offline", "Offline"], + ["unavailable", "Away"], + ])("renders presence indicator with tooltip for DM rooms", async (presenceStr, renderedStr) => { + mocked(isPresenceEnabled).mockReturnValue(true); + const DM_USER_ID = "@bob:foo.bar"; + const dmRoomMap = { + getUserIdForRoomId: () => { + return DM_USER_ID; + }, + } as unknown as DMRoomMap; + jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap); + room.getMember = jest.fn((userId) => { + const member = new RoomMember(room.roomId, userId); + member.user = new User(userId); + member.user.presence = presenceStr; + return member; + }); + + const { container, asFragment } = renderComponent(); + + const presence = container.querySelector(".mx_WithPresenceIndicator_icon")!; + expect(presence).toBeVisible(); + await userEvent.hover(presence!); + + // wait for the tooltip to open + const tooltip = await waitFor(() => { + const tooltip = document.getElementById(presence.getAttribute("aria-describedby")!); + expect(tooltip).toBeVisible(); + return tooltip; + }); + expect(tooltip).toHaveTextContent(renderedStr); + + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/avatars/__snapshots__/WithPresenceIndicator-test.tsx.snap b/test/components/views/avatars/__snapshots__/WithPresenceIndicator-test.tsx.snap new file mode 100644 index 0000000000..aefa78f271 --- /dev/null +++ b/test/components/views/avatars/__snapshots__/WithPresenceIndicator-test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 1`] = ` + +
+ +
+
+ +`; + +exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 2`] = ` + +
+ +
+
+ +`; + +exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 3`] = ` + +
+ +
+
+ +`; diff --git a/test/components/views/context_menus/MessageContextMenu-test.tsx b/test/components/views/context_menus/MessageContextMenu-test.tsx index 8fd2f9f416..2be71e39cb 100644 --- a/test/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/components/views/context_menus/MessageContextMenu-test.tsx @@ -535,7 +535,7 @@ function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial>, + props?: Partial, context?: Partial, ): RenderResult { // XXX: We probably shouldn't be assuming all events are going to be message events, but considering this @@ -552,7 +552,7 @@ function makeDefaultRoom(): Room { function createMenu( mxEvent: MatrixEvent, - props?: Partial>, + props?: Partial, context: Partial = {}, beacons: Map = new Map(), room: Room = makeDefaultRoom(), diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx b/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx index 00b7242d96..1475908bb0 100644 --- a/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx +++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx @@ -129,11 +129,11 @@ describe("AccessSecretStorageDialog", () => { expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey); await submitDialog(); - expect( - screen.getByText( + await expect( + screen.findByText( "👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.", ), - ).toBeInTheDocument(); + ).resolves.toBeInTheDocument(); expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus(); }); diff --git a/test/components/views/dialogs/InviteDialog-test.tsx b/test/components/views/dialogs/InviteDialog-test.tsx index 4e1dca4193..e55dbf050a 100644 --- a/test/components/views/dialogs/InviteDialog-test.tsx +++ b/test/components/views/dialogs/InviteDialog-test.tsx @@ -429,7 +429,7 @@ describe("InviteDialog", () => { describe("when clicking »Start DM anyway«", () => { beforeEach(async () => { - await userEvent.click(screen.getByRole("button", { name: "Start DM anyway", exact: true })); + await userEvent.click(screen.getByRole("button", { name: "Start DM anyway" })); }); it("should start the DM", () => { diff --git a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx index 2bd388103d..f306c856f6 100644 --- a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx +++ b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { render, RenderResult } from "@testing-library/react"; +import { render, RenderResult, waitForElementToBeRemoved } from "@testing-library/react"; import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; @@ -39,6 +39,7 @@ describe("", () => { async function renderComponent(): Promise { const result = render(); + await waitForElementToBeRemoved(() => result.queryByRole("progressbar")); await flushPromises(); return result; } diff --git a/test/components/views/dialogs/RoomSettingsDialog-test.tsx b/test/components/views/dialogs/RoomSettingsDialog-test.tsx index a94e35dc71..4d53e714c7 100644 --- a/test/components/views/dialogs/RoomSettingsDialog-test.tsx +++ b/test/components/views/dialogs/RoomSettingsDialog-test.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { EventTimeline, EventType, @@ -129,7 +129,7 @@ describe("", () => { expect(screen.getByTestId("settings-tab-ROOM_PEOPLE_TAB")).toBeInTheDocument(); }); - it("re-renders on room join rule changes", () => { + it("re-renders on room join rule changes", async () => { jest.spyOn(SettingsStore, "getValue").mockImplementation( (setting) => setting === "feature_ask_to_join", ); @@ -142,7 +142,9 @@ describe("", () => { room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, null, ); - expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(); + await waitFor(() => + expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(), + ); }); }); @@ -183,7 +185,7 @@ describe("", () => { it("displays poll history when tab clicked", () => { const { container } = getComponent(); - fireEvent.click(screen.getByText("Poll history")); + fireEvent.click(screen.getByText("Polls")); expect(container.querySelector(".mx_SettingsTab")).toMatchSnapshot(); }); diff --git a/test/components/views/dialogs/UnpinAllDialog-test.tsx b/test/components/views/dialogs/UnpinAllDialog-test.tsx new file mode 100644 index 0000000000..95018cc72d --- /dev/null +++ b/test/components/views/dialogs/UnpinAllDialog-test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { EventType } from "matrix-js-sdk/src/matrix"; + +import { UnpinAllDialog } from "../../../../src/components/views/dialogs/UnpinAllDialog"; +import { createTestClient } from "../../../test-utils"; + +describe("", () => { + const client = createTestClient(); + const roomId = "!room:example.org"; + + function renderDialog(onFinished = jest.fn()) { + return render(); + } + + it("should render", () => { + const { asFragment } = renderDialog(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should remove all pinned events when clicked on Continue", async () => { + const onFinished = jest.fn(); + renderDialog(onFinished); + + await userEvent.click(screen.getByText("Continue")); + expect(client.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPinnedEvents, { pinned: [] }, ""); + expect(onFinished).toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/dialogs/UserSettingsDialog-test.tsx b/test/components/views/dialogs/UserSettingsDialog-test.tsx index f404b7f208..9da7f7ccff 100644 --- a/test/components/views/dialogs/UserSettingsDialog-test.tsx +++ b/test/components/views/dialogs/UserSettingsDialog-test.tsx @@ -57,14 +57,9 @@ jest.mock("../../../../src/settings/SettingsStore", () => ({ settingIsOveriddenAtConfigLevel: jest.fn(), })); -jest.mock("../../../../src/SdkConfig", () => ({ - get: jest.fn(), -})); - describe("", () => { const userId = "@alice:server.org"; const mockSettingsStore = mocked(SettingsStore); - const mockSdkConfig = mocked(SdkConfig); let mockClient!: MockedObject; let sdkContext: SdkContextClass; @@ -89,7 +84,8 @@ describe("", () => { mockSettingsStore.getValue.mockReturnValue(false); mockSettingsStore.getValueAt.mockReturnValue(false); mockSettingsStore.getFeatureSettingNames.mockReturnValue([]); - mockSdkConfig.get.mockReturnValue({ brand: "Test" }); + SdkConfig.reset(); + SdkConfig.put({ brand: "Test" }); }); const getActiveTabLabel = (container: Element) => @@ -98,7 +94,7 @@ describe("", () => { it("should render general settings tab when no initialTabId", () => { const { container } = render(getComponent()); - expect(getActiveTabLabel(container)).toEqual("General"); + expect(getActiveTabLabel(container)).toEqual("Account"); }); it("should render initial tab when initialTabId is set", () => { @@ -111,10 +107,13 @@ describe("", () => { // mjolnir tab is only rendered in some configs const { container } = render(getComponent({ initialTabId: UserTab.Mjolnir })); - expect(getActiveTabLabel(container)).toEqual("General"); + expect(getActiveTabLabel(container)).toEqual("Account"); }); it("renders tabs correctly", () => { + SdkConfig.add({ + show_labs_settings: true, + }); const { container } = render(getComponent()); expect(container.querySelectorAll(".mx_TabbedView_tabLabel")).toMatchSnapshot(); }); @@ -181,7 +180,7 @@ describe("", () => { expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Voice & Video"); }); - it("renders with secutity tab selected", () => { + it("renders with security tab selected", () => { const { container } = render(getComponent({ initialTabId: UserTab.Security })); expect(getActiveTabLabel(container)).toEqual("Security & Privacy"); @@ -189,18 +188,8 @@ describe("", () => { }); it("renders with labs tab selected", () => { - // @ts-ignore I give up trying to get the types right here - // why do we have functions that return different things depending on what they're passed? - mockSdkConfig.get.mockImplementation((x) => { - const mockConfig = { show_labs_settings: true, brand: "Test" }; - switch (x) { - case "show_labs_settings": - case "brand": - // @ts-ignore - return mockConfig[x]; - default: - return mockConfig; - } + SdkConfig.add({ + show_labs_settings: true, }); const { container } = render(getComponent({ initialTabId: UserTab.Labs })); @@ -223,8 +212,9 @@ describe("", () => { }); it("renders labs tab when show_labs_settings is enabled in config", () => { - // @ts-ignore simplified test stub - mockSdkConfig.get.mockImplementation((configName) => configName === "show_labs_settings"); + SdkConfig.add({ + show_labs_settings: true, + }); const { getByTestId } = render(getComponent()); expect(getByTestId(`settings-tab-${UserTab.Labs}`)).toBeTruthy(); }); @@ -238,7 +228,7 @@ describe("", () => { expect(getByTestId(`settings-tab-${UserTab.Labs}`)).toBeTruthy(); }); - it("watches settings", () => { + it("watches settings", async () => { const watchSettingCallbacks: Record = {}; mockSettingsStore.watchSetting.mockImplementation((settingName, roomId, callback) => { @@ -247,7 +237,7 @@ describe("", () => { }); mockSettingsStore.getValue.mockReturnValue(false); - const { queryByTestId, unmount } = render(getComponent()); + const { queryByTestId, findByTestId, unmount } = render(getComponent()); expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeFalsy(); expect(mockSettingsStore.watchSetting).toHaveBeenCalledWith("feature_mjolnir", null, expect.anything()); @@ -257,7 +247,7 @@ describe("", () => { watchSettingCallbacks["feature_mjolnir"]("feature_mjolnir", "", SettingLevel.ACCOUNT, true, true); // tab is rendered now - expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeTruthy(); + await expect(findByTestId(`settings-tab-${UserTab.Mjolnir}`)).resolves.toBeTruthy(); unmount(); diff --git a/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap index a455e4c59a..43f46d2edc 100644 --- a/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap +++ b/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap @@ -77,7 +77,7 @@ exports[` should match the snapshot 1`] = ` class="mx_EventTile_content" > My Great Massage @@ -291,7 +291,7 @@ exports[` should support events with 1`] = ` class="mx_EventTile_content" > My Great Missage diff --git a/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap index 6c148400d8..8f60a60c08 100644 --- a/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap +++ b/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap @@ -89,7 +89,7 @@ NodeList [ class="mx_TabbedView_tabLabel_text" id="mx_tabpanel_ROOM_POLL_HISTORY_TAB_label" > - Poll history + Polls , ] @@ -105,7 +105,7 @@ exports[` poll history displays poll history when tab clic

- Poll history + Polls

should render 1`] = ` + +
+ `; exports[` renders formatted m.text correctly pills appear for an MXID permalink 1`] = ` - Chat with @@ -81,7 +105,7 @@ exports[` renders formatted m.text correctly pills appear for an - +
`; exports[` renders formatted m.text correctly pills appear for event permalinks without a custom label 1`] = ` @@ -89,8 +113,8 @@ exports[` renders formatted m.text correctly pills appear for eve
- See this message @@ -128,7 +152,7 @@ exports[` renders formatted m.text correctly pills appear for eve - +
`; @@ -138,8 +162,8 @@ exports[` renders formatted m.text correctly pills appear for roo
- A @@ -178,7 +202,7 @@ exports[` renders formatted m.text correctly pills appear for roo with vias - +
`; @@ -188,8 +212,8 @@ exports[` renders formatted m.text correctly pills do not appear
- An @@ -200,14 +224,14 @@ exports[` renders formatted m.text correctly pills do not appear event link with text - +
`; exports[` renders formatted m.text correctly pills do not appear in code blocks 1`] = ` -

@@ -242,12 +266,12 @@ exports[` renders formatted m.text correctly pills do not appear

-
+
`; exports[` renders formatted m.text correctly pills get injected correctly into the DOM 1`] = ` - Hey @@ -285,16 +309,87 @@ exports[` renders formatted m.text correctly pills get injected c +
+`; + +exports[` renders formatted m.text correctly renders formatted body without html correctly 1`] = ` +
+ escaped *markdown* +
+`; + +exports[` renders formatted m.text correctly spoilers get injected properly into the DOM 1`] = ` +
+ Hey + + + +
+`; + +exports[` renders m.emote correctly 1`] = ` + + winks `; +exports[` renders m.notice correctly 1`] = ` +
+ this is a notice, probably from a bot +
+`; + +exports[` renders plain-text m.text correctly linkification get applied correctly into the DOM 1`] = ` + +`; + exports[` renders plain-text m.text correctly should pillify a permalink to a message in the same room with the label »Message from Member« 1`] = `"Visit Message from Member"`; exports[` renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit Message in Room 2"`; exports[` renders plain-text m.text correctly should pillify a permalink to an unknown message in the same room with the label »Message« 1`] = ` - Visit @@ -315,5 +410,14 @@ exports[` renders plain-text m.text correctly should pillify a pe -
+
+`; + +exports[` renders plain-text m.text correctly simple message renders as expected 1`] = ` +
+ this is a plaintext message +
`; diff --git a/test/components/views/polls/pollHistory/PollHistory-test.tsx b/test/components/views/polls/pollHistory/PollHistory-test.tsx index 2a5fc80168..29c1b5d7cf 100644 --- a/test/components/views/polls/pollHistory/PollHistory-test.tsx +++ b/test/components/views/polls/pollHistory/PollHistory-test.tsx @@ -24,6 +24,7 @@ import { getMockClientWithEventEmitter, makePollEndEvent, makePollStartEvent, + mockClientMethodsRooms, mockClientMethodsUser, mockIntlDateTimeFormat, setupRoomWithPollEvents, @@ -41,7 +42,7 @@ describe("", () => { const roomId = "!room:domain.org"; const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), - getRoom: jest.fn(), + ...mockClientMethodsRooms([]), relations: jest.fn(), decryptEventIfNeeded: jest.fn(), getOrCreateFilter: jest.fn(), @@ -117,7 +118,7 @@ describe("", () => { expect(getByText("Loading polls")).toBeInTheDocument(); // flush filter creation request - await flushPromises(); + await act(flushPromises); expect(liveTimeline.getPaginationToken).toHaveBeenCalledWith(EventTimeline.BACKWARDS); expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(liveTimeline, { backwards: true }); @@ -147,7 +148,7 @@ describe("", () => { ); // flush filter creation request - await flushPromises(); + await act(flushPromises); // once per page expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3); @@ -182,7 +183,7 @@ describe("", () => { it("renders a no polls message when there are no active polls in the room", async () => { const { getByText } = getComponent(); - await flushPromises(); + await act(flushPromises); expect(getByText("There are no active polls in this room")).toBeTruthy(); }); @@ -320,7 +321,7 @@ describe("", () => { fireEvent.click(getByText("Question?")); - expect(queryByText("Poll history")).not.toBeInTheDocument(); + expect(queryByText("Polls")).not.toBeInTheDocument(); // elements from MPollBody expect(getByText("Question?")).toMatchSnapshot(); expect(getByText("Socks")).toBeInTheDocument(); @@ -396,13 +397,13 @@ describe("", () => { expect(getByText("Question?")).toBeInTheDocument(); // header not shown - expect(queryByText("Poll history")).not.toBeInTheDocument(); + expect(queryByText("Polls")).not.toBeInTheDocument(); expect(getByText("Active polls")).toMatchSnapshot(); fireEvent.click(getByText("Active polls")); // main list header displayed again - expect(getByText("Poll history")).toBeInTheDocument(); + expect(getByText("Polls")).toBeInTheDocument(); // active filter still active expect(getByTestId("filter-tab-PollHistory_filter-ACTIVE").firstElementChild).toBeChecked(); // list displayed diff --git a/test/components/views/polls/pollHistory/PollListItemEnded-test.tsx b/test/components/views/polls/pollHistory/PollListItemEnded-test.tsx index 7bf27ee447..a2fc6cc9b1 100644 --- a/test/components/views/polls/pollHistory/PollListItemEnded-test.tsx +++ b/test/components/views/polls/pollHistory/PollListItemEnded-test.tsx @@ -163,7 +163,7 @@ describe("", () => { await setupRoomWithPollEvents([pollStartEvent], responses, [pollEndEvent], mockClient, room); const poll = room.polls.get(pollId)!; - const { getByText, queryByText } = getComponent({ event: pollStartEvent, poll }); + const { getByText, queryByText, findByText } = getComponent({ event: pollStartEvent, poll }); // fetch relations await flushPromises(); @@ -174,7 +174,7 @@ describe("", () => { ]); // updated with more responses - expect(getByText("Final result based on 3 votes")).toBeInTheDocument(); + await expect(findByText("Final result based on 3 votes")).resolves.toBeInTheDocument(); expect(getByText("Nissan Silvia S15")).toBeInTheDocument(); expect(queryByText("Mitsubishi Lancer Evolution IX")).not.toBeInTheDocument(); }); diff --git a/test/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap b/test/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap index 70f66bb803..f78f2e4642 100644 --- a/test/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap +++ b/test/components/views/polls/pollHistory/__snapshots__/PollHistory-test.tsx.snap @@ -37,7 +37,7 @@ exports[` renders a list of active polls when there are polls in

- Poll history + Polls

", () => { + let client: Mocked; + let room: Room; + + beforeEach(() => { + client = mocked(stubClient()); + room = new Room("!room:server", client, client.getSafeUserId()); + mocked(WidgetUtils.getWidgetName).mockImplementation((app) => app?.name ?? "No Name"); + }); + + it("should render empty state", () => { + mocked(useWidgets).mockReturnValue([]); + const { asFragment } = render(); + expect(screen.getByText("Boost productivity with more tools, widgets and bots")).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render widgets", async () => { + mocked(useWidgets).mockReturnValue([ + { + id: "id", + roomId: room.roomId, + eventId: "$event1", + creatorUserId: client.getSafeUserId(), + type: MatrixWidgetType.Custom, + name: "Custom Widget", + url: "http://url1", + }, + { + id: "jitsi", + roomId: room.roomId, + eventId: "$event2", + creatorUserId: client.getSafeUserId(), + type: MatrixWidgetType.JitsiMeet, + name: "Jitsi", + url: "http://jitsi", + }, + ] satisfies IApp[]); + + const { asFragment } = render(); + expect(screen.getByText("Custom Widget")).toBeInTheDocument(); + expect(screen.getByText("Jitsi")).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should show context menu on widget row", async () => { + jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true); + mocked(useWidgets).mockReturnValue([ + { + id: "id", + roomId: room.roomId, + eventId: "$event1", + creatorUserId: client.getSafeUserId(), + type: MatrixWidgetType.Custom, + name: "Custom Widget", + url: "http://url1", + }, + ] satisfies IApp[]); + + const { container } = render(); + await userEvent.click(container.querySelector(".mx_ExtensionsCard_app_options")!); + expect(document.querySelector(".mx_IconizedContextMenu")).toMatchSnapshot(); + }); + + it("should show set room layout button", async () => { + jest.spyOn(WidgetLayoutStore.instance, "canCopyLayoutToRoom").mockReturnValue(true); + mocked(useWidgets).mockReturnValue([ + { + id: "id", + roomId: room.roomId, + eventId: "$event1", + creatorUserId: client.getSafeUserId(), + type: MatrixWidgetType.Custom, + name: "Custom Widget", + url: "http://url1", + }, + ] satisfies IApp[]); + + render(); + expect(screen.getByText("Set layout for everyone")).toBeInTheDocument(); + }); + + it("should show widget as pinned", async () => { + jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true); + mocked(useWidgets).mockReturnValue([ + { + id: "id", + roomId: room.roomId, + eventId: "$event1", + creatorUserId: client.getSafeUserId(), + type: MatrixWidgetType.Custom, + name: "Custom Widget", + url: "http://url1", + }, + ] satisfies IApp[]); + + render(); + expect(screen.getByText("Custom Widget").closest(".mx_ExtensionsCard_Button_pinned")).toBeInTheDocument(); + }); + + it("should show cannot pin warning", async () => { + jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false); + jest.spyOn(WidgetLayoutStore.instance, "canAddToContainer").mockReturnValue(false); + mocked(useWidgets).mockReturnValue([ + { + id: "id", + roomId: room.roomId, + eventId: "$event1", + creatorUserId: client.getSafeUserId(), + type: MatrixWidgetType.Custom, + name: "Custom Widget", + url: "http://url1", + }, + ] satisfies IApp[]); + + render(); + expect(screen.getByLabelText("You can only pin up to 3 widgets")).toBeInTheDocument(); + }); + + it("should should open integration manager on click", async () => { + jest.spyOn(IntegrationManagers.sharedInstance(), "hasManager").mockReturnValue(false); + const spy = jest.spyOn(IntegrationManagers.sharedInstance(), "openNoManagerDialog"); + render(); + await userEvent.click(screen.getByText("Add extensions")); + expect(spy).toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/right_panel/LegacyRoomHeaderButtons-test.tsx b/test/components/views/right_panel/LegacyRoomHeaderButtons-test.tsx index ef42956217..8f352d0fd5 100644 --- a/test/components/views/right_panel/LegacyRoomHeaderButtons-test.tsx +++ b/test/components/views/right_panel/LegacyRoomHeaderButtons-test.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import { MatrixEvent, MsgType, @@ -79,21 +79,23 @@ describe("LegacyRoomHeaderButtons-test.tsx", function () { expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); }); - it("thread notification does change the thread button", () => { + it("thread notification does change the thread button", async () => { const { container } = getComponent(room); expect(getThreadButton(container)!.className.includes("mx_LegacyRoomHeader_button--unread")).toBeFalsy(); room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1); - expect(getThreadButton(container)!.className.includes("mx_LegacyRoomHeader_button--unread")).toBeTruthy(); - expect(isIndicatorOfType(container, "notification")).toBe(true); + await waitFor(() => { + expect(getThreadButton(container)!.className.includes("mx_LegacyRoomHeader_button--unread")).toBeTruthy(); + expect(isIndicatorOfType(container, "notification")).toBe(true); + }); room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1); - expect(isIndicatorOfType(container, "highlight")).toBe(true); + await waitFor(() => expect(isIndicatorOfType(container, "highlight")).toBe(true)); room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 0); room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 0); - expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); + await waitFor(() => expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull()); }); it("thread activity does change the thread button", async () => { @@ -122,7 +124,7 @@ describe("LegacyRoomHeaderButtons-test.tsx", function () { }, }); room.addReceipt(receipt); - expect(isIndicatorOfType(container, "activity")).toBe(true); + await waitFor(() => expect(isIndicatorOfType(container, "activity")).toBe(true)); // Sending the last event should clear the notification. let event = mkEvent({ @@ -140,7 +142,7 @@ describe("LegacyRoomHeaderButtons-test.tsx", function () { }, }); room.addLiveEvents([event]); - expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); + await waitFor(() => expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull()); // Mark it as unread again. event = mkEvent({ @@ -158,7 +160,7 @@ describe("LegacyRoomHeaderButtons-test.tsx", function () { }, }); room.addLiveEvents([event]); - expect(isIndicatorOfType(container, "activity")).toBe(true); + await waitFor(() => expect(isIndicatorOfType(container, "activity")).toBe(true)); // Sending a read receipt on an earlier event shouldn't do anything. receipt = new MatrixEvent({ @@ -173,7 +175,7 @@ describe("LegacyRoomHeaderButtons-test.tsx", function () { }, }); room.addReceipt(receipt); - expect(isIndicatorOfType(container, "activity")).toBe(true); + await waitFor(() => expect(isIndicatorOfType(container, "activity")).toBe(true)); // Sending a receipt on the latest event should clear the notification. receipt = new MatrixEvent({ @@ -188,6 +190,6 @@ describe("LegacyRoomHeaderButtons-test.tsx", function () { }, }); room.addReceipt(receipt); - expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); + await waitFor(() => expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull()); }); }); diff --git a/test/components/views/right_panel/PinnedMessagesCard-test.tsx b/test/components/views/right_panel/PinnedMessagesCard-test.tsx index d773b51fb9..64961ca144 100644 --- a/test/components/views/right_panel/PinnedMessagesCard-test.tsx +++ b/test/components/views/right_panel/PinnedMessagesCard-test.tsx @@ -15,37 +15,44 @@ limitations under the License. */ import React from "react"; -import { render, act, RenderResult, fireEvent, waitForElementToBeRemoved, screen } from "@testing-library/react"; -import { mocked } from "jest-mock"; +import { render, act, RenderResult, waitForElementToBeRemoved, screen } from "@testing-library/react"; +import { mocked, MockedObject } from "jest-mock"; import { MatrixEvent, RoomStateEvent, IEvent, Room, - EventTimelineSet, IMinimalEvent, EventType, RelationType, MsgType, M_POLL_KIND_DISCLOSED, + EventTimeline, + MatrixClient, } from "matrix-js-sdk/src/matrix"; import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent"; import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent"; import { sleep } from "matrix-js-sdk/src/utils"; +import userEvent from "@testing-library/user-event"; import { stubClient, mkEvent, mkMessage, flushPromises } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import PinnedMessagesCard from "../../../../src/components/views/right_panel/PinnedMessagesCard"; +import { PinnedMessagesCard } from "../../../../src/components/views/right_panel/PinnedMessagesCard"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; +import Modal from "../../../../src/Modal"; +import { UnpinAllDialog } from "../../../../src/components/views/dialogs/UnpinAllDialog"; describe("", () => { - stubClient(); - const cli = mocked(MatrixClientPeg.safeGet()); - cli.getUserId.mockReturnValue("@alice:example.org"); - cli.setRoomAccountData.mockResolvedValue({}); - cli.relations.mockResolvedValue({ originalEvent: {} as unknown as MatrixEvent, events: [] }); + let cli: MockedObject; + beforeEach(() => { + stubClient(); + cli = mocked(MatrixClientPeg.safeGet()); + cli.getUserId.mockReturnValue("@alice:example.org"); + cli.setRoomAccountData.mockResolvedValue({}); + cli.relations.mockResolvedValue({ originalEvent: {} as unknown as MatrixEvent, events: [] }); + }); const mkRoom = (localPins: MatrixEvent[], nonLocalPins: MatrixEvent[]): Room => { const room = new Room("!room:example.org", cli, "@me:example.org"); @@ -53,27 +60,27 @@ describe("", () => { const pins = () => [...localPins, ...nonLocalPins]; // Insert pin IDs into room state - jest.spyOn(room.currentState, "getStateEvents").mockImplementation((): any => - mkEvent({ - event: true, - type: EventType.RoomPinnedEvents, - content: { - pinned: pins().map((e) => e.getId()), - }, - user: "@user:example.org", - room: "!room:example.org", - }), + jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "getStateEvents").mockImplementation( + (): any => + mkEvent({ + event: true, + type: EventType.RoomPinnedEvents, + content: { + pinned: pins().map((e) => e.getId()), + }, + user: "@user:example.org", + room: "!room:example.org", + }), ); - jest.spyOn(room.currentState, "on"); - - // Insert local pins into local timeline set - room.getUnfilteredTimelineSet = () => - ({ - getTimelineForEvent: () => ({ - getEvents: () => localPins, - }), - }) as unknown as EventTimelineSet; + jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "mayClientSendStateEvent").mockReturnValue( + true, + ); + // poll end event validates against this + jest.spyOn( + room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, + "maySendRedactionForEvent", + ).mockReturnValue(true); // Return all pins over fetchRoomEvent cli.fetchRoomEvent.mockImplementation((roomId, eventId) => { @@ -86,38 +93,73 @@ describe("", () => { return room; }; - const mountPins = async (room: Room): Promise => { - let pins!: RenderResult; - await act(async () => { - pins = render( - - - , - ); - // Wait a tick for state updates - await sleep(0); - }); - - return pins; - }; + async function renderMessagePinList(room: Room): Promise { + const renderResult = render( + + + , + ); + // Wait a tick for state updates + await act(() => sleep(0)); - const emitPinUpdates = async (room: Room) => { - const pinListener = mocked(room.currentState).on.mock.calls.find( - ([eventName, listener]) => eventName === RoomStateEvent.Events, - )![1]; + return renderResult; + } + /** + * + * @param room + */ + async function emitPinUpdate(room: Room) { await act(async () => { - // Emit the update - // @ts-ignore what is going on here? - pinListener(room.currentState.getStateEvents()); - // Wait a tick for state updates - await sleep(0); + const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!; + roomState.emit( + RoomStateEvent.Events, + new MatrixEvent({ type: EventType.RoomPinnedEvents }), + roomState, + null, + ); }); - }; + } + + /** + * Initialize the pinned messages card with the given pinned messages. + * Return the room, testing library helpers and functions to add and remove pinned messages. + * @param localPins + * @param nonLocalPins + */ + async function initPinnedMessagesCard(localPins: MatrixEvent[], nonLocalPins: MatrixEvent[]) { + const room = mkRoom(localPins, nonLocalPins); + const addLocalPinEvent = async (event: MatrixEvent) => { + localPins.push(event); + await emitPinUpdate(room); + }; + const removeLastLocalPinEvent = async () => { + localPins.pop(); + await emitPinUpdate(room); + }; + const addNonLocalPinEvent = async (event: MatrixEvent) => { + nonLocalPins.push(event); + await emitPinUpdate(room); + }; + const removeLastNonLocalPinEvent = async () => { + nonLocalPins.pop(); + await emitPinUpdate(room); + }; + const renderResult = await renderMessagePinList(room); + + return { + ...renderResult, + addLocalPinEvent, + removeLastLocalPinEvent, + addNonLocalPinEvent, + removeLastNonLocalPinEvent, + room, + }; + } const pin1 = mkMessage({ event: true, @@ -132,75 +174,66 @@ describe("", () => { msg: "The second one", }); - it("updates when messages are pinned", async () => { + it("should show spinner whilst loading", async () => { + const room = mkRoom([], [pin1]); + render( + + + , + ); + + await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar")); + }); + + it("should show the empty state when there are no pins", async () => { + const { asFragment } = await initPinnedMessagesCard([], []); + + expect(screen.getByText("Pin important messages so that they can be easily discovered")).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should show two pinned messages", async () => { + //const room = mkRoom([pin1], [pin2]); + const { asFragment } = await initPinnedMessagesCard([pin1], [pin2]); + + expect(screen.queryAllByRole("listitem")).toHaveLength(2); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should updates when messages are pinned", async () => { // Start with nothing pinned - const localPins: MatrixEvent[] = []; - const nonLocalPins: MatrixEvent[] = []; - const room = mkRoom(localPins, nonLocalPins); - const pins = await mountPins(room); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0); + const { addLocalPinEvent, addNonLocalPinEvent } = await initPinnedMessagesCard([], []); + + expect(screen.queryAllByRole("listitem")).toHaveLength(0); // Pin the first message - localPins.push(pin1); - await emitPinUpdates(room); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(1); + await addLocalPinEvent(pin1); + expect(screen.getAllByRole("listitem")).toHaveLength(1); // Pin the second message - nonLocalPins.push(pin2); - await emitPinUpdates(room); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(2); + await addNonLocalPinEvent(pin2); + expect(screen.getAllByRole("listitem")).toHaveLength(2); }); - it("updates when messages are unpinned", async () => { + it("should updates when messages are unpinned", async () => { // Start with two pins - const localPins = [pin1]; - const nonLocalPins = [pin2]; - const room = mkRoom(localPins, nonLocalPins); - const pins = await mountPins(room); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(2); + const { removeLastLocalPinEvent, removeLastNonLocalPinEvent } = await initPinnedMessagesCard([pin1], [pin2]); + expect(screen.getAllByRole("listitem")).toHaveLength(2); // Unpin the first message - localPins.pop(); - await emitPinUpdates(room); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(1); + await removeLastLocalPinEvent(); + expect(screen.getAllByRole("listitem")).toHaveLength(1); // Unpin the second message - nonLocalPins.pop(); - await emitPinUpdates(room); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0); + await removeLastNonLocalPinEvent(); + expect(screen.queryAllByRole("listitem")).toHaveLength(0); }); - it("hides unpinnable events found in local timeline", async () => { - // Redacted messages are unpinnable - const pin = mkEvent({ - event: true, - type: EventType.RoomMessage, - content: {}, - unsigned: { redacted_because: {} as unknown as IEvent }, - room: "!room:example.org", - user: "@alice:example.org", - }); - - const pins = await mountPins(mkRoom([pin], [])); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0); - }); - - it("hides unpinnable events not found in local timeline", async () => { - // Redacted messages are unpinnable - const pin = mkEvent({ - event: true, - type: EventType.RoomMessage, - content: {}, - unsigned: { redacted_because: {} as unknown as IEvent }, - room: "!room:example.org", - user: "@alice:example.org", - }); - - const pins = await mountPins(mkRoom([], [pin])); - expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0); - }); - - it("accounts for edits", async () => { + it("should display an edited pinned event", async () => { const messageEvent = mkEvent({ event: true, type: EventType.RoomMessage, @@ -224,13 +257,78 @@ describe("", () => { events: [messageEvent], }); - const pins = await mountPins(mkRoom([], [pin1])); - const pinTile = pins.container.querySelectorAll(".mx_PinnedEventTile"); - expect(pinTile.length).toBe(1); - expect(pinTile[0].querySelector(".mx_EventTile_body")!).toHaveTextContent("First pinned message, edited"); + await initPinnedMessagesCard([], [pin1]); + expect(screen.getByText("First pinned message, edited")).toBeInTheDocument(); + }); + + describe("unpinnable event", () => { + it("should hide unpinnable events found in local timeline", async () => { + // Redacted messages are unpinnable + const pin = mkEvent({ + event: true, + type: EventType.RoomMessage, + content: {}, + unsigned: { redacted_because: {} as unknown as IEvent }, + room: "!room:example.org", + user: "@alice:example.org", + }); + await initPinnedMessagesCard([pin], []); + expect(screen.queryAllByRole("listitem")).toHaveLength(0); + }); + + it("hides unpinnable events not found in local timeline", async () => { + // Redacted messages are unpinnable + const pin = mkEvent({ + event: true, + type: EventType.RoomMessage, + content: {}, + unsigned: { redacted_because: {} as unknown as IEvent }, + room: "!room:example.org", + user: "@alice:example.org", + }); + await initPinnedMessagesCard([], [pin]); + expect(screen.queryAllByRole("listitem")).toHaveLength(0); + }); + }); + + describe("unpin all", () => { + it("should not allow to unpinall", async () => { + const room = mkRoom([pin1], [pin2]); + jest.spyOn( + room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, + "mayClientSendStateEvent", + ).mockReturnValue(false); + + const { asFragment } = render( + + + , + ); + + // Wait a tick for state updates + await act(() => sleep(0)); + + expect(screen.queryByText("Unpin all messages")).toBeNull(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should allow unpinning all messages", async () => { + jest.spyOn(Modal, "createDialog"); + + const { room } = await initPinnedMessagesCard([pin1], [pin2]); + expect(screen.getByText("Unpin all messages")).toBeInTheDocument(); + + await userEvent.click(screen.getByText("Unpin all messages")); + // Should open the UnpinAllDialog dialog + expect(Modal.createDialog).toHaveBeenCalledWith(UnpinAllDialog, { roomId: room.roomId, matrixClient: cli }); + }); }); - it("displays votes on polls not found in local timeline", async () => { + it("should displays votes on polls not found in local timeline", async () => { const poll = mkEvent({ ...PollStartEvent.from("A poll", ["Option 1", "Option 2"], M_POLL_KIND_DISCLOSED).serialize(), event: true, @@ -273,11 +371,8 @@ describe("", () => { return { originalEvent: undefined as unknown as MatrixEvent, events: [] }; }); - const room = mkRoom([], [poll]); - // poll end event validates against this - jest.spyOn(room.currentState, "maySendRedactionForEvent").mockReturnValue(true); + const { room } = await initPinnedMessagesCard([], [poll]); - const pins = await mountPins(room); // two pages of results await flushPromises(); await flushPromises(); @@ -285,35 +380,12 @@ describe("", () => { const pollInstance = room.polls.get(poll.getId()!); expect(pollInstance).toBeTruthy(); - const pinTile = pins.container.querySelectorAll(".mx_MPollBody"); - - expect(pinTile).toHaveLength(1); - expect(pinTile[0].querySelectorAll(".mx_PollOption_ended")).toHaveLength(2); - expect(pinTile[0].querySelectorAll(".mx_PollOption_optionVoteCount")[0]).toHaveTextContent("2 votes"); - expect([...pinTile[0].querySelectorAll(".mx_PollOption_optionVoteCount")].at(-1)).toHaveTextContent("1 vote"); - }); - - it("should allow admins to unpin messages", async () => { - const nonLocalPins = [pin1]; - const room = mkRoom([], nonLocalPins); - jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); - const sendStateEvent = jest.spyOn(cli, "sendStateEvent"); - - const pins = await mountPins(room); - const pinTile = pins.container.querySelectorAll(".mx_PinnedEventTile"); - expect(pinTile).toHaveLength(1); - - fireEvent.click(pinTile[0].querySelector(".mx_PinnedEventTile_unpinButton")!); - expect(sendStateEvent).toHaveBeenCalledWith(room.roomId, "m.room.pinned_events", { pinned: [] }, ""); + expect(screen.getByText("A poll")).toBeInTheDocument(); - nonLocalPins.pop(); - await Promise.all([waitForElementToBeRemoved(pinTile[0]), emitPinUpdates(room)]); - }); + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("2 votes")).toBeInTheDocument(); - it("should show spinner whilst loading", async () => { - const room = mkRoom([], [pin1]); - mountPins(room); - const spinner = await screen.findByTestId("spinner"); - await waitForElementToBeRemoved(spinner); + expect(screen.getByText("Option 2")).toBeInTheDocument(); + expect(screen.getByText("1 vote")).toBeInTheDocument(); }); }); diff --git a/test/components/views/right_panel/RightPanelTabs-test.tsx b/test/components/views/right_panel/RightPanelTabs-test.tsx index dae7b1a79a..4f702a46ae 100644 --- a/test/components/views/right_panel/RightPanelTabs-test.tsx +++ b/test/components/views/right_panel/RightPanelTabs-test.tsx @@ -38,8 +38,8 @@ describe("", () => { const { container } = render(); expect(container).toMatchSnapshot(); // Assert that the active tab is Info - expect(container.querySelectorAll("[aria-selected='true'").length).toEqual(1); - expect(container.querySelector("[aria-selected='true'")).toHaveAccessibleName("People"); + expect(container.querySelectorAll("[aria-selected='true']").length).toEqual(1); + expect(container.querySelector("[aria-selected='true']")).toHaveAccessibleName("People"); }); it("Renders nothing for some phases, eg: FilePanel", () => { diff --git a/test/components/views/right_panel/RoomSummaryCard-test.tsx b/test/components/views/right_panel/RoomSummaryCard-test.tsx index 1ddea76382..db49ad5da8 100644 --- a/test/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/components/views/right_panel/RoomSummaryCard-test.tsx @@ -125,7 +125,7 @@ describe("", () => { expect(container).toMatchSnapshot(); }); - it("has button to edit topic when expanded", () => { + it("has button to edit topic", () => { room.currentState.setStateEvents([ new MatrixEvent({ type: "m.room.topic", @@ -138,7 +138,6 @@ describe("", () => { }), ]); const { container, getByText } = getComponent(); - fireEvent.click(screen.getByText("This is the room's topic.")); expect(getByText("Edit")).toBeInTheDocument(); expect(container).toMatchSnapshot(); }); @@ -272,7 +271,7 @@ describe("", () => { mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_pinning"); const { getByText } = getComponent(); - expect(getByText("Pinned")).toBeInTheDocument(); + expect(getByText("Pinned messages")).toBeInTheDocument(); }); }); @@ -280,14 +279,14 @@ describe("", () => { it("renders poll history option", () => { const { getByText } = getComponent(); - expect(getByText("Poll history")).toBeInTheDocument(); + expect(getByText("Polls")).toBeInTheDocument(); }); it("opens poll history dialog on button click", () => { const permalinkCreator = new RoomPermalinkCreator(room); const { getByText } = getComponent({ permalinkCreator }); - fireEvent.click(getByText("Poll history")); + fireEvent.click(getByText("Polls")); expect(Modal.createDialog).toHaveBeenCalledWith(PollHistoryDialog, { room, diff --git a/test/components/views/right_panel/UserInfo-test.tsx b/test/components/views/right_panel/UserInfo-test.tsx index 3b36468786..a12ce75b2e 100644 --- a/test/components/views/right_panel/UserInfo-test.tsx +++ b/test/components/views/right_panel/UserInfo-test.tsx @@ -287,10 +287,10 @@ describe("", () => { expect(spy).not.toHaveBeenCalled(); }); - it("renders close button correctly when encryption panel with a pending verification request", () => { + it("renders close button correctly when encryption panel with a pending verification request", async () => { renderComponent({ phase: RightPanelPhases.EncryptionPanel, verificationRequest }); screen.getByTestId("base-card-close-button").focus(); - expect(screen.getByRole("tooltip")).toHaveTextContent("Cancel"); + await expect(screen.findByRole("tooltip", { name: "Cancel" })).resolves.toBeInTheDocument(); }); }); @@ -927,19 +927,19 @@ describe("", () => { }); }); - it("when call to client.getRoom is null, does not show read receipt button", () => { + it("when call to client.getRoom is null, shows disabled read receipt button", () => { mockClient.getRoom.mockReturnValueOnce(null); renderComponent(); - expect(screen.queryByRole("button", { name: "Jump to read receipt" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Jump to read receipt" })).toBeDisabled(); }); - it("when call to client.getRoom is non-null and room.getEventReadUpTo is null, does not show read receipt button", () => { + it("when call to client.getRoom is non-null and room.getEventReadUpTo is null, shows disabled read receipt button", () => { mockRoom.getEventReadUpTo.mockReturnValueOnce(null); mockClient.getRoom.mockReturnValueOnce(mockRoom); renderComponent(); - expect(screen.queryByRole("button", { name: "Jump to read receipt" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Jump to read receipt" })).toBeDisabled(); }); it("when calls to client.getRoom and room.getEventReadUpTo are non-null, shows read receipt button", () => { diff --git a/test/components/views/right_panel/__snapshots__/ExtensionsCard-test.tsx.snap b/test/components/views/right_panel/__snapshots__/ExtensionsCard-test.tsx.snap new file mode 100644 index 0000000000..2cff7803ac --- /dev/null +++ b/test/components/views/right_panel/__snapshots__/ExtensionsCard-test.tsx.snap @@ -0,0 +1,194 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render empty state 1`] = ` + +
+
+ +
+
+
+
+

+ Boost productivity with more tools, widgets and bots +

+

+ Select “Add extensions” to browse and add extensions to this room +

+
+
+
+ +`; + +exports[` should render widgets 1`] = ` + +
+
+ +
+
+
should render 1`] = `
- Hey you. You're the best! - +
should render 1`] = `
- Hey you. You're the best! - +
", () => { - it("should allow a phone number to be added", async () => { - SdkConfig.add({ - default_country_code: "GB", - }); - - const cli = stubClient(); - const onMsisdnsChange = jest.fn(); - const { asFragment, getByLabelText, getByText } = render( - , - ); - - mocked(cli.requestAdd3pidMsisdnToken).mockResolvedValue({ - sid: "SID", - msisdn: "447900111222", - submit_url: "https://server.url", - success: true, - intl_fmt: "no-clue", - }); - mocked(cli.submitMsisdnTokenOtherUrl).mockResolvedValue({ success: true }); - mocked(cli.addThreePidOnly).mockResolvedValue({}); - - const phoneNumberField = getByLabelText("Phone Number"); - await userEvent.type(phoneNumberField, "7900111222"); - await userEvent.click(getByText("Add")); - - expect(cli.requestAdd3pidMsisdnToken).toHaveBeenCalledWith("GB", "7900111222", "t35tcl1Ent5ECr3T", 1); - expect(asFragment()).toMatchSnapshot(); - - const verificationCodeField = getByLabelText("Verification code"); - await userEvent.type(verificationCodeField, "123666"); - await userEvent.click(getByText("Continue")); - - expect(cli.submitMsisdnTokenOtherUrl).toHaveBeenCalledWith( - "https://server.url", - "SID", - "t35tcl1Ent5ECr3T", - "123666", - ); - expect(onMsisdnsChange).toHaveBeenCalledWith([{ address: "447900111222", medium: "msisdn" }]); - }); -}); diff --git a/test/components/views/settings/account/__snapshots__/PhoneNumbers-test.tsx.snap b/test/components/views/settings/account/__snapshots__/PhoneNumbers-test.tsx.snap deleted file mode 100644 index 417101d360..0000000000 --- a/test/components/views/settings/account/__snapshots__/PhoneNumbers-test.tsx.snap +++ /dev/null @@ -1,110 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should allow a phone number to be added 1`] = ` - -
-
-
- -
- -
-
- - -
-
-
-
-
- A text message has been sent to +447900111222. Please enter the verification code it contains. -
-
-
-
- - -
-
- Continue -
-
-
-
-`; diff --git a/test/components/views/settings/devices/DeviceDetailHeading-test.tsx b/test/components/views/settings/devices/DeviceDetailHeading-test.tsx index 966c0a4be8..ffc4b8c445 100644 --- a/test/components/views/settings/devices/DeviceDetailHeading-test.tsx +++ b/test/components/views/settings/devices/DeviceDetailHeading-test.tsx @@ -108,7 +108,7 @@ describe("", () => { }); it("toggles out of editing mode when device name is saved successfully", async () => { - const { getByTestId } = render(getComponent()); + const { getByTestId, findByTestId } = render(getComponent()); // start editing fireEvent.click(getByTestId("device-heading-rename-cta")); @@ -118,12 +118,12 @@ describe("", () => { await flushPromisesWithFakeTimers(); // read mode displayed - expect(getByTestId("device-detail-heading")).toBeTruthy(); + await expect(findByTestId("device-detail-heading")).resolves.toBeTruthy(); }); it("displays error when device name fails to save", async () => { const saveDeviceName = jest.fn().mockRejectedValueOnce("oups").mockResolvedValue({}); - const { getByTestId, queryByText, container } = render(getComponent({ saveDeviceName })); + const { getByTestId, queryByText, findByText, container } = render(getComponent({ saveDeviceName })); // start editing fireEvent.click(getByTestId("device-heading-rename-cta")); @@ -136,7 +136,7 @@ describe("", () => { await flushPromisesWithFakeTimers(); // error message displayed - expect(queryByText("Failed to set session name")).toBeTruthy(); + await expect(findByText("Failed to set session name")).resolves.toBeTruthy(); // spinner removed expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy(); diff --git a/test/components/views/settings/devices/FilteredDeviceList-test.tsx b/test/components/views/settings/devices/FilteredDeviceList-test.tsx index c37ea61228..be1ec2aa87 100644 --- a/test/components/views/settings/devices/FilteredDeviceList-test.tsx +++ b/test/components/views/settings/devices/FilteredDeviceList-test.tsx @@ -120,16 +120,15 @@ describe("", () => { }); describe("filtering", () => { - const setFilter = async (container: HTMLElement, option: DeviceSecurityVariation | string) => - await act(async () => { - const dropdown = container.querySelector('[aria-label="Filter devices"]'); + const setFilter = async (container: HTMLElement, option: DeviceSecurityVariation | string) => { + const dropdown = container.querySelector('[aria-label="Filter devices"]'); - fireEvent.click(dropdown as Element); - // tick to let dropdown render - await flushPromises(); + fireEvent.click(dropdown as Element); + // tick to let dropdown render + await flushPromises(); - fireEvent.click(container.querySelector(`#device-list-filter__${option}`) as Element); - }); + fireEvent.click(container.querySelector(`#device-list-filter__${option}`) as Element); + }; it("does not display filter description when filter is falsy", () => { const { container } = render(getComponent({ filter: undefined })); diff --git a/test/components/views/settings/discovery/EmailAddresses-test.tsx b/test/components/views/settings/discovery/EmailAddresses-test.tsx deleted file mode 100644 index 547f802873..0000000000 --- a/test/components/views/settings/discovery/EmailAddresses-test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react"; -import { IThreepid, ThreepidMedium, IRequestTokenResponse, MatrixError } from "matrix-js-sdk/src/matrix"; - -import { TranslationKey, UserFriendlyError } from "../../../../../src/languageHandler"; -import EmailAddresses, { EmailAddress } from "../../../../../src/components/views/settings/discovery/EmailAddresses"; -import { clearAllModals, getMockClientWithEventEmitter } from "../../../../test-utils"; - -const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken"); -jest.mock("../../../../../src/IdentityAuthClient", () => - jest.fn().mockImplementation(() => ({ - getAccessToken: mockGetAccessToken, - })), -); - -const emailThreepidFixture: IThreepid = { - medium: ThreepidMedium.Email, - address: "foo@bar.com", - validated_at: 12345, - added_at: 12342, - bound: false, -}; - -describe("", () => { - const mockClient = getMockClientWithEventEmitter({ - getIdentityServerUrl: jest.fn().mockReturnValue("https://fake-identity-server"), - generateClientSecret: jest.fn(), - requestEmailToken: jest.fn(), - bindThreePid: jest.fn(), - }); - - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(async () => { - jest.useRealTimers(); - await clearAllModals(); - }); - - it("should track props.email.bound changes", async () => { - const { rerender } = render(); - await screen.findByText("Share"); - - rerender( - , - ); - await screen.findByText("Revoke"); - }); - - describe("Email verification share phase", () => { - it("shows translated error message", async () => { - render(); - mockClient.requestEmailToken.mockRejectedValue( - new MatrixError( - { errcode: "M_THREEPID_IN_USE", error: "Some fake MatrixError occured" }, - 400, - "https://fake-url/", - ), - ); - fireEvent.click(screen.getByText("Share")); - - // Expect error dialog/modal to be shown. We have to wait for the UI to transition. - expect(await screen.findByText("This email address is already in use")).toBeInTheDocument(); - }); - }); - - describe("Email verification complete phase", () => { - beforeEach(async () => { - // Start these tests out at the "Complete" phase - render(); - mockClient.requestEmailToken.mockResolvedValue({ sid: "123-fake-sid" } satisfies IRequestTokenResponse); - fireEvent.click(screen.getByText("Share")); - // Then wait for the completion screen to come up - await screen.findByText("Complete"); - }); - - it("Shows error dialog when share completion fails (email not verified yet)", async () => { - mockClient.bindThreePid.mockRejectedValue( - new MatrixError( - { errcode: "M_THREEPID_AUTH_FAILED", error: "Some fake MatrixError occured" }, - 403, - "https://fake-url/", - ), - ); - fireEvent.click(screen.getByText("Complete")); - - // Expect error dialog/modal to be shown. We have to wait for the UI to transition. - // Check the title - expect(await screen.findByText("Your email address hasn't been verified yet")).toBeInTheDocument(); - // Check the description - expect( - await screen.findByText( - "Click the link in the email you received to verify and then click continue again.", - ), - ).toBeInTheDocument(); - }); - - it("Shows error dialog when share completion fails (UserFriendlyError)", async () => { - const fakeErrorText = "Fake UserFriendlyError error in test" as TranslationKey; - mockClient.bindThreePid.mockRejectedValue(new UserFriendlyError(fakeErrorText)); - fireEvent.click(screen.getByText("Complete")); - - // Expect error dialog/modal to be shown. We have to wait for the UI to transition. - // Check the title - expect(await screen.findByText("Unable to verify email address.")).toBeInTheDocument(); - // Check the description - expect(await screen.findByText(fakeErrorText)).toBeInTheDocument(); - }); - - it("Shows error dialog when share completion fails (generic error)", async () => { - const fakeErrorText = "Fake plain error in test"; - mockClient.bindThreePid.mockRejectedValue(new Error(fakeErrorText)); - fireEvent.click(screen.getByText("Complete")); - - // Expect error dialog/modal to be shown. We have to wait for the UI to transition. - // Check the title - expect(await screen.findByText("Unable to verify email address.")).toBeInTheDocument(); - // Check the description - expect(await screen.findByText(fakeErrorText)).toBeInTheDocument(); - }); - }); -}); - -describe("", () => { - it("should render a loader while loading", async () => { - const { container } = render(); - - expect(container).toMatchSnapshot(); - }); - - it("should render email addresses", async () => { - const { container } = render(); - - expect(container).toMatchSnapshot(); - }); - - it("should handle no email addresses", async () => { - const { container } = render(); - - expect(container).toMatchSnapshot(); - }); -}); diff --git a/test/components/views/settings/discovery/PhoneNumbers-test.tsx b/test/components/views/settings/discovery/PhoneNumbers-test.tsx deleted file mode 100644 index 19aede79cd..0000000000 --- a/test/components/views/settings/discovery/PhoneNumbers-test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/matrix"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; - -import PhoneNumbers, { PhoneNumber } from "../../../../../src/components/views/settings/discovery/PhoneNumbers"; -import { stubClient } from "../../../../test-utils"; - -const msisdn: IThreepid = { - medium: ThreepidMedium.Phone, - address: "441111111111", - validated_at: 12345, - added_at: 12342, - bound: false, -}; -describe("", () => { - it("should track props.msisdn.bound changes", async () => { - const { rerender } = render(); - await screen.findByText("Share"); - - rerender(); - await screen.findByText("Revoke"); - }); -}); - -const mockGetAccessToken = jest.fn().mockResolvedValue("$$getAccessToken"); -jest.mock("../../../../../src/IdentityAuthClient", () => - jest.fn().mockImplementation(() => ({ - getAccessToken: mockGetAccessToken, - })), -); - -describe("", () => { - it("should render a loader while loading", async () => { - const { container } = render(); - - expect(container).toMatchSnapshot(); - }); - - it("should render phone numbers", async () => { - const { container } = render(); - - expect(container).toMatchSnapshot(); - }); - - it("should handle no numbers", async () => { - const { container } = render(); - - expect(container).toMatchSnapshot(); - }); - - it("should allow binding msisdn", async () => { - const cli = stubClient(); - const { getByText, getByLabelText, asFragment } = render( - , - ); - - mocked(cli.requestMsisdnToken).mockResolvedValue({ - sid: "SID", - msisdn: "+447900111222", - submit_url: "https://server.url", - success: true, - intl_fmt: "no-clue", - }); - - fireEvent.click(getByText("Share")); - await waitFor(() => - expect(cli.requestMsisdnToken).toHaveBeenCalledWith( - null, - "+441111111111", - "t35tcl1Ent5ECr3T", - 1, - undefined, - "$$getAccessToken", - ), - ); - expect(asFragment()).toMatchSnapshot(); - - const verificationCodeField = getByLabelText("Verification code"); - await userEvent.type(verificationCodeField, "123666{Enter}"); - - expect(cli.submitMsisdnToken).toHaveBeenCalledWith("SID", "t35tcl1Ent5ECr3T", "123666", "$$getAccessToken"); - }); -}); diff --git a/test/components/views/settings/discovery/__snapshots__/EmailAddresses-test.tsx.snap b/test/components/views/settings/discovery/__snapshots__/EmailAddresses-test.tsx.snap deleted file mode 100644 index 536c72e8eb..0000000000 --- a/test/components/views/settings/discovery/__snapshots__/EmailAddresses-test.tsx.snap +++ /dev/null @@ -1,97 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should handle no email addresses 1`] = ` -
-
-
-

- Email addresses -

-
-
-
- Discovery options will appear once you have added an email. -
-
-
-
-`; - -exports[` should render a loader while loading 1`] = ` -
-
-
-

- Email addresses -

-
-
-
-
-
-
-
-
-`; - -exports[` should render email addresses 1`] = ` -
-
-
-

- Email addresses -

-
-
-
- - foo@bar.com - -
- Share -
-
-
-
-
-`; diff --git a/test/components/views/settings/discovery/__snapshots__/PhoneNumbers-test.tsx.snap b/test/components/views/settings/discovery/__snapshots__/PhoneNumbers-test.tsx.snap deleted file mode 100644 index 948ee105b0..0000000000 --- a/test/components/views/settings/discovery/__snapshots__/PhoneNumbers-test.tsx.snap +++ /dev/null @@ -1,163 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should allow binding msisdn 1`] = ` - -
-
-

- Phone numbers -

-
-
-
- - +441111111111 - - - - Please enter verification code sent via text. -
-
-
-
- - -
-
-
-
-
-
-
-`; - -exports[` should handle no numbers 1`] = ` -
-
-
-

- Phone numbers -

-
-
-
- Discovery options will appear once you have added a phone number. -
-
-
-
-`; - -exports[` should render a loader while loading 1`] = ` -
-
-
-

- Phone numbers -

-
-
-
-
-
-
-
-
-`; - -exports[` should render phone numbers 1`] = ` -
-
-
-

- Phone numbers -

-
-
-
- - + - 441111111111 - -
- Share -
-
-
-
-
-`; diff --git a/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx similarity index 69% rename from test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx rename to test/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx index 8627f41063..ecc261d84d 100644 --- a/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/AccountUserSettingsTab-test.tsx @@ -13,10 +13,12 @@ limitations under the License. import { fireEvent, render, screen, within } from "@testing-library/react"; import React from "react"; -import { ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, ThreepidMedium } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import userEvent from "@testing-library/user-event"; +import { MockedObject } from "jest-mock"; -import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab"; +import AccountUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/AccountUserSettingsTab"; import { SdkContextClass, SDKContext } from "../../../../../../src/contexts/SDKContext"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import { @@ -29,28 +31,35 @@ import { import { UIFeature } from "../../../../../../src/settings/UIFeature"; import { OidcClientStore } from "../../../../../../src/stores/oidc/OidcClientStore"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; - -describe("", () => { +import Modal from "../../../../../../src/Modal"; + +let changePasswordOnError: (e: Error) => void; +let changePasswordOnFinished: () => void; + +jest.mock( + "../../../../../../src/components/views/settings/ChangePassword", + () => + ({ onError, onFinished }: { onError: (e: Error) => void; onFinished: () => void }) => { + changePasswordOnError = onError; + changePasswordOnFinished = onFinished; + return ; + }, +); + +describe("", () => { const defaultProps = { closeSettingsFn: jest.fn(), }; const userId = "@alice:server.org"; - const mockClient = getMockClientWithEventEmitter({ - ...mockClientMethodsUser(userId), - ...mockClientMethodsServer(), - getCapabilities: jest.fn(), - getThreePids: jest.fn(), - getIdentityServerUrl: jest.fn(), - deleteThreePid: jest.fn(), - }); + let mockClient: MockedObject; let stores: SdkContextClass; const getComponent = () => ( - + ); @@ -62,6 +71,15 @@ describe("", () => { jest.spyOn(SettingsStore, "getValue").mockRestore(); jest.spyOn(logger, "error").mockRestore(); + mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + getCapabilities: jest.fn(), + getThreePids: jest.fn(), + getIdentityServerUrl: jest.fn(), + deleteThreePid: jest.fn(), + }); + mockClient.getCapabilities.mockResolvedValue({}); mockClient.getThreePids.mockResolvedValue({ threepids: [], @@ -77,6 +95,10 @@ describe("", () => { jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it("does not show account management link when not available", () => { const { queryByTestId } = render(getComponent()); @@ -131,6 +153,52 @@ describe("", () => { expect(screen.getByText("Deactivate Account", { selector: "h2" }).parentElement!).toMatchSnapshot(); }); + it("should display the deactivate account dialog when clicked", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === UIFeature.Deactivate, + ); + + const createDialogFn = jest.fn(); + jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); + + render(getComponent()); + + await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" })); + + expect(createDialogFn).toHaveBeenCalled(); + }); + it("should close settings if account deactivated", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === UIFeature.Deactivate, + ); + + const createDialogFn = jest.fn(); + jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); + + render(getComponent()); + + await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" })); + + createDialogFn.mock.calls[0][1].onFinished(true); + + expect(defaultProps.closeSettingsFn).toHaveBeenCalled(); + }); + it("should not close settings if account not deactivated", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === UIFeature.Deactivate, + ); + + const createDialogFn = jest.fn(); + jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); + + render(getComponent()); + + await userEvent.click(screen.getByRole("button", { name: "Deactivate Account" })); + + createDialogFn.mock.calls[0][1].onFinished(false); + + expect(defaultProps.closeSettingsFn).not.toHaveBeenCalled(); + }); }); describe("3pids", () => { @@ -302,4 +370,53 @@ describe("", () => { }); }); }); + + describe("Password change", () => { + beforeEach(() => { + mockClient.getCapabilities.mockResolvedValue({ + "m.change_password": { + enabled: true, + }, + }); + }); + + it("should display a dialog if password change succeeded", async () => { + const createDialogFn = jest.fn(); + jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); + + render(getComponent()); + + const changeButton = await screen.findByRole("button", { name: "Mock change password" }); + userEvent.click(changeButton); + + expect(changePasswordOnFinished).toBeDefined(); + changePasswordOnFinished(); + + expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), { + title: "Success", + description: "Your password was successfully changed.", + }); + }); + + it("should display an error if password change failed", async () => { + const ERROR_STRING = + "Your password must contain exactly 5 lowercase letters, a box drawing character and the badger emoji."; + + const createDialogFn = jest.fn(); + jest.spyOn(Modal, "createDialog").mockImplementation(createDialogFn); + + render(getComponent()); + + const changeButton = await screen.findByRole("button", { name: "Mock change password" }); + userEvent.click(changeButton); + + expect(changePasswordOnError).toBeDefined(); + changePasswordOnError(new Error(ERROR_STRING)); + + expect(createDialogFn).toHaveBeenCalledWith(expect.anything(), { + title: "Error changing password", + description: ERROR_STRING, + }); + }); + }); }); diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index a88c322361..770b84bef7 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -15,7 +15,16 @@ limitations under the License. */ import React from "react"; -import { act, fireEvent, render, RenderResult, screen } from "@testing-library/react"; +import { + act, + fireEvent, + render, + RenderResult, + screen, + waitFor, + waitForElementToBeRemoved, + within, +} from "@testing-library/react"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { logger } from "matrix-js-sdk/src/logger"; import { CryptoApi, DeviceVerificationStatus, VerificationRequest } from "matrix-js-sdk/src/crypto-api"; @@ -146,7 +155,7 @@ describe("", () => { // open device detail const tile = getByTestId(`device-tile-${deviceId}`); const label = isOpen ? "Hide details" : "Show details"; - const toggle = tile.querySelector(`[aria-label="${label}"]`) as Element; + const toggle = within(tile).getByLabelText(label); fireEvent.click(toggle); }; @@ -165,16 +174,14 @@ describe("", () => { return getByTestId(`device-tile-${deviceId}`); }; - const setFilter = async (container: HTMLElement, option: DeviceSecurityVariation | string) => - await act(async () => { - const dropdown = container.querySelector('[aria-label="Filter devices"]'); + const setFilter = async (container: HTMLElement, option: DeviceSecurityVariation | string) => { + const dropdown = within(container).getByLabelText("Filter devices"); - fireEvent.click(dropdown as Element); - // tick to let dropdown render - await flushPromises(); + fireEvent.click(dropdown); + screen.getByRole("listbox"); - fireEvent.click(container.querySelector(`#device-list-filter__${option}`) as Element); - }); + fireEvent.click(screen.getByTestId(`filter-option-${option}`) as Element); + }; const isDeviceSelected = ( getByTestId: ReturnType["getByTestId"], @@ -920,37 +927,31 @@ describe("", () => { it("deletes a device when interactive auth is not required", async () => { mockClient.deleteMultipleDevices.mockResolvedValue({}); - mockClient.getDevices - .mockResolvedValueOnce({ - devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice], - }) - // pretend it was really deleted on refresh - .mockResolvedValueOnce({ - devices: [alicesDevice, alicesOlderMobileDevice], - }); + mockClient.getDevices.mockResolvedValue({ + devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice], + }); - const { getByTestId } = render(getComponent()); + const { getByTestId, findByTestId } = render(getComponent()); - await act(async () => { - await flushPromises(); + await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar")); + await toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id); + + const signOutButton = await within( + await findByTestId(`device-detail-${alicesMobileDevice.device_id}`), + ).findByTestId("device-detail-sign-out-cta"); + + // pretend it was really deleted on refresh + mockClient.getDevices.mockResolvedValueOnce({ + devices: [alicesDevice, alicesOlderMobileDevice], }); - toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id); + // sign out button is disabled with spinner + const prom = waitFor(() => expect(signOutButton).toHaveAttribute("aria-disabled", "true")); - const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`); - const signOutButton = deviceDetails.querySelector( - '[data-testid="device-detail-sign-out-cta"]', - ) as Element; fireEvent.click(signOutButton); - await confirmSignout(getByTestId); + await prom; - // sign out button is disabled with spinner - expect( - (deviceDetails.querySelector('[data-testid="device-detail-sign-out-cta"]') as Element).getAttribute( - "aria-disabled", - ), - ).toEqual("true"); // delete called expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith( [alicesMobileDevice.device_id], @@ -1008,9 +1009,7 @@ describe("", () => { const { getByTestId, getByLabelText } = render(getComponent()); - await act(async () => { - await flushPromises(); - }); + await act(flushPromises); // reset mock count after initial load mockClient.getDevices.mockClear(); @@ -1570,9 +1569,7 @@ describe("", () => { }); const { getByTestId, container } = render(getComponent()); - await act(async () => { - await flushPromises(); - }); + await act(flushPromises); // filter for inactive sessions await setFilter(container, DeviceSecurityVariation.Inactive); @@ -1765,6 +1762,7 @@ describe("", () => { await flushPromises(); fireEvent.click(getByText("Show QR code")); + await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar")); await expect(findByTestId("login-with-qr")).resolves.toBeTruthy(); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/GeneralUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/AccountUserSettingsTab-test.tsx.snap similarity index 59% rename from test/components/views/settings/tabs/user/__snapshots__/GeneralUserSettingsTab-test.tsx.snap rename to test/components/views/settings/tabs/user/__snapshots__/AccountUserSettingsTab-test.tsx.snap index 08cd795def..6c51cc41ab 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/GeneralUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/AccountUserSettingsTab-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` 3pids should display 3pid email addresses and phone numbers 1`] = ` +exports[` 3pids should display 3pid email addresses and phone numbers 1`] = `
3pids should display 3pid email addresses an class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch" >
test@test.io @@ -42,14 +42,14 @@ exports[` 3pids should display 3pid email addresses an > @@ -66,7 +66,7 @@ exports[` 3pids should display 3pid email addresses an
`; -exports[` 3pids should display 3pid email addresses and phone numbers 2`] = ` +exports[` 3pids should display 3pid email addresses and phone numbers 2`] = `
3pids should display 3pid email addresses an class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch" >
- + 123456789
3pids should display 3pid email addresses an
-
- +
- - - -
+
+ + + +
+
+ Add
-
- Add -
`; -exports[` deactive account should render section when account deactivation feature is enabled 1`] = ` +exports[` deactive account should render section when account deactivation feature is enabled 1`] = `
diff --git a/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap index 7117262071..a8d2de3400 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap @@ -244,12 +244,12 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
- Hey you. You're the best! - +
- Hey you. You're the best! - +
- Hey you. You're the best! - +
The app will reload after selecting another language
diff --git a/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap b/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap index 1e1ed87e6c..ceddf215f1 100644 --- a/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap +++ b/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap @@ -62,7 +62,7 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] = xmlns="http://www.w3.org/2000/svg" >
> = {}): RenderResult => { const propsWithDefaults = { - acceptLabel: "Accept", + primaryLabel: "Accept", description:
Description
, - onAccept: () => {}, - onReject: () => {}, - rejectLabel: "Reject", + onPrimaryClick: () => {}, + onSecondaryClick: () => {}, + secondaryLabel: "Reject", ...props, }; diff --git a/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap b/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap index ee033f70f4..f4a41e25be 100644 --- a/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap +++ b/test/components/views/toasts/__snapshots__/GenericToast-test.tsx.snap @@ -14,20 +14,24 @@ exports[`GenericToast should render as expected with detail content 1`] = ` aria-live="off" class="mx_Toast_buttons" > -
Reject -
-
+
+
@@ -52,20 +56,24 @@ exports[`GenericToast should render as expected without detail content 1`] = ` aria-live="off" class="mx_Toast_buttons" > -
Reject -
-
+
+
diff --git a/test/components/views/toasts/__snapshots__/VerificationRequestToast-test.tsx.snap b/test/components/views/toasts/__snapshots__/VerificationRequestToast-test.tsx.snap index ab302309fc..b427e443cf 100644 --- a/test/components/views/toasts/__snapshots__/VerificationRequestToast-test.tsx.snap +++ b/test/components/views/toasts/__snapshots__/VerificationRequestToast-test.tsx.snap @@ -12,20 +12,24 @@ exports[`VerificationRequestToast should render a cross-user verification 1`] = aria-live="off" class="mx_Toast_buttons" > -
Ignore -
-
+
+
@@ -48,20 +52,24 @@ exports[`VerificationRequestToast should render a self-verification 1`] = ` aria-live="off" class="mx_Toast_buttons" > -
Ignore -
-
+
+
diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts index 8fcf1df586..1024985292 100644 --- a/test/createRoom-test.ts +++ b/test/createRoom-test.ts @@ -17,8 +17,7 @@ limitations under the License. import { mocked, Mocked } from "jest-mock"; import { MatrixClient, Device, Preset, RoomType } from "matrix-js-sdk/src/matrix"; import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg, getMockClientWithEventEmitter } from "./test-utils"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; diff --git a/test/customisations/Media-test.ts b/test/customisations/Media-test.ts new file mode 100644 index 0000000000..d4db3677cf --- /dev/null +++ b/test/customisations/Media-test.ts @@ -0,0 +1,39 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import fetchMockJest from "fetch-mock-jest"; +import { mocked } from "jest-mock"; + +import { mediaFromMxc } from "../../src/customisations/Media"; +import { stubClient } from "../test-utils"; + +describe("Media", () => { + it("should not download error if server returns one", async () => { + const cli = stubClient(); + // eslint-disable-next-line no-restricted-properties + mocked(cli.mxcUrlToHttp).mockImplementation( + (mxc) => `https://matrix.org/_matrix/media/r0/download/${mxc.slice(6)}`, + ); + + fetchMockJest.get("https://matrix.org/_matrix/media/r0/download/matrix.org/1234", { + status: 404, + body: { errcode: "M_NOT_FOUND", error: "Not found" }, + }); + + const media = mediaFromMxc("mxc://matrix.org/1234"); + await expect(media.downloadSource()).rejects.toThrow("Not found"); + }); +}); diff --git a/test/editor/serialize-test.ts b/test/editor/serialize-test.ts index 48644da110..552872eb36 100644 --- a/test/editor/serialize-test.ts +++ b/test/editor/serialize-test.ts @@ -14,10 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { mocked } from "jest-mock"; + import EditorModel from "../../src/editor/model"; import { htmlSerializeIfNeeded } from "../../src/editor/serialize"; import { createPartCreator } from "./mock"; +import { IConfigOptions } from "../../src/IConfigOptions"; import SettingsStore from "../../src/settings/SettingsStore"; +import SdkConfig from "../../src/SdkConfig"; describe("editor/serialize", function () { describe("with markdown", function () { @@ -104,6 +108,32 @@ describe("editor/serialize", function () { const html = htmlSerializeIfNeeded(model, {}); expect(html).toBe('
    \n
  1. foo
  2. \n
\n'); }); + describe("with permalink_prefix set", function () { + const sdkConfigGet = SdkConfig.get; + beforeEach(() => { + jest.spyOn(SdkConfig, "get").mockImplementation((key: keyof IConfigOptions, altCaseName?: string) => { + if (key === "permalink_prefix") { + return "https://element.fs.tld"; + } else return sdkConfigGet(key, altCaseName); + }); + }); + + it("user pill uses matrix.to", function () { + const pc = createPartCreator(); + const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('
Alice'); + }); + it("room pill uses matrix.to", function () { + const pc = createPartCreator(); + const model = new EditorModel([pc.roomPill("#room:hs.tld")], pc); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('#room:hs.tld'); + }); + afterEach(() => { + mocked(SdkConfig.get).mockRestore(); + }); + }); }); describe("with plaintext", function () { diff --git a/test/hooks/useNotificationSettings-test.tsx b/test/hooks/useNotificationSettings-test.tsx index 5b0e490426..f43c43538a 100644 --- a/test/hooks/useNotificationSettings-test.tsx +++ b/test/hooks/useNotificationSettings-test.tsx @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { act } from "@testing-library/react"; import { renderHook } from "@testing-library/react-hooks/dom"; +import { waitFor } from "@testing-library/react"; import { IPushRules, MatrixClient, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; import { useNotificationSettings } from "../../src/hooks/useNotificationSettings"; @@ -68,99 +68,87 @@ describe("useNotificationSettings", () => { }); it("correctly parses model", async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useNotificationSettings(cli)); - expect(result.current.model).toEqual(null); - await waitForNextUpdate(); - expect(result.current.model).toEqual(expectedModel); - expect(result.current.hasPendingChanges).toBeFalsy(); - }); + const { result } = renderHook(() => useNotificationSettings(cli)); + expect(result.current.model).toEqual(null); + await waitFor(() => expect(result.current.model).toEqual(expectedModel)); + expect(result.current.hasPendingChanges).toBeFalsy(); }); it("correctly generates change calls", async () => { - await act(async () => { - const addPushRule = jest.fn(cli.addPushRule); - cli.addPushRule = addPushRule; - const deletePushRule = jest.fn(cli.deletePushRule); - cli.deletePushRule = deletePushRule; - const setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled); - cli.setPushRuleEnabled = setPushRuleEnabled; - const setPushRuleActions = jest.fn(cli.setPushRuleActions); - cli.setPushRuleActions = setPushRuleActions; + const addPushRule = jest.fn(cli.addPushRule); + cli.addPushRule = addPushRule; + const deletePushRule = jest.fn(cli.deletePushRule); + cli.deletePushRule = deletePushRule; + const setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled); + cli.setPushRuleEnabled = setPushRuleEnabled; + const setPushRuleActions = jest.fn(cli.setPushRuleActions); + cli.setPushRuleActions = setPushRuleActions; - const { result, waitForNextUpdate } = renderHook(() => useNotificationSettings(cli)); - expect(result.current.model).toEqual(null); - await waitForNextUpdate(); - expect(result.current.model).toEqual(expectedModel); - expect(result.current.hasPendingChanges).toBeFalsy(); - await result.current.reconcile(DefaultNotificationSettings); - await waitForNextUpdate(); - expect(result.current.hasPendingChanges).toBeFalsy(); - expect(addPushRule).toHaveBeenCalledTimes(0); - expect(deletePushRule).toHaveBeenCalledTimes(9); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justjann3"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nn3"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nne"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Janne"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "J4nne"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Jann3"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jann3"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "j4nne"); - expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "janne"); - expect(setPushRuleEnabled).toHaveBeenCalledTimes(6); - expect(setPushRuleEnabled).toHaveBeenCalledWith( - "global", - PushRuleKind.Underride, - RuleId.EncryptedMessage, - true, - ); - expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true); - expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.EncryptedDM, true); - expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true); - expect(setPushRuleEnabled).toHaveBeenCalledWith( - "global", - PushRuleKind.Override, - RuleId.SuppressNotices, - false, - ); - expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.InviteToSelf, true); - expect(setPushRuleActions).toHaveBeenCalledTimes(6); - expect(setPushRuleActions).toHaveBeenCalledWith( - "global", - PushRuleKind.Underride, - RuleId.EncryptedMessage, - StandardActions.ACTION_NOTIFY, - ); - expect(setPushRuleActions).toHaveBeenCalledWith( - "global", - PushRuleKind.Underride, - RuleId.Message, - StandardActions.ACTION_NOTIFY, - ); - expect(setPushRuleActions).toHaveBeenCalledWith( - "global", - PushRuleKind.Underride, - RuleId.EncryptedDM, - StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - ); - expect(setPushRuleActions).toHaveBeenCalledWith( - "global", - PushRuleKind.Underride, - RuleId.DM, - StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - ); - expect(setPushRuleActions).toHaveBeenCalledWith( - "global", - PushRuleKind.Override, - RuleId.SuppressNotices, - StandardActions.ACTION_DONT_NOTIFY, - ); - expect(setPushRuleActions).toHaveBeenCalledWith( - "global", - PushRuleKind.Override, - RuleId.InviteToSelf, - StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - ); - }); + const { result } = renderHook(() => useNotificationSettings(cli)); + expect(result.current.model).toEqual(null); + await waitFor(() => expect(result.current.model).toEqual(expectedModel)); + expect(result.current.hasPendingChanges).toBeFalsy(); + await result.current.reconcile(DefaultNotificationSettings); + await waitFor(() => expect(result.current.hasPendingChanges).toBeFalsy()); + expect(addPushRule).toHaveBeenCalledTimes(0); + expect(deletePushRule).toHaveBeenCalledTimes(9); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justjann3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nn3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Janne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "J4nne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "Jann3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jann3"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "j4nne"); + expect(deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "janne"); + expect(setPushRuleEnabled).toHaveBeenCalledTimes(6); + expect(setPushRuleEnabled).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.EncryptedMessage, + true, + ); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.EncryptedDM, true); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.SuppressNotices, false); + expect(setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.InviteToSelf, true); + expect(setPushRuleActions).toHaveBeenCalledTimes(6); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.EncryptedMessage, + StandardActions.ACTION_NOTIFY, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.Message, + StandardActions.ACTION_NOTIFY, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.EncryptedDM, + StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Underride, + RuleId.DM, + StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Override, + RuleId.SuppressNotices, + StandardActions.ACTION_DONT_NOTIFY, + ); + expect(setPushRuleActions).toHaveBeenCalledWith( + "global", + PushRuleKind.Override, + RuleId.InviteToSelf, + StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + ); }); }); diff --git a/test/hooks/useUserOnboardingTasks-test.tsx b/test/hooks/useUserOnboardingTasks-test.tsx index f2d65382a4..f541747d10 100644 --- a/test/hooks/useUserOnboardingTasks-test.tsx +++ b/test/hooks/useUserOnboardingTasks-test.tsx @@ -14,9 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import { renderHook } from "@testing-library/react-hooks"; +import { waitFor } from "@testing-library/react"; import { useUserOnboardingTasks } from "../../src/hooks/useUserOnboardingTasks"; +import { useUserOnboardingContext } from "../../src/hooks/useUserOnboardingContext"; +import { stubClient } from "../test-utils"; +import MatrixClientContext from "../../src/contexts/MatrixClientContext"; +import DMRoomMap from "../../src/utils/DMRoomMap"; +import PlatformPeg from "../../src/PlatformPeg"; describe("useUserOnboardingTasks", () => { it.each([ @@ -25,7 +32,7 @@ describe("useUserOnboardingTasks", () => { hasAvatar: false, hasDevices: false, hasDmRooms: false, - hasNotificationsEnabled: false, + showNotificationsPrompt: false, }, }, { @@ -33,7 +40,7 @@ describe("useUserOnboardingTasks", () => { hasAvatar: true, hasDevices: false, hasDmRooms: false, - hasNotificationsEnabled: true, + showNotificationsPrompt: true, }, }, ])("sequence should stay static", async ({ context }) => { @@ -46,4 +53,39 @@ describe("useUserOnboardingTasks", () => { expect(result.current[3].id).toBe("setup-profile"); expect(result.current[4].id).toBe("permission-notifications"); }); + + it("should mark desktop notifications task completed on click", async () => { + jest.spyOn(PlatformPeg, "get").mockReturnValue({ + supportsNotifications: jest.fn().mockReturnValue(true), + maySendNotifications: jest.fn().mockReturnValue(false), + } as any); + + const cli = stubClient(); + cli.pushRules = { + global: { + override: [ + { + rule_id: ".m.rule.master", + enabled: false, + actions: [], + default: true, + }, + ], + }, + }; + DMRoomMap.makeShared(cli); + const context = renderHook(() => useUserOnboardingContext(), { + wrapper: (props) => { + return {props.children}; + }, + }); + const { result, rerender } = renderHook(() => useUserOnboardingTasks(context.result.current)); + expect(result.current[4].id).toBe("permission-notifications"); + expect(result.current[4].completed).toBe(false); + result.current[4].action!.onClick!({ type: "click" } as any); + await waitFor(() => { + rerender(); + expect(result.current[4].completed).toBe(true); + }); + }); }); diff --git a/test/languageHandler-test.tsx b/test/languageHandler-test.tsx index a9ad673a70..aeecaa0e18 100644 --- a/test/languageHandler-test.tsx +++ b/test/languageHandler-test.tsx @@ -32,6 +32,8 @@ import { TranslatedString, UserFriendlyError, TranslationKey, + IVariables, + Tags, } from "../src/languageHandler"; import { stubClient } from "./test-utils"; import { setupLanguageMock } from "./setup/setupLanguage"; @@ -214,13 +216,7 @@ describe("languageHandler JSX", function () { const plurals = "common|and_n_others"; const variableSub = "slash_command|ignore_dialog_description"; - type TestCase = [ - string, - TranslationKey, - Record, - Record | undefined, - TranslatedString, - ]; + type TestCase = [string, TranslationKey, IVariables, Tags | undefined, TranslatedString]; const testCasesEn: TestCase[] = [ // description of the test case, translationString, variables, tags, expected result ["translates a basic string", basicString, {}, undefined, "Rooms"], diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts index 5748b507ac..0b5a108f0e 100644 --- a/test/models/Call-test.ts +++ b/test/models/Call-test.ts @@ -28,12 +28,12 @@ import { } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { Widget } from "matrix-widget-api"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; -// eslint-disable-next-line no-restricted-imports -import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership"; -// eslint-disable-next-line no-restricted-imports -import { MatrixRTCSession, MatrixRTCSessionEvent } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { + CallMembership, + MatrixRTCSessionManagerEvents, + MatrixRTCSession, + MatrixRTCSessionEvent, +} from "matrix-js-sdk/src/matrixrtc"; import type { Mocked } from "jest-mock"; import type { MatrixClient, IMyDevice, RoomMember } from "matrix-js-sdk/src/matrix"; @@ -965,6 +965,18 @@ describe("ElementCall", () => { expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {}); }); + it("acknowledges mute_device widget action", async () => { + await callConnectProcedure(call); + const preventDefault = jest.fn(); + const mockEv = { + preventDefault, + detail: { video_enabled: false }, + }; + messaging.emit(`action:${ElementWidgetActions.DeviceMute}`, mockEv); + expect(messaging.transport.reply).toHaveBeenCalledWith({ video_enabled: false }, {}); + expect(preventDefault).toHaveBeenCalled(); + }); + it("emits events when connection state changes", async () => { // const wait = jest.spyOn(CallModule, "waitForEvent"); const onConnectionState = jest.fn(); diff --git a/test/modules/ProxiedModuleApi-test.tsx b/test/modules/ProxiedModuleApi-test.tsx index bec6215232..1e98e69508 100644 --- a/test/modules/ProxiedModuleApi-test.tsx +++ b/test/modules/ProxiedModuleApi-test.tsx @@ -118,6 +118,9 @@ describe("ProxiedApiModule", () => { describe("openDialog", () => { it("should open dialog with a custom title and default options", async () => { class MyDialogContent extends DialogContent { + public constructor(props: DialogProps) { + super(props); + } trySubmit = async () => ({ result: true }); render = () =>

This is my example content.

; } @@ -147,6 +150,9 @@ describe("ProxiedApiModule", () => { it("should open dialog with custom options", async () => { class MyDialogContent extends DialogContent { + public constructor(props: DialogProps) { + super(props); + } trySubmit = async () => ({ result: true }); render = () =>

This is my example content.

; } @@ -178,6 +184,9 @@ describe("ProxiedApiModule", () => { it("should update the options from the opened dialog", async () => { class MyDialogContent extends DialogContent { + public constructor(props: DialogProps) { + super(props); + } trySubmit = async () => ({ result: true }); render = () => { const onClick = () => { @@ -231,6 +240,9 @@ describe("ProxiedApiModule", () => { it("should cancel the dialog from within the dialog", async () => { class MyDialogContent extends DialogContent { + public constructor(props: DialogProps) { + super(props); + } trySubmit = async () => ({ result: true }); render = () => ( diff --git a/test/utils/device/parseUserAgent-test.ts b/test/utils/device/parseUserAgent-test.ts index 65e31a68a0..7e418702de 100644 --- a/test/utils/device/parseUserAgent-test.ts +++ b/test/utils/device/parseUserAgent-test.ts @@ -40,6 +40,7 @@ const ANDROID_UA = [ // Legacy User Agent Implementation "Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)", "Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)", + "Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", ]; const ANDROID_EXPECTED_RESULT = [ @@ -50,6 +51,7 @@ const ANDROID_EXPECTED_RESULT = [ makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) (5)", "Android 7.0"), makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-A510F", "Android 6.0.1"), makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G610M", "Android 7.0"), + makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G973U", "Android 9", "Chrome", "69.0.3497.100"), ]; const IOS_UA = [ @@ -57,12 +59,16 @@ const IOS_UA = [ "Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)", "Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)", "Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)", + "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", + "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", ]; const IOS_EXPECTED_RESULT = [ makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 15.2"), makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone XS Max", "iOS 15.2"), makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (11-inch)", "iOS 15.2"), makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (12.9-inch) (3rd generation)", "iOS 15.2"), + makeDeviceExtendedInfo(DeviceType.Web, "Apple iPad", "iOS", "Mobile Safari", "8.0"), + makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 8.4.1", "Mobile Safari", "8.0"), ]; const DESKTOP_UA = [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102" + @@ -81,10 +87,6 @@ const WEB_UA = [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18", "Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", - // using mobile browser - "Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", - "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4", - "Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", ]; const WEB_EXPECTED_RESULT = [ @@ -94,10 +96,6 @@ const WEB_EXPECTED_RESULT = [ makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Safari", "8.0.3"), makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Firefox", "40.0"), makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Edge", "12.246"), - // using mobile browser - makeDeviceExtendedInfo(DeviceType.Web, "Apple iPad", "iOS", "Mobile Safari", "8.0"), - makeDeviceExtendedInfo(DeviceType.Web, "Apple iPhone", "iOS", "Mobile Safari", "8.0"), - makeDeviceExtendedInfo(DeviceType.Web, "Samsung SM-G973U", "Android", "Chrome", "69.0.3497.100"), ]; const MISC_UA = [ diff --git a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index 3958005c5b..4eb9720b9b 100644 --- a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -62,7 +62,7 @@ exports[`HTMLExport should export 1`] = `

-
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0
  • +
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0
  • diff --git a/test/utils/permalinks/Permalinks-test.ts b/test/utils/permalinks/Permalinks-test.ts index 3c3bbbbec9..e21634bf41 100644 --- a/test/utils/permalinks/Permalinks-test.ts +++ b/test/utils/permalinks/Permalinks-test.ts @@ -25,6 +25,8 @@ import { parsePermalink, RoomPermalinkCreator, } from "../../../src/utils/permalinks/Permalinks"; +import { IConfigOptions } from "../../../src/IConfigOptions"; +import SdkConfig from "../../../src/SdkConfig"; import { getMockClientWithEventEmitter } from "../../test-utils"; describe("Permalinks", function () { @@ -391,6 +393,17 @@ describe("Permalinks", function () { expect(result).toBe("https://matrix.to/#/@someone:example.org"); }); + it("should use permalink_prefix for permalinks", function () { + const sdkConfigGet = SdkConfig.get; + jest.spyOn(SdkConfig, "get").mockImplementation((key: keyof IConfigOptions, altCaseName?: string) => { + if (key === "permalink_prefix") { + return "https://element.fs.tld"; + } else return sdkConfigGet(key, altCaseName); + }); + const result = makeUserPermalink("@someone:example.org"); + expect(result).toBe("https://element.fs.tld/#/user/@someone:example.org"); + }); + describe("parsePermalink", () => { it("should correctly parse room permalinks with a via argument", () => { const result = parsePermalink("https://matrix.to/#/!room_id:server?via=some.org"); diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx index a15efc86bc..2756f44086 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx @@ -109,10 +109,8 @@ describe("VoiceBroadcastPreRecordingPip", () => { describe("and double clicking »Go live«", () => { beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByText("Go live")); - await userEvent.click(screen.getByText("Go live")); - }); + await userEvent.click(screen.getByText("Go live")); + await userEvent.click(screen.getByText("Go live")); }); it("should call start once", () => { diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx index 5c2371e1a0..e2dbadd03e 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx @@ -117,10 +117,8 @@ describe("VoiceBroadcastRecordingPip", () => { describe("and selecting another input device", () => { beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByLabelText("Change input device")); - await userEvent.click(screen.getByText("Device 1")); - }); + await userEvent.click(screen.getByLabelText("Change input device")); + await userEvent.click(screen.getByText("Device 1")); }); it("should select the device and pause and resume the broadcast", () => { @@ -199,8 +197,8 @@ describe("VoiceBroadcastRecordingPip", () => { client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); }); - it("should render a paused recording", () => { - expect(screen.getByLabelText("resume voice broadcast")).toBeInTheDocument(); + it("should render a paused recording", async () => { + await expect(screen.findByLabelText("resume voice broadcast")).resolves.toBeInTheDocument(); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 3118f598c4..55e83d95b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,9 +4,10 @@ "emitDecoratorMetadata": false, "resolveJsonModule": true, "esModuleInterop": true, + "useDefineForClassFields": true, "module": "es2022", "moduleResolution": "node", - "target": "es2018", + "target": "es2022", "noUnusedLocals": true, "sourceMap": false, "outDir": "./lib", diff --git a/yarn.lock b/yarn.lock index 4365b80225..ece55b3d96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,9 +40,9 @@ axe-core "~4.9.1" "@babel/cli@^7.12.10": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.24.7.tgz#eb2868c1fa384b17ea88d60107577d3e6fd05c4e" - integrity sha512-8dfPprJgV4O14WTx+AQyEA+opgUKPrsIXX/MdL50J1n06EQJ6m1T+CdsJe0qEC0B/Xl85i+Un5KVAxd/PACX9A== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.24.8.tgz#79eaa55a69c77cafbea3e87537fd1df5a5a2edf8" + integrity sha512-isdp+G6DpRyKc+3Gqxy2rjzgF7Zj9K0mzLNnxz+E/fgeag8qT3vVulX4gY9dGO1q0y+0lUv6V3a+uhUzMzrwXg== dependencies: "@jridgewell/trace-mapping" "^0.3.25" commander "^6.2.0" @@ -79,26 +79,31 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.7": +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.8": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.9.tgz#53eee4e68f1c1d0282aa0eb05ddb02d033fc43a0" + integrity sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng== + +"@babel/compat-data@^7.23.5": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== "@babel/core@^7.0.0", "@babel/core@^7.12.10": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" - integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.9.tgz#dc07c9d307162c97fa9484ea997ade65841c7c82" + integrity sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helpers" "^7.24.7" - "@babel/parser" "^7.24.7" + "@babel/generator" "^7.24.9" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-module-transforms" "^7.24.9" + "@babel/helpers" "^7.24.8" + "@babel/parser" "^7.24.8" "@babel/template" "^7.24.7" - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.9" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -127,9 +132,9 @@ semver "^6.3.1" "@babel/eslint-parser@^7.12.10": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz#27ebab1a1ec21f48ae191a8aaac5b82baf80d9c7" - integrity sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.24.8.tgz#bc655255fa4ded3694cc10ef3dbea6d69639c831" + integrity sha512-nYAikI4XTGokU2QX7Jx+v4rxZKhKivaQaREZjuW3mrJrbdWJ5yUfohnoUULge+zEEaKjPYNxhoRgUKktjXtbwA== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" @@ -152,7 +157,7 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.23.3", "@babel/generator@^7.24.7": +"@babel/generator@^7.23.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== @@ -162,6 +167,16 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" +"@babel/generator@^7.24.7", "@babel/generator@^7.24.8", "@babel/generator@^7.24.9": + version "7.24.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.10.tgz#a4ab681ec2a78bbb9ba22a3941195e28a81d8e76" + integrity sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg== + dependencies: + "@babel/types" "^7.24.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" @@ -188,26 +203,26 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9" - integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg== +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz#b607c3161cd9d1744977d4f97139572fe778c271" + integrity sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw== dependencies: - "@babel/compat-data" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - browserslist "^4.22.2" + "@babel/compat-data" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" lru-cache "^5.1.1" semver "^6.3.1" "@babel/helper-create-class-features-plugin@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" - integrity sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz#47f546408d13c200c0867f9d935184eaa0851b09" + integrity sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA== dependencies: "@babel/helper-annotate-as-pure" "^7.24.7" "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-function-name" "^7.24.7" - "@babel/helper-member-expression-to-functions" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" "@babel/helper-optimise-call-expression" "^7.24.7" "@babel/helper-replace-supers" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" @@ -256,13 +271,13 @@ dependencies: "@babel/types" "^7.24.7" -"@babel/helper-member-expression-to-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz#67613d068615a70e4ed5101099affc7a41c5225f" - integrity sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w== +"@babel/helper-member-expression-to-functions@^7.24.7", "@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" "@babel/helper-module-imports@^7.22.15": version "7.24.3" @@ -290,10 +305,10 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.20" -"@babel/helper-module-transforms@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" - integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ== +"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.24.9": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz#e13d26306b89eea569180868e652e7f514de9d29" + integrity sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw== dependencies: "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-module-imports" "^7.24.7" @@ -308,10 +323,10 @@ dependencies: "@babel/types" "^7.24.7" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" - integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== "@babel/helper-remap-async-to-generator@^7.24.7": version "7.24.7" @@ -361,21 +376,31 @@ dependencies: "@babel/types" "^7.24.7" -"@babel/helper-string-parser@^7.22.5", "@babel/helper-string-parser@^7.24.1", "@babel/helper-string-parser@^7.24.7": +"@babel/helper-string-parser@^7.22.5", "@babel/helper-string-parser@^7.24.1": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== +"@babel/helper-string-parser@^7.24.7", "@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + "@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.24.5", "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== -"@babel/helper-validator-option@^7.23.5", "@babel/helper-validator-option@^7.24.7": +"@babel/helper-validator-option@^7.23.5": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== +"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + "@babel/helper-wrap-function@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" @@ -395,13 +420,13 @@ "@babel/traverse" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/helpers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416" - integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg== +"@babel/helpers@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.8.tgz#2820d64d5d6686cca8789dd15b074cd862795873" + integrity sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ== dependencies: "@babel/template" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/types" "^7.24.8" "@babel/highlight@^7.22.13": version "7.23.4" @@ -422,10 +447,10 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.5", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.22.16", "@babel/parser@^7.23.3", "@babel/parser@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" - integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.5", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.22.16", "@babel/parser@^7.23.3", "@babel/parser@^7.24.7", "@babel/parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.8.tgz#58a4dbbcad7eb1d48930524a3fd93d93e9084c6f" + integrity sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w== "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": version "7.24.7" @@ -698,16 +723,16 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-classes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz#4ae6ef43a12492134138c1e45913f7c46c41b4bf" - integrity sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw== +"@babel/plugin-transform-classes@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz#ad23301fe5bc153ca4cf7fb572a9bc8b0b711cf7" + integrity sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA== dependencies: "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-compilation-targets" "^7.24.8" "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-function-name" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/helper-replace-supers" "^7.24.7" "@babel/helper-split-export-declaration" "^7.24.7" globals "^11.1.0" @@ -720,12 +745,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/template" "^7.24.7" -"@babel/plugin-transform-destructuring@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz#a097f25292defb6e6cc16d6333a4cfc1e3c72d9e" - integrity sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw== +"@babel/plugin-transform-destructuring@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" + integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-transform-dotall-regex@^7.24.7": version "7.24.7" @@ -830,6 +855,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-simple-access" "^7.24.7" +"@babel/plugin-transform-modules-commonjs@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== + dependencies: + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" + "@babel/plugin-transform-modules-systemjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz#f8012316c5098f6e8dee6ecd58e2bc6f003d0ce7" @@ -905,12 +939,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz#b8f6848a80cf2da98a8a204429bec04756c6d454" - integrity sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ== +"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" + integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" @@ -1035,12 +1069,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-typeof-symbol@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz#f074be466580d47d6e6b27473a840c9f9ca08fb0" - integrity sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg== +"@babel/plugin-transform-typeof-symbol@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" + integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-transform-typescript@^7.24.7": version "7.24.7" @@ -1084,14 +1118,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/preset-env@^7.12.11": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.7.tgz#ff067b4e30ba4a72f225f12f123173e77b987f37" - integrity sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.8.tgz#e0db94d7f17d6f0e2564e8d29190bc8cdacec2d1" + integrity sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ== dependencies: - "@babel/compat-data" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" + "@babel/compat-data" "^7.24.8" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.7" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.7" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" @@ -1122,9 +1156,9 @@ "@babel/plugin-transform-block-scoping" "^7.24.7" "@babel/plugin-transform-class-properties" "^7.24.7" "@babel/plugin-transform-class-static-block" "^7.24.7" - "@babel/plugin-transform-classes" "^7.24.7" + "@babel/plugin-transform-classes" "^7.24.8" "@babel/plugin-transform-computed-properties" "^7.24.7" - "@babel/plugin-transform-destructuring" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" "@babel/plugin-transform-dotall-regex" "^7.24.7" "@babel/plugin-transform-duplicate-keys" "^7.24.7" "@babel/plugin-transform-dynamic-import" "^7.24.7" @@ -1137,7 +1171,7 @@ "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" "@babel/plugin-transform-member-expression-literals" "^7.24.7" "@babel/plugin-transform-modules-amd" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" "@babel/plugin-transform-modules-systemjs" "^7.24.7" "@babel/plugin-transform-modules-umd" "^7.24.7" "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" @@ -1147,7 +1181,7 @@ "@babel/plugin-transform-object-rest-spread" "^7.24.7" "@babel/plugin-transform-object-super" "^7.24.7" "@babel/plugin-transform-optional-catch-binding" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" "@babel/plugin-transform-parameters" "^7.24.7" "@babel/plugin-transform-private-methods" "^7.24.7" "@babel/plugin-transform-private-property-in-object" "^7.24.7" @@ -1158,7 +1192,7 @@ "@babel/plugin-transform-spread" "^7.24.7" "@babel/plugin-transform-sticky-regex" "^7.24.7" "@babel/plugin-transform-template-literals" "^7.24.7" - "@babel/plugin-transform-typeof-symbol" "^7.24.7" + "@babel/plugin-transform-typeof-symbol" "^7.24.8" "@babel/plugin-transform-unicode-escapes" "^7.24.7" "@babel/plugin-transform-unicode-property-regex" "^7.24.7" "@babel/plugin-transform-unicode-regex" "^7.24.7" @@ -1167,7 +1201,7 @@ babel-plugin-polyfill-corejs2 "^0.4.10" babel-plugin-polyfill-corejs3 "^0.10.4" babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.31.0" + core-js-compat "^3.37.1" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -1218,14 +1252,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" - integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.13.10": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== @@ -1250,7 +1277,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.18.5", "@babel/traverse@^7.24.7": +"@babel/traverse@^7.18.5": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== @@ -1282,6 +1309,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.8.tgz#6c14ed5232b7549df3371d820fbd9abfcd7dfab7" + integrity sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.8" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/parser" "^7.24.8" + "@babel/types" "^7.24.8" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.3.3": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" @@ -1300,7 +1343,7 @@ "@babel/helper-validator-identifier" "^7.24.5" to-fast-properties "^2.0.0" -"@babel/types@^7.23.0", "@babel/types@^7.23.3", "@babel/types@^7.24.0", "@babel/types@^7.24.5", "@babel/types@^7.24.7", "@babel/types@^7.4.4": +"@babel/types@^7.23.0", "@babel/types@^7.23.3", "@babel/types@^7.24.0", "@babel/types@^7.24.5": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q== @@ -1309,6 +1352,15 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" +"@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.24.9", "@babel/types@^7.4.4": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.9.tgz#228ce953d7b0d16646e755acf204f4cf3d08cc73" + integrity sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1330,20 +1382,20 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@csstools/css-parser-algorithms@^2.6.3": - version "2.6.3" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz#b5e7eb2bd2a42e968ef61484f1490a8a4148a8eb" - integrity sha512-xI/tL2zxzEbESvnSxwFgwvy5HS00oCXxL4MLs6HUiDcYfwowsoQaABKxUElp1ARITrINzBnsECOc1q0eg2GOrA== +"@csstools/css-parser-algorithms@^2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz#6d93a8f7d8aeb7cd9ed0868f946e46f021b6aa70" + integrity sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw== -"@csstools/css-tokenizer@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.3.1.tgz#3d47e101ad48d815a4bdce8159fb5764f087f17a" - integrity sha512-iMNHTyxLbBlWIfGtabT157LH9DUx9X8+Y3oymFEuMj8HNc+rpE3dPFGFgHjpKfjeFDjLjYIAIhXPGvS2lKxL9g== +"@csstools/css-tokenizer@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz#1d8b2e200197cf5f35ceb07ca2dade31f3a00ae8" + integrity sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg== -"@csstools/media-query-list-parser@^2.1.11": - version "2.1.11" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.11.tgz#465aa42f268599729350e305e1ae14a30c1daf51" - integrity sha512-uox5MVhvNHqitPP+SynrB1o8oPxPMt2JLgp5ghJOWf54WGQ5OKu47efne49r1SWqs3wRP8xSWjnO9MBKxhB1dA== +"@csstools/media-query-list-parser@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz#f00be93f6bede07c14ddf51a168ad2748e4fe9e5" + integrity sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA== "@csstools/selector-specificity@^3.1.1": version "3.1.1" @@ -1408,21 +1460,21 @@ integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== "@floating-ui/core@^1.6.0": - version "1.6.4" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.4.tgz#0140cf5091c8dee602bff9da5ab330840ff91df6" - integrity sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA== + version "1.6.5" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.5.tgz#102335cac0d22035b04d70ca5ff092d2d1a26f2b" + integrity sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA== dependencies: - "@floating-ui/utils" "^0.2.4" + "@floating-ui/utils" "^0.2.5" "@floating-ui/dom@^1.0.0": - version "1.6.7" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.7.tgz#85d22f731fcc5b209db504478fb1df5116a83015" - integrity sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng== + version "1.6.8" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.8.tgz#45e20532b6d8a061b356a4fb336022cf2609754d" + integrity sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q== dependencies: "@floating-ui/core" "^1.6.0" - "@floating-ui/utils" "^0.2.4" + "@floating-ui/utils" "^0.2.5" -"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.0.8": +"@floating-ui/react-dom@^2.0.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0" integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg== @@ -1438,10 +1490,10 @@ "@floating-ui/utils" "^0.2.0" tabbable "^6.0.0" -"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.4": - version "0.2.4" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.4.tgz#1d459cee5031893a08a0e064c406ad2130cced7c" - integrity sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA== +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.5.tgz#105c37d9d9620ce69b7f692a20c821bf1ad2cbf9" + integrity sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ== "@humanwhocodes/config-array@^0.11.14": version "0.11.14" @@ -1707,9 +1759,9 @@ integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== "@jridgewell/trace-mapping@0.3.9": version "0.3.9" @@ -1793,15 +1845,17 @@ emojibase "^15.0.0" emojibase-data "^15.0.0" -"@matrix-org/matrix-sdk-crypto-wasm@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-6.1.0.tgz#1cedf2bcbd6795e297fd45ea4a33f2c8c5204fdd" - integrity sha512-8Wn4TT9PEJswfE8+6mA60JHrxyiWYXfM4EM5800tLz+Rl9QRGk9JDF0o0cTb26v6bfXTa3/pCGWAkUVk0ROPEw== +"@matrix-org/matrix-sdk-crypto-wasm@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-7.0.0.tgz#8d6abdb9ded8656cc9e2a7909913a34bf3fc9b3a" + integrity sha512-MOencXiW/gI5MuTtCNsuojjwT5DXCrjMqv9xOslJC9h2tPdLFFFMGr58dY5Lis4DRd9MRWcgrGowUIHOqieWTA== -"@matrix-org/matrix-wysiwyg@2.37.4": - version "2.37.4" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.37.4.tgz#bd9b46051a21c9986477e3a83a1417b1ee926d81" - integrity sha512-4OtBWAHNAOu9P5C6jOIeHlu4ChwV2YusxnbGuN20IceC4bT2h38flZQgm0x9/jgHfF0LwnKUwKXsxtRoq8xW0g== +"@matrix-org/matrix-wysiwyg@2.37.8": + version "2.37.8" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.37.8.tgz#0b61e9023e3c73ca8789e97c80d7282e8c823c6b" + integrity sha512-fx8KGpztmJvwiY1OvE9A7r08kNcUZQIZXPbWcXNtQ61GwV5VyKvMxCmxfRlZ6Ac8oagVxRPu4WySCRX44Y9Ylw== + dependencies: + eslint-plugin-unicorn "^54.0.0" "@matrix-org/olm@3.2.15": version "3.2.15" @@ -1886,11 +1940,11 @@ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@playwright/test@^1.40.1": - version "1.45.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.45.2.tgz#e1b8512e20916720de1c5f5e89a362a252ea78ca" - integrity sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ== + version "1.45.3" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.45.3.tgz#22e9c38b3081d6674b28c6e22f784087776c72e5" + integrity sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA== dependencies: - playwright "1.45.2" + playwright "1.45.3" "@radix-ui/primitive@1.0.1": version "1.0.1" @@ -2232,76 +2286,76 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== -"@sentry-internal/browser-utils@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.16.0.tgz#182931f169a586dde50cf255237b129aad00dde7" - integrity sha512-40lzNy5F6dUFCN85AGThBxHPQLSwoNhZM2hWqhAR5rZ3Yed0uBaKlm4aNJCeeUB9l4kd0sH0In+i9Nqu6TGKrw== +"@sentry-internal/browser-utils@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.19.0.tgz#7a25111c5c3430c07b881d5fc83d1598af6d5676" + integrity sha512-kM/2KlikKuBR63nFi2q7MGS3V9K9hakjvUknhr/jHZqDVfEuBKmp1ZlHFAdJtglKHHJy07gPj/XqDH7BbYh5yg== dependencies: - "@sentry/core" "8.16.0" - "@sentry/types" "8.16.0" - "@sentry/utils" "8.16.0" + "@sentry/core" "8.19.0" + "@sentry/types" "8.19.0" + "@sentry/utils" "8.19.0" -"@sentry-internal/feedback@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.16.0.tgz#dc8a3b807a37d0df136e62937e87ac23ce2ce6a8" - integrity sha512-BmRazZKl6iiVSg6eybUNOI1ve4eZqYpJYjkX48Jedn+7iZg7z12MNYl6IWPFBcN+sg+clf4wiKDr/SYS0yNemQ== +"@sentry-internal/feedback@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.19.0.tgz#7efef695fd4a058b36ef5c98145c4a990bbe877c" + integrity sha512-Jc77H8fEaGcBhERc2U/o7Q8CZHvlZLT9vAlzq0ZZR20v/1vwYcJW1ysKfTuvmw22hCR6ukhFNl6pqJocXFVhvA== dependencies: - "@sentry/core" "8.16.0" - "@sentry/types" "8.16.0" - "@sentry/utils" "8.16.0" + "@sentry/core" "8.19.0" + "@sentry/types" "8.19.0" + "@sentry/utils" "8.19.0" -"@sentry-internal/replay-canvas@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.16.0.tgz#c6501dd9f7e5dac1399978cc9e2797eb281a8f70" - integrity sha512-Bjh6pCDLZIPAPU2dNvJfI7BQV16rsRtYcylJgkGamjf8IcaBu7r/Whsvt1q34xO29xc0ISlp+0xG+YAdN1690Q== +"@sentry-internal/replay-canvas@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.19.0.tgz#4afe06acadf14a709e36efe5ad7da350e3ce0815" + integrity sha512-l4pKJDHrXEctxrK7Xme/+fKToXpGwr/G2t77BzeE1WEw9LwSwADz/hi8HoMdZzuKWriM2BNbz20tpVS84sODxA== dependencies: - "@sentry-internal/replay" "8.16.0" - "@sentry/core" "8.16.0" - "@sentry/types" "8.16.0" - "@sentry/utils" "8.16.0" + "@sentry-internal/replay" "8.19.0" + "@sentry/core" "8.19.0" + "@sentry/types" "8.19.0" + "@sentry/utils" "8.19.0" -"@sentry-internal/replay@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.16.0.tgz#5bf564d7293d4fb4993327567e9ad12079ceb951" - integrity sha512-JT/wmYU2JPtl8Ldl9oml/25Yz6C5wG+SpylDeUx4mPh728E/iI9vesIc2652J/0xots/DZXe4K6K5nYjdFtEcQ== +"@sentry-internal/replay@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.19.0.tgz#7b290b19d6ba5c0ab742c4c48839cce329129b3f" + integrity sha512-EW9e1J6XbqXUXQST1AfSIzT9O8OwPyeFOkhkn9/gqOQv08TJvQEIBtWJEoJS+XFMEUuB8IqIzVWNVko/DnGt9A== dependencies: - "@sentry-internal/browser-utils" "8.16.0" - "@sentry/core" "8.16.0" - "@sentry/types" "8.16.0" - "@sentry/utils" "8.16.0" + "@sentry-internal/browser-utils" "8.19.0" + "@sentry/core" "8.19.0" + "@sentry/types" "8.19.0" + "@sentry/utils" "8.19.0" "@sentry/browser@^8.0.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.16.0.tgz#af9b7b7556198d6de03cbc41b7abb5a16ecfc342" - integrity sha512-8Fxmk2aFWRixi2IKixiJR10Du34yb13HYr2iRw1haPKb5ZKa6CFA+XAnSzwpPZxO0RSHuPQR06YNkXaQ8fRAQQ== - dependencies: - "@sentry-internal/browser-utils" "8.16.0" - "@sentry-internal/feedback" "8.16.0" - "@sentry-internal/replay" "8.16.0" - "@sentry-internal/replay-canvas" "8.16.0" - "@sentry/core" "8.16.0" - "@sentry/types" "8.16.0" - "@sentry/utils" "8.16.0" - -"@sentry/core@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.16.0.tgz#cf2f4e572240983ec7e9fa083cc1ffce3147f20b" - integrity sha512-l9mQgm5OqnykvZMh6PmJ/9ygW4qLyEFop+pQH/uM5zQCZQvEa7rvAd9QXKHdbVKq1CxJa/nJiByc8wPWxsftGQ== - dependencies: - "@sentry/types" "8.16.0" - "@sentry/utils" "8.16.0" - -"@sentry/types@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.16.0.tgz#a9ae39cffd50a0bdba0556a1596fb135d035cf26" - integrity sha512-cIRsn7gWGVaWHgCniBWA0N8PNwzDYibhjyjPRTMxUjuZCT37i7zxByKKmd9u4TpRIJ64MyirNyM0O6T0A26fpg== - -"@sentry/utils@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.16.0.tgz#5d1c9fb6cd562660b507c6647e6437282bef939a" - integrity sha512-tltCf2DVzz5TiYjxu/Rxbc9Qmm04893MFshV97jOTBcQeO2AAZBEl5rAoTCv1P08y7Yg+KiVwCx9Zj2x5U80/g== - dependencies: - "@sentry/types" "8.16.0" + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.19.0.tgz#64551150cda979728297035f9a367ed344c7d586" + integrity sha512-ZC1HxIFm4TIGONyy9MkPG6Dw8IAhzq43t5mq9PqrB1ehuWj8GX6Vk3E26kuc2sydAm4AXbj0562OmvZHsAJpUA== + dependencies: + "@sentry-internal/browser-utils" "8.19.0" + "@sentry-internal/feedback" "8.19.0" + "@sentry-internal/replay" "8.19.0" + "@sentry-internal/replay-canvas" "8.19.0" + "@sentry/core" "8.19.0" + "@sentry/types" "8.19.0" + "@sentry/utils" "8.19.0" + +"@sentry/core@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.19.0.tgz#427d09ca27557ddc7c1bfa5e810b7f802836e0b4" + integrity sha512-MrgjsZCEjOJgQjIznnDSrLEy7qL+4LVpNieAvr49cV1rzBNSwGmWRnt/puVaPsLyCUgupVx/43BPUHB/HtKNUw== + dependencies: + "@sentry/types" "8.19.0" + "@sentry/utils" "8.19.0" + +"@sentry/types@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.19.0.tgz#26a5d56c823c5eabbb7d6f53112da335b6d96dcb" + integrity sha512-52C8X5V7mK2KIxMJt8MV5TxXAFHqrQR1RKm1oPTwKVWm8hKr1ZYJXINymNrWvpAc3oVIKLC/sa9WFYgXQh+YlA== + +"@sentry/utils@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.19.0.tgz#f22df2a38327b1cff1e04ba7f11fdf1a32d3ba22" + integrity sha512-8dWJJKaUN6Hf92Oxw2TBmHchGua2W3ZmonrZTTwLvl06jcAigbiQD0MGuF5ytZP8PHx860orV+SbTGKFzfU3Pg== + dependencies: + "@sentry/types" "8.19.0" "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -2351,9 +2405,9 @@ pretty-format "^27.0.2" "@testing-library/jest-dom@^6.0.0": - version "6.4.6" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz#ec1df8108651bed5475534955565bed88c6732ce" - integrity sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w== + version "6.4.8" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz#9c435742b20c6183d4e7034f2b329d562c079daa" + integrity sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw== dependencies: "@adobe/css-tools" "^4.4.0" "@babel/runtime" "^7.9.2" @@ -2670,9 +2724,9 @@ undici-types "~5.26.4" "@types/node@18": - version "18.19.39" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.39.tgz#c316340a5b4adca3aee9dcbf05de385978590593" - integrity sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ== + version "18.19.42" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.42.tgz#b54ed4752c85427906aab40917b0f7f3d724bf72" + integrity sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg== dependencies: undici-types "~5.26.4" @@ -2835,29 +2889,29 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^7.0.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz#b3563927341eca15124a18c6f94215f779f5c02a" - integrity sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw== + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz#c8ed1af1ad2928ede5cdd207f7e3090499e1f77b" + integrity sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.16.0" - "@typescript-eslint/type-utils" "7.16.0" - "@typescript-eslint/utils" "7.16.0" - "@typescript-eslint/visitor-keys" "7.16.0" + "@typescript-eslint/scope-manager" "7.17.0" + "@typescript-eslint/type-utils" "7.17.0" + "@typescript-eslint/utils" "7.17.0" + "@typescript-eslint/visitor-keys" "7.17.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^7.0.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.16.0.tgz#53fae8112f8c912024aea7b499cf7374487af6d8" - integrity sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw== - dependencies: - "@typescript-eslint/scope-manager" "7.16.0" - "@typescript-eslint/types" "7.16.0" - "@typescript-eslint/typescript-estree" "7.16.0" - "@typescript-eslint/visitor-keys" "7.16.0" + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.17.0.tgz#be8e32c159190cd40a305a2121220eadea5a88e7" + integrity sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A== + dependencies: + "@typescript-eslint/scope-manager" "7.17.0" + "@typescript-eslint/types" "7.17.0" + "@typescript-eslint/typescript-estree" "7.17.0" + "@typescript-eslint/visitor-keys" "7.17.0" debug "^4.3.4" "@typescript-eslint/scope-manager@7.13.0": @@ -2868,21 +2922,21 @@ "@typescript-eslint/types" "7.13.0" "@typescript-eslint/visitor-keys" "7.13.0" -"@typescript-eslint/scope-manager@7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz#eb0757af5720c9c53c8010d7a0355ae27e17b7e5" - integrity sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw== +"@typescript-eslint/scope-manager@7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz#e072d0f914662a7bfd6c058165e3c2b35ea26b9d" + integrity sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA== dependencies: - "@typescript-eslint/types" "7.16.0" - "@typescript-eslint/visitor-keys" "7.16.0" + "@typescript-eslint/types" "7.17.0" + "@typescript-eslint/visitor-keys" "7.17.0" -"@typescript-eslint/type-utils@7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz#ec52b1932b8fb44a15a3e20208e0bd49d0b6bd00" - integrity sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg== +"@typescript-eslint/type-utils@7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz#c5da78feb134c9c9978cbe89e2b1a589ed22091a" + integrity sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA== dependencies: - "@typescript-eslint/typescript-estree" "7.16.0" - "@typescript-eslint/utils" "7.16.0" + "@typescript-eslint/typescript-estree" "7.17.0" + "@typescript-eslint/utils" "7.17.0" debug "^4.3.4" ts-api-utils "^1.3.0" @@ -2891,10 +2945,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.13.0.tgz#0cca95edf1f1fdb0cfe1bb875e121b49617477c5" integrity sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA== -"@typescript-eslint/types@7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.16.0.tgz#60a19d7e7a6b1caa2c06fac860829d162a036ed2" - integrity sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw== +"@typescript-eslint/types@7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.17.0.tgz#7ce8185bdf06bc3494e73d143dbf3293111b9cff" + integrity sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A== "@typescript-eslint/typescript-estree@7.13.0": version "7.13.0" @@ -2910,13 +2964,13 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/typescript-estree@7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz#98ac779d526fab2a781e5619c9250f3e33867c09" - integrity sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw== +"@typescript-eslint/typescript-estree@7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz#dcab3fea4c07482329dd6107d3c6480e228e4130" + integrity sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw== dependencies: - "@typescript-eslint/types" "7.16.0" - "@typescript-eslint/visitor-keys" "7.16.0" + "@typescript-eslint/types" "7.17.0" + "@typescript-eslint/visitor-keys" "7.17.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -2924,15 +2978,15 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.16.0.tgz#b38dc0ce1778e8182e227c98d91d3418449aa17f" - integrity sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA== +"@typescript-eslint/utils@7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.17.0.tgz#815cd85b9001845d41b699b0ce4f92d6dfb84902" + integrity sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "7.16.0" - "@typescript-eslint/types" "7.16.0" - "@typescript-eslint/typescript-estree" "7.16.0" + "@typescript-eslint/scope-manager" "7.17.0" + "@typescript-eslint/types" "7.17.0" + "@typescript-eslint/typescript-estree" "7.17.0" "@typescript-eslint/utils@^6.0.0 || ^7.0.0": version "7.13.0" @@ -2952,12 +3006,12 @@ "@typescript-eslint/types" "7.13.0" eslint-visitor-keys "^3.4.3" -"@typescript-eslint/visitor-keys@7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz#a1d99fa7a3787962d6e0efd436575ef840e23b06" - integrity sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg== +"@typescript-eslint/visitor-keys@7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz#680465c734be30969e564b4647f38d6cdf49bfb0" + integrity sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A== dependencies: - "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/types" "7.17.0" eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": @@ -2965,18 +3019,17 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vector-im/compound-design-tokens@^1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.6.1.tgz#3f1bb5b2b9f8aff10144aab19dfa11165c3c927b" - integrity sha512-u5xG/8AN7QkPPWhugj0ZrQtWsAjuKHzuOoP0s3bbDg7ZkKTE9l5tM29bdOHnSv9mEYKO+KVMMfsl0W1rlaTmAw== +"@vector-im/compound-design-tokens@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.8.0.tgz#bc844cb6b9842c1eb8e5c42f5cedcaf51a49b86f" + integrity sha512-PtQMG7kDzwtjw/fLKD63uWP5rJ8cgWc/aXarfEzxYUf9ceWxBajnYOBI2jDqtE3WIUe9uTVBzNEvmOBG/VIgTA== -"@vector-im/compound-web@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-5.4.0.tgz#b95262197199c11931a8c6f5269514eb9461f187" - integrity sha512-+EPbr8HzlGEWSePEcPs2iQEBnjXvHGWK177SKF8IO2C7Z2Ygddxa2VTQ7oqtrUfgT+NB5IBTLyXV4Nx7FLgmMA== +"@vector-im/compound-web@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-5.5.0.tgz#c646cd8c59aa7e5df527d843ad3b7b7c064d5224" + integrity sha512-Z+QSXOkJE4/LYk9j9FMVE2m5noz3gEA4yRxetjSJyXB5mDpyIJ1OYAweuZJXS3foxqygVDeB82YgTw1JgDtUvg== dependencies: "@floating-ui/react" "^0.26.9" - "@floating-ui/react-dom" "^2.0.8" "@radix-ui/react-context-menu" "^2.1.5" "@radix-ui/react-dropdown-menu" "^2.0.6" "@radix-ui/react-form" "^0.0.3" @@ -3079,14 +3132,14 @@ ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.1: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" - integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.4.1" another-json@^0.2.0: version "0.2.0" @@ -3100,6 +3153,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -3129,7 +3189,7 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.1.0: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -3264,16 +3324,6 @@ array.prototype.flatmap@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.toreversed@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" - integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - array.prototype.tosorted@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" @@ -3461,10 +3511,10 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -blob-polyfill@^7.0.0: - version "7.0.20220408" - resolved "https://registry.yarnpkg.com/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz#38bf5e046c41a21bb13654d9d19f303233b8218c" - integrity sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ== +blob-polyfill@^9.0.0: + version "9.0.20240710" + resolved "https://registry.yarnpkg.com/blob-polyfill/-/blob-polyfill-9.0.20240710.tgz#2ef075a207311ea327704f04dc4a98cbefe4143b" + integrity sha512-DPUO/EjNANCgSVg0geTy1vmUpu5hhp9tV2F7xUSTUd1jwe4XpwupGB+lt5PhVUqpqAk+zK1etqp6Pl/HVf71Ug== bloom-filters@^3.0.1: version "3.0.2" @@ -3526,15 +3576,15 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.22.2, browserslist@^4.23.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" - integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== +browserslist@^4.22.2, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.2: + version "4.23.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed" + integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== dependencies: - caniuse-lite "^1.0.30001587" - electron-to-chromium "^1.4.668" + caniuse-lite "^1.0.30001640" + electron-to-chromium "^1.4.820" node-releases "^2.0.14" - update-browserslist-db "^1.0.13" + update-browserslist-db "^1.1.0" bs58@^6.0.0: version "6.0.0" @@ -3591,10 +3641,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001587: - version "1.0.30001629" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz#907a36f4669031bd8a1a8dbc2fa08b29e0db297e" - integrity sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw== +caniuse-lite@1.0.30001643, caniuse-lite@^1.0.30001640: + version "1.0.30001643" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz#9c004caef315de9452ab970c3da71085f8241dbd" + integrity sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg== chalk@5.2.0: version "5.2.0" @@ -3626,6 +3676,11 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -3678,6 +3733,21 @@ clean-regexp@^1.0.0: dependencies: escape-string-regexp "^1.0.5" +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + dependencies: + restore-cursor "^5.0.0" + +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -3744,6 +3814,11 @@ colord@^2.9.3: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -3761,6 +3836,11 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@~12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3813,7 +3893,7 @@ cookie@0.6.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== -core-js-compat@^3.31.0, core-js-compat@^3.36.1, core-js-compat@^3.37.0: +core-js-compat@^3.36.1, core-js-compat@^3.37.0, core-js-compat@^3.37.1: version "3.37.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== @@ -4022,13 +4102,20 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.5: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== dependencies: ms "2.1.2" +debug@~4.3.4: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -4242,16 +4329,21 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.668: - version "1.4.792" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.792.tgz#738712f99d02f70c5754ca4264782915fa946849" - integrity sha512-rkg5/N3L+Y844JyfgPUyuKK0Hk0efo3JNxUDKvz3HgP6EmN4rNGhr2D8boLsfTV/hGo7ZGAL8djw+jlg99zQyA== +electron-to-chromium@1.5.2, electron-to-chromium@^1.4.820, electron-to-chromium@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz#6126ad229ce45e781ec54ca40db0504787f23d19" + integrity sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ== emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== +emoji-regex@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -4312,6 +4404,11 @@ env-paths@^2.2.1: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -4580,28 +4677,28 @@ eslint-plugin-react-hooks@^4.3.0: integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== eslint-plugin-react@^7.28.0: - version "7.34.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b" - integrity sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA== + version "7.35.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz#00b1e4559896710e58af6358898f2ff917ea4c41" + integrity sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" - array.prototype.toreversed "^1.1.2" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" object.entries "^1.1.8" object.fromentries "^2.0.8" - object.hasown "^1.1.4" object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" eslint-plugin-unicorn@^54.0.0: version "54.0.0" @@ -4762,6 +4859,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -4789,6 +4891,21 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@~8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -4878,6 +4995,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + fastest-levenshtein@1.0.16, fastest-levenshtein@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -5137,6 +5259,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" + integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" @@ -5163,6 +5290,11 @@ get-stream@^6.0.0, get-stream@^6.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -5433,6 +5565,16 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + +husky@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" + integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5617,9 +5759,9 @@ is-core-module@^2.11.0: has "^1.0.3" is-core-module@^2.13.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" - integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== dependencies: hasown "^2.0.2" @@ -5661,6 +5803,18 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + is-generator-fn@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" @@ -5756,6 +5910,11 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -6457,10 +6616,10 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -known-css-properties@^0.31.0: - version "0.31.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.31.0.tgz#5c8d9d8777b3ca09482b2397f6a241e5d69a1023" - integrity sha512-sBPIUGTNF0czz0mwGGUoKKJC8Q7On1GPbCSFPfyEsfHb2DyBG0Y4QtV+EVWpINSaiGKZblDNuF5AezxSgOhesQ== +known-css-properties@^0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.34.0.tgz#ccd7e9f4388302231b3f174a8b1d5b1f7b576cea" + integrity sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ== language-subtag-registry@^0.3.20: version "0.3.23" @@ -6494,6 +6653,11 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +lilconfig@~3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -6519,6 +6683,34 @@ linkifyjs@4.1.3: resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f" integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg== +lint-staged@^15.0.2: + version "15.2.7" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" + integrity sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw== + dependencies: + chalk "~5.3.0" + commander "~12.1.0" + debug "~4.3.4" + execa "~8.0.1" + lilconfig "~3.1.1" + listr2 "~8.2.1" + micromatch "~4.0.7" + pidtree "~0.6.0" + string-argv "~0.3.2" + yaml "~2.4.2" + +listr2@~8.2.1: + version "8.2.4" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.4.tgz#486b51cbdb41889108cb7e2c90eeb44519f5a77f" + integrity sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g== + dependencies: + cli-truncate "^4.0.0" + colorette "^2.0.20" + eventemitter3 "^5.0.1" + log-update "^6.1.0" + rfdc "^1.4.1" + wrap-ansi "^9.0.0" + loader-utils@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" @@ -6590,10 +6782,21 @@ lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-update@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.1.0.tgz#1a04ff38166f94647ae1af562f4bd6a15b1b7cd4" + integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== + dependencies: + ansi-escapes "^7.0.0" + cli-cursor "^5.0.0" + slice-ansi "^7.1.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + loglevel@^1.7.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" - integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== + version "1.9.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" + integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg== long@^5.2.0: version "5.2.3" @@ -6710,13 +6913,13 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@34.2.0: - version "34.2.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-34.2.0.tgz#5e7eff9b4c15689d7f07ad3686373f821e2f06bf" - integrity sha512-dygfH/a0C/Q+a5dSfudxxwA0g9peLsBbalC6LaxPa7AEFb4Gg9d8kiGnlqaFb1U9bGUapk8duBsAC526BjXbdA== +matrix-js-sdk@34.4.0: + version "34.4.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-34.4.0.tgz#ceb3403c92dbff3b37e776745a2997ee78fa1eac" + integrity sha512-bI5xJZS3/qhjPQqQL5HhOQ1iBvnHxiqhS2zgzk9SarEuXiH08wbVl9gAAuDqOYE3miNGs4WQQJ19MoaUEOnNwg== dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^6.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^7.0.0" "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0" @@ -6724,7 +6927,7 @@ matrix-js-sdk@34.2.0: jwt-decode "^4.0.0" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.6.0" + matrix-widget-api "^1.8.2" oidc-client-ts "^3.0.1" p-retry "4" sdp-transform "^2.14.1" @@ -6742,10 +6945,10 @@ matrix-web-i18n@^3.2.1: minimist "^1.2.8" walk "^2.3.15" -matrix-widget-api@^1.5.0, matrix-widget-api@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.6.0.tgz#f0075411edffc6de339580ade7e6e6e6edb01af4" - integrity sha512-VXIJyAZ/WnBmT4C7ePqevgMYGneKMCP/0JuCOqntSsaNlCRHJvwvTxmqUU+ufOpzIF5gYNyIrAjbgrEbK3iqJQ== +matrix-widget-api@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.8.2.tgz#28d344502a85593740f560b0f8120e474a054505" + integrity sha512-kdmks3CvFNPIYN669Y4rO13KrazDvX8KHC7i6jOzJs8uZ8s54FNkuRVVyiQHeVCSZG5ixUqW9UuCj9lf03qxTQ== dependencies: "@types/events" "^3.0.0" events "^3.2.0" @@ -6809,7 +7012,7 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.4, micromatch@^4.0.7: +micromatch@^4.0.4, micromatch@^4.0.7, micromatch@~4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== @@ -6839,6 +7042,16 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -6956,9 +7169,9 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== normalize-package-data@^2.5.0: version "2.5.0" @@ -6982,6 +7195,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + nwsapi@^2.2.2: version "2.2.7" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" @@ -7049,15 +7269,6 @@ object.groupby@^1.0.1: es-abstract "^1.22.1" get-intrinsic "^1.2.1" -object.hasown@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" - integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== - dependencies: - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" @@ -7095,6 +7306,20 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -7229,6 +7454,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -7275,6 +7505,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pidtree@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -7299,17 +7534,17 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.45.2, playwright-core@^1.45.1: - version "1.45.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.2.tgz#c8b8b7f66eda47fb2bd24e5435c92d1163022df8" - integrity sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw== +playwright-core@1.45.3, playwright-core@^1.45.1: + version "1.45.3" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.3.tgz#e77bc4c78a621b96c3e629027534ee1d25faac93" + integrity sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA== -playwright@1.45.2: - version "1.45.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.2.tgz#21082072120a2c8a7e3bbb2792e81e8aa367b7a7" - integrity sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g== +playwright@1.45.3: + version "1.45.3" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.3.tgz#75143f73093a6e1467f7097083d2f0846fb8dd2f" + integrity sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww== dependencies: - playwright-core "1.45.2" + playwright-core "1.45.3" optionalDependencies: fsevents "2.3.2" @@ -7346,9 +7581,9 @@ postcss-media-query-parser@^0.2.3: integrity sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig== postcss-resolve-nested-selector@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" - integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== + version "0.1.4" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.4.tgz#0068767902fb40f0e6cd7b24faee4fa4bc14a5da" + integrity sha512-R6vHqZWgVnTAPq0C+xjyHfEZqfIYboCBVSy24MjxEDm+tIh1BU4O6o7DP7AA7kHzf136d+Qc5duI4tlpHjixDw== postcss-safe-parser@^7.0.0: version "7.0.0" @@ -7361,9 +7596,9 @@ postcss-scss@^4.0.4: integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A== postcss-selector-parser@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" - integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38" + integrity sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -7382,19 +7617,19 @@ postcss@^8.3.11: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.38: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== +postcss@^8.4.39: + version "8.4.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" + integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw== dependencies: nanoid "^3.3.7" - picocolors "^1.0.0" + picocolors "^1.0.1" source-map-js "^1.2.0" -posthog-js@1.145.0: - version "1.145.0" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.145.0.tgz#5159459f02988b74407a1dd2b19469c422b31feb" - integrity sha512-LQdH6S2Ks3mnCI0q9aD5SZS0Uujc/90nuJuEeGDeGkWkVkYOSQJt4n0UHrIWEsZdmIKZf0a6OIBhTmO+yUiY3w== +posthog-js@1.149.1: + version "1.149.1" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.149.1.tgz#8c15ca4fa2b9261abbfd4977921b42cc68ffe585" + integrity sha512-n3mkDlV0vJ1QhkDkWwUzY9RIFTPbzDzbKRyjzRE4D6H2PoH3vsrR05DNujoCr3t0hqgsaO4RLXO3VlctpdkGKQ== dependencies: fflate "^0.4.8" preact "^10.19.3" @@ -7415,10 +7650,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" - integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== +prettier@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== pretty-format@^27.0.2: version "27.5.1" @@ -7923,6 +8158,14 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-5.1.0.tgz#0766d95699efacb14150993f55baf0953ea1ebe7" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + dependencies: + onetime "^7.0.0" + signal-exit "^4.1.0" + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -7938,6 +8181,11 @@ rfc4648@^1.4.0: resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.3.tgz#e62b81736c10361ca614efe618a566e93d0b41c0" integrity sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ== +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -7946,11 +8194,12 @@ rimraf@^3.0.2: glob "^7.1.3" rimraf@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.0.tgz#503bb3d9283272384c121792d40e7ee3ab763cde" - integrity sha512-u+yqhM92LW+89cxUQK0SRyvXYQmyuKHx0jkx4W7KfwLGLqJnQM5031Uv1trE4gB9XEXBM/s6MxKlfW95IidqaA== + version "6.0.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e" + integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A== dependencies: glob "^11.0.0" + package-json-from-dist "^1.0.0" run-parallel@^1.1.9: version "1.2.0" @@ -8037,9 +8286,9 @@ schema-utils@^3.0.0: ajv-keywords "^3.5.2" sdp-transform@^2.14.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.1.tgz#2bb443583d478dee217df4caa284c46b870d5827" - integrity sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw== + version "2.14.2" + resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.2.tgz#d2cee6a1f7abe44e6332ac6cbb94e8600f32d813" + integrity sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA== seedrandom@^3.0.5: version "3.0.5" @@ -8070,7 +8319,12 @@ semver@^7.5.4: dependencies: lru-cache "^6.0.0" -semver@^7.6.0, semver@^7.6.1: +semver@^7.6.0: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +semver@^7.6.1: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -8175,7 +8429,7 @@ signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -8204,6 +8458,22 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + source-map-js@^1.0.1, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" @@ -8290,6 +8560,11 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +string-argv@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" + integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -8325,6 +8600,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string.prototype.includes@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" @@ -8430,6 +8714,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -8455,24 +8744,24 @@ stylelint-config-standard@^36.0.0: stylelint-config-recommended "^14.0.1" stylelint-scss@^6.0.0: - version "6.3.2" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.3.2.tgz#085072e774e5a31e65aa2acefaad5417a29d6ec1" - integrity sha512-pNk9mXOVKkQtd+SROPC9io8ISSgX+tOVPhFdBE+LaKQnJMLdWPbGKAGYv4Wmf/RrnOjkutunNTN9kKMhkdE5qA== + version "6.4.1" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.4.1.tgz#78a197bbcdf9a61b7365769a9a42dddc722a24c5" + integrity sha512-+clI2bQC2FPOt06ZwUlXZZ95IO2C5bKTP0GLN1LNQPVvISfSNcgMKv/VTwym1mK9vnqhHbOk8lO4rj4nY7L9pw== dependencies: - known-css-properties "^0.31.0" + known-css-properties "^0.34.0" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" postcss-selector-parser "^6.1.0" postcss-value-parser "^4.2.0" stylelint@^16.1.0: - version "16.6.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.6.1.tgz#84735aca2bb5cde535572b7a9b878d2ec983a570" - integrity sha512-yNgz2PqWLkhH2hw6X9AweV9YvoafbAD5ZsFdKN9BvSDVwGvPh+AUIrn7lYwy1S7IHmtFin75LLfX1m0D2tHu8Q== + version "16.7.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.7.0.tgz#5f6acf516aedecba7a6472ba0cc1ffc20e2be86b" + integrity sha512-Q1ATiXlz+wYr37a7TGsfvqYn2nSR3T/isw3IWlZQzFzCNoACHuGBb6xBplZXz56/uDRJHIygxjh7jbV/8isewA== dependencies: - "@csstools/css-parser-algorithms" "^2.6.3" - "@csstools/css-tokenizer" "^2.3.1" - "@csstools/media-query-list-parser" "^2.1.11" + "@csstools/css-parser-algorithms" "^2.7.1" + "@csstools/css-tokenizer" "^2.4.1" + "@csstools/media-query-list-parser" "^2.1.13" "@csstools/selector-specificity" "^3.1.1" "@dual-bundle/import-meta-resolve" "^4.1.0" balanced-match "^2.0.0" @@ -8480,7 +8769,7 @@ stylelint@^16.1.0: cosmiconfig "^9.0.0" css-functions-list "^3.2.2" css-tree "^2.3.1" - debug "^4.3.4" + debug "^4.3.5" fast-glob "^3.3.2" fastest-levenshtein "^1.0.16" file-entry-cache "^9.0.0" @@ -8491,13 +8780,13 @@ stylelint@^16.1.0: ignore "^5.3.1" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.31.0" + known-css-properties "^0.34.0" mathml-tag-names "^2.1.3" meow "^13.2.0" micromatch "^4.0.7" normalize-path "^3.0.0" picocolors "^1.0.1" - postcss "^8.4.38" + postcss "^8.4.39" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^7.0.0" postcss-selector-parser "^6.1.0" @@ -8809,10 +9098,10 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typescript@5.5.3: - version "5.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" - integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== ua-parser-js@^1.0.2: version "1.0.38" @@ -8877,15 +9166,15 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.13: - version "1.0.16" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" - integrity sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: escalade "^3.1.2" picocolors "^1.0.1" -uri-js@^4.2.2, uri-js@^4.4.1: +uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== @@ -9192,6 +9481,15 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -9260,6 +9558,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@~2.4.2: + version "2.4.5" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" + integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"