diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5a762fe224..4d173ab222 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,12 +4,13 @@ /pnpm-lock.yaml @element-hq/element-web-team /apps/web/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers -/apps/web/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers /apps/web/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/async-components/dialogs/security/ @element-hq/element-crypto-web-reviewers /apps/web/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers -/apps/web/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers /apps/web/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers -/apps/web/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers +/apps/web/test/unit-tests/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers /apps/web/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers /apps/web/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers /apps/web/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 1c66b02414..0ae746d438 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -163,7 +163,7 @@ jobs: - name: Run Playwright tests working-directory: apps/web run: | - pnpm playwright test \ + pnpm test:playwright \ --shard "$SHARD" \ --project="${{ matrix.project }}" \ ${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }} diff --git a/.github/workflows/shared-component-publish.yaml b/.github/workflows/npm-publish.yaml similarity index 69% rename from .github/workflows/shared-component-publish.yaml rename to .github/workflows/npm-publish.yaml index c728c303d5..708d233d26 100644 --- a/.github/workflows/shared-component-publish.yaml +++ b/.github/workflows/npm-publish.yaml @@ -1,6 +1,15 @@ -name: Publish shared component npm package +name: Publish npm package +run-name: Publish ${{ inputs.package }} on: - workflow_dispatch: {} + workflow_dispatch: + inputs: + package: + description: Which package to release + required: true + type: choice + options: + - playwright-common + - shared-components concurrency: release jobs: @@ -29,10 +38,9 @@ jobs: - name: Update npm run: npm install -g npm@latest - # Need to setup element web too as it needs the translations - - name: 🛠️ Setup EW + - name: 🛠️ Install dependencies run: pnpm install --frozen-lockfile - name: 🚀 Publish to npm - working-directory: packages/shared-components + working-directory: packages/${{ inputs.package }} run: npm publish --access public --provenance diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a4f9c68437..eb63cd55ee 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -105,7 +105,7 @@ "typescript": "5.9.3" }, "hakDependencies": { - "matrix-seshat": "^4.0.1" + "matrix-seshat": "4.0.1" }, "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" } diff --git a/apps/web/package.json b/apps/web/package.json index 98b6f8c876..6640de4ff9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,9 +30,9 @@ "lint:types": "nx lint:types", "lint:style": "stylelint \"res/css/**/*.pcss\"", "test": "nx test:unit", - "test:playwright": "playwright test", - "test:playwright:open": "pnpm test:playwright --ui", - "test:playwright:screenshots": "playwright-screenshots-experimental pnpm playwright test --update-snapshots --project=Chrome --grep @screenshot", + "test:playwright": "nx test:playwright --", + "test:playwright:open": "nx test:playwright -- --ui", + "test:playwright:screenshots": "nx test:playwright:screenshots --", "coverage": "pnpm test --coverage", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp" }, @@ -127,8 +127,7 @@ "@babel/preset-typescript": "^7.12.7", "@casualbot/jest-sonar-reporter": "2.5.0", "@element-hq/element-call-embedded": "0.18.0", - "@element-hq/element-web-playwright-common": "catalog:", - "@element-hq/element-web-playwright-common-local": "workspace:*", + "@element-hq/element-web-playwright-common": "workspace:*", "@fetch-mock/jest": "^0.2.20", "@jest/globals": "^30.2.0", "@peculiar/webcrypto": "^1.4.3", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index f6d15f4e79..d2d787f46f 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -23,6 +23,7 @@ const chromeProject: Project { }); test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => { - // We need to wait for there to be two toasts as the wait below won't work in isolation: - // playwright only evaluates the 'first()' call initially, not subsequent times it checks, so - // it would always be checking the same toast, even if another one is now the first. - await expect(page.getByRole("alert")).toHaveCount(2); - await expect(page.getByRole("alert").first()).toMatchScreenshot( + await expect(page.getByRole("alert").filter({ hasText: "Your key storage is out of sync." })).toMatchScreenshot( "key-storage-out-of-sync-toast.png", screenshotOptions, ); diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 5907ad6d97..79cc0b4cf3 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -328,11 +328,11 @@ test.describe("Room list", () => { const roomListView = getRoomList(page); const videoRoom = roomListView.getByRole("option", { name: "video room" }); + await expect(videoRoom).toHaveAttribute("aria-selected", "true"); // wait for room list update // focus the user menu to avoid to have hover decoration await page.getByRole("button", { name: "User menu" }).focus(); - await expect(videoRoom).toBeVisible(); await expect(videoRoom).toMatchScreenshot("room-list-item-video.png"); }); }); diff --git a/apps/web/playwright/e2e/room-directory/room-directory.spec.ts b/apps/web/playwright/e2e/room-directory/room-directory.spec.ts index 741fde3505..6eea5abce7 100644 --- a/apps/web/playwright/e2e/room-directory/room-directory.spec.ts +++ b/apps/web/playwright/e2e/room-directory/room-directory.spec.ts @@ -48,9 +48,9 @@ test.describe("Room Directory", () => { await app.closeDialog(); const resp = await bot.publicRooms({}); - expect(resp.total_room_count_estimate).toEqual(1); - expect(resp.chunk).toHaveLength(1); - expect(resp.chunk[0].room_id).toEqual(roomId); + expect(resp.total_room_count_estimate).toBeGreaterThanOrEqual(1); + expect(resp.chunk).toHaveLength(resp.total_room_count_estimate); + expect(resp.chunk.find((r) => r.room_id === roomId)).toBeTruthy(); }, ); diff --git a/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png b/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png index dcdd2cb0f7..904e23c35a 100644 Binary files a/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png and b/apps/web/playwright/snapshots/crypto/complete-security.spec.ts/complete-security-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 3be7831755..0a99a4cf86 100644 Binary files a/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/apps/web/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png b/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png index 78ff359a99..f2e5ab1350 100644 Binary files a/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png and b/apps/web/playwright/snapshots/crypto/device-verification.spec.ts/recovery-key-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png b/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png index b84cdd4e8f..adea2eac4e 100644 Binary files a/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png and b/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png b/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png index 7aeac7a9a9..01ad3384c1 100644 Binary files a/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png and b/apps/web/playwright/snapshots/crypto/toasts.spec.ts/verify-this-device-linux.png differ diff --git a/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index ada2b8a60e..7fb7e011bb 100644 Binary files a/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/apps/web/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png index a42cb394d7..a6b0afc57d 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png index a9f601e822..2fa939ff85 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png index be1a42d63f..f340588e3e 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png index 9108514219..5700565252 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png differ diff --git a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png index 87d1cc85a4..bd69b42ab1 100644 Binary files a/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png and b/apps/web/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png differ diff --git a/apps/web/project.json b/apps/web/project.json index 95f9a6828d..59fa9f3a66 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -44,7 +44,7 @@ "parallel": false, "cwd": "apps/web" }, - "dependsOn": ["^build"] + "dependsOn": ["^build", "^build:playwright"] }, "test:unit": { "executor": "@nx/jest:jest", @@ -53,6 +53,16 @@ "cwd": "apps/web" }, "dependsOn": ["^build"] + }, + "test:playwright": { + "command": "playwright test", + "options": { "cwd": "apps/web" }, + "dependsOn": ["^build:playwright"] + }, + "test:playwright:screenshots": { + "command": "playwright-screenshots nx test:playwright --update-snapshots --project=Chrome --grep @screenshot", + "options": { "cwd": "apps/web" }, + "dependsOn": ["^build:playwright"] } } } diff --git a/apps/web/src/Notifier.ts b/apps/web/src/Notifier.ts index dbe890909e..b0a2ce5b42 100644 --- a/apps/web/src/Notifier.ts +++ b/apps/web/src/Notifier.ts @@ -81,6 +81,59 @@ const msgTypeHandlers: Record string | null> = { }, }; +/** + * Extracts plain text from a message body, replacing any spoilered content + * with '[Spoiler]' to prevent spoilers in desktop notifications. + */ +function getNotificationBodyWithoutSpoilers(ev: MatrixEvent): string { + const content = ev.getContent(); + const plainBody = content.body ?? ""; + const formattedBody = content.formatted_body; + + if (typeof formattedBody !== "string" || !formattedBody.length) { + return plainBody; + } + + /** Recursively walks HTML tree to hide spoilers. */ + function replaceSpoilers(node: Node): Node { + if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof Element)) { + return node; + } + + if (node.hasAttribute("data-mx-spoiler")) { + const e = document.createElement("span"); + e.appendChild(document.createTextNode("[Spoiler]")); + return e; + } + + for (const childNode of node.childNodes) { + node.replaceChild(replaceSpoilers(childNode), childNode); + } + + return node; + } + + try { + // Dev note: ideally we would reuse more of the existing rendering stack + // rather than re-parsing and updating the generated HTML here. However, + // that rendering stack is currently quite consolidated and cannot + // easily be refactored to allow the call-site to control how spoilers + // are rendered. The problem is that we now need two different output + // formats: + // - The existing format where spoilers are wrapped in html tags + // - The new format where the spoilered text is replaced with [Spoiler] + + const parser = new DOMParser(); + const doc = parser.parseFromString(formattedBody, "text/html"); + + // Use textContent rather than innerHTML/outerHTML since textContent is + // XSS-safe and the input is untrusted. + return replaceSpoilers(doc.body).textContent ?? plainBody; + } catch { + return plainBody; + } +} + export const enum NotifierEvent { NotificationHiddenChange = "notification_hidden_change", } @@ -134,7 +187,7 @@ class NotifierClass extends TypedEventEmitter { - const inviter = new MultiInviter(client, roomId, options); - return { states: await inviter.invite(addresses), inviter }; -} - export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. Modal.createDialog( diff --git a/apps/web/src/components/structures/SpaceRoomView.tsx b/apps/web/src/components/structures/SpaceRoomView.tsx index 7b7b79c126..9bb04ce552 100644 --- a/apps/web/src/components/structures/SpaceRoomView.tsx +++ b/apps/web/src/components/structures/SpaceRoomView.tsx @@ -34,7 +34,7 @@ import { useFeatureEnabled } from "../../hooks/useSettings"; import { useStateArray } from "../../hooks/useStateArray"; import { _t } from "../../languageHandler"; import PosthogTrackers from "../../PosthogTrackers"; -import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite"; +import { showRoomInviteDialog } from "../../RoomInvite"; import { UIComponent } from "../../settings/UIFeature"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import RightPanelStore from "../../stores/right-panel/RightPanelStore"; @@ -76,6 +76,7 @@ import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import SpacePillButton from "./SpacePillButton.tsx"; import { useRoomName } from "../../hooks/useRoomName.ts"; +import MultiInviter from "../../utils/MultiInviter.ts"; interface IProps { space: Room; @@ -538,11 +539,12 @@ const SpaceSetupPrivateInvite: React.FC<{ setBusy(true); const targetIds = emailAddresses.map((name) => name.trim()).filter(Boolean); try { - const result = await inviteMultipleToRoom(space.client, space.roomId, targetIds); + const inviter = new MultiInviter(space.client, space.roomId); + const states = await inviter.invite(targetIds); - const failedUsers = Object.keys(result.states).filter((a) => result.states[a] === "error"); + const failedUsers = Object.keys(states).filter((a) => states[a] === "error"); if (failedUsers.length > 0) { - logger.log("Failed to invite users to space: ", result); + logger.log("Failed to invite users to space:", states); setError( _t("create_space|failed_invite_users", { csvUsers: failedUsers.join(", "), diff --git a/apps/web/src/components/views/dialogs/BugReportDialog.tsx b/apps/web/src/components/views/dialogs/BugReportDialog.tsx index 2977570957..1b108bcb52 100644 --- a/apps/web/src/components/views/dialogs/BugReportDialog.tsx +++ b/apps/web/src/components/views/dialogs/BugReportDialog.tsx @@ -120,7 +120,7 @@ export default class BugReportDialog extends React.Component

{errorText}

{error.policyURL && ( - + {_t("action|learn_more")} )} diff --git a/apps/web/src/components/views/dialogs/InviteDialog.tsx b/apps/web/src/components/views/dialogs/InviteDialog.tsx index f23aadcdad..8a9c36e5de 100644 --- a/apps/web/src/components/views/dialogs/InviteDialog.tsx +++ b/apps/web/src/components/views/dialogs/InviteDialog.tsx @@ -25,7 +25,7 @@ import { getDefaultIdentityServerUrl, setToDefaultIdentityServer } from "../../. import { buildActivityScores, buildMemberScores, compareMembers } from "../../../utils/SortMembers"; import { abbreviateUrl } from "../../../utils/UrlUtils"; import IdentityAuthClient from "../../../IdentityAuthClient"; -import { type IInviteResult, inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite"; +import { showAnyInviteErrors } from "../../../RoomInvite"; import { Action } from "../../../dispatcher/actions"; import { DefaultTagID } from "../../../stores/room-list-v3/skip-list/tag"; import RoomListStore from "../../../stores/room-list/RoomListStore"; @@ -63,6 +63,7 @@ import { type NonEmptyArray } from "../../../@types/common"; import { SdkContextClass } from "../../../contexts/SDKContext"; import { type UserProfilesStore } from "../../../stores/UserProfilesStore"; import InviteProgressBody from "./InviteProgressBody.tsx"; +import MultiInviter, { type CompletionStates as MultiInviterCompletionStates } from "../../../utils/MultiInviter.ts"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -409,10 +410,14 @@ export default class InviteDialog extends React.PureComponent ({ userId: member.userId, user: toMember(member) })); } - private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean { + private shouldAbortAfterInviteError( + states: MultiInviterCompletionStates, + inviter: MultiInviter, + room: Room, + ): boolean { this.setState({ busy: false }); const userMap = new Map(this.state.targets.map((member) => [member.userId, member])); - return !showAnyInviteErrors(result.states, room, result.inviter, userMap); + return !showAnyInviteErrors(states, room, inviter, userMap); } private convertFilter(): Member[] { @@ -483,11 +488,12 @@ export default class InviteDialog extends React.PureComponent
- + diff --git a/apps/web/src/device-listener/DeviceListenerCurrentDevice.ts b/apps/web/src/device-listener/DeviceListenerCurrentDevice.ts index 7dbb976489..0460d82366 100644 --- a/apps/web/src/device-listener/DeviceListenerCurrentDevice.ts +++ b/apps/web/src/device-listener/DeviceListenerCurrentDevice.ts @@ -228,21 +228,25 @@ export class DeviceListenerCurrentDevice { logSpan.info("No default 4S key but backup disabled: no toast needed"); await this.setDeviceState("ok", logSpan); } - } else { - // If we get here, then we are verified, have key backup, and - // 4S, but allSystemsReady is false, which means that either - // secretStorageStatus.ready is false (which means that 4S - // doesn't have all the secrets), or we don't have the backup - // key cached locally. If any of the cross-signing keys are - // missing locally, that is handled by the - // `!allCrossSigningSecretsCached` branch above. - logSpan.warn("4S is missing secrets or backup key not cached", { + } else if (!recoveryIsOk) { + logSpan.warn("4S is missing secrets: setting state to KEY_STORAGE_OUT_OF_SYNC", { secretStorageStatus, allCrossSigningSecretsCached, isCurrentDeviceTrusted, keyBackupDownloadIsOk, }); await this.setDeviceState("key_storage_out_of_sync", logSpan); + } else if (!keyBackupDownloadIsOk) { + logSpan.warn("Backup key is not cached locally: setting state to KEY_STORAGE_OUT_OF_SYNC", { + secretStorageStatus, + allCrossSigningSecretsCached, + isCurrentDeviceTrusted, + keyBackupDownloadIsOk, + }); + await this.setDeviceState("key_storage_out_of_sync", logSpan); + } else { + // We should not get here + logSpan.error("DeviceListenerCurrentDevice: allSystemsReady was false, but no case matched."); } } } diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json index 481a2ad478..ac4b3d3e9a 100644 --- a/apps/web/src/i18n/strings/en_EN.json +++ b/apps/web/src/i18n/strings/en_EN.json @@ -1114,7 +1114,7 @@ "verification_requested_toast_title": "Verification requested", "verified_identity_changed": "%(displayName)s's (%(userId)s) digital identity was reset. Learn more", "verified_identity_changed_no_displayname": "%(userId)s's digital identity was reset. Learn more", - "verify_toast_description": "As of end of April 2026, unverified devices will not be able to send and receive messages. Learn more", + "verify_toast_description": "As of end of October 2026, unverified devices will not be able to send and receive messages. Learn more", "verify_toast_title": "Verify this device", "withdraw_verification_action": "Withdraw verification" }, diff --git a/apps/web/src/utils/RoomUpgrade.ts b/apps/web/src/utils/RoomUpgrade.ts index 6fe6a2f4d1..d92038e0db 100644 --- a/apps/web/src/utils/RoomUpgrade.ts +++ b/apps/web/src/utils/RoomUpgrade.ts @@ -10,13 +10,13 @@ import { ClientEvent, EventType, type MatrixClient, type Room } from "matrix-js- import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; -import { inviteMultipleToRoom, showAnyInviteErrors } from "../RoomInvite"; +import { showAnyInviteErrors } from "../RoomInvite"; import Modal, { type IHandle } from "../Modal"; import { _t } from "../languageHandler"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import SpaceStore from "../stores/spaces/SpaceStore"; import Spinner from "../components/views/elements/Spinner"; -import type { MultiInviterOptions } from "./MultiInviter"; +import MultiInviter, { type MultiInviterOptions } from "./MultiInviter"; export interface RoomUpgradeProgress { roomUpgraded: boolean; @@ -158,7 +158,8 @@ async function inviteUsersToRoom( userIds: string[], inviteOptions: MultiInviterOptions, ): Promise { - const result = await inviteMultipleToRoom(client, roomId, userIds, inviteOptions); + const inviter = new MultiInviter(client, roomId, inviteOptions); + const states = await inviter.invite(userIds); const room = client.getRoom(roomId)!; - showAnyInviteErrors(result.states, room, result.inviter); + showAnyInviteErrors(states, room, inviter); } diff --git a/apps/web/test/unit-tests/Notifier-test.ts b/apps/web/test/unit-tests/Notifier-test.ts index ee3a42dfda..bd1b20c0be 100644 --- a/apps/web/test/unit-tests/Notifier-test.ts +++ b/apps/web/test/unit-tests/Notifier-test.ts @@ -358,6 +358,46 @@ describe("Notifier", () => { reply, ); }); + + it.each([ + ["This was a triumph", "This was a triumph", "This was a triumph"], + ["This was a triumph", "This was a triumph", "[Spoiler]"], + ["This was a triumph", 'This was a triumph', "[Spoiler]"], + ["foo bar baz", "foo bar baz", "foo [Spoiler] baz"], + ["foo foo foo", "foo foo foo", "foo [Spoiler] foo"], + [ + "a b c d e", + "a b c d e", + "a [Spoiler] c [Spoiler] e", + ], + ["foo foo", "foo foo", "foo [Spoiler] foo"], + ["foo bar baz", "foo bar baz", "foo [Spoiler] baz"], + ["foobar", "foobar", "[Spoiler][Spoiler]"], + ["foo bar baz", "foo bar baz", "foo [Spoiler] baz"], + ["foo baz", "foo <bar> baz", "foo [Spoiler] baz"], + ["foo\nbar\nbaz", "foo
bar
baz", "foo[Spoiler]baz"], + ])("should hide spoilers in notification", (body, formattedBody, expected) => { + const spoilerEvent = mkEvent({ + event: true, + type: EventType.RoomMessage, + user: mockClient.getSafeUserId(), + room: testRoom.roomId, + content: { + msgtype: MsgType.Text, + body: body, + format: "org.matrix.custom.html", + formatted_body: formattedBody, + }, + }); + Notifier.displayPopupNotification(spoilerEvent, testRoom); + expect(MockPlatform.displayNotification).toHaveBeenCalledWith( + "@bob:example.org (!room1:server)", + expected, + expect.any(String), + testRoom, + spoilerEvent, + ); + }); }); describe("getSoundForRoom", () => { diff --git a/apps/web/test/unit-tests/RoomInvite-test.ts b/apps/web/test/unit-tests/RoomInvite-test.ts deleted file mode 100644 index 32ef8dc73f..0000000000 --- a/apps/web/test/unit-tests/RoomInvite-test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright 2025 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 { getMockClientWithEventEmitter } from "../test-utils"; -import { inviteMultipleToRoom } from "../../src/RoomInvite.tsx"; - -afterEach(() => { - jest.restoreAllMocks(); -}); - -describe("inviteMultipleToRoom", () => { - it("can be called wth no `options`", async () => { - const client = getMockClientWithEventEmitter({}); - const { states, inviter } = await inviteMultipleToRoom(client, "!room:id", []); - expect(states).toEqual({}); - - // @ts-ignore reference to private property - expect(inviter.options).toEqual({}); - }); -}); diff --git a/apps/web/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap b/apps/web/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap index a7abafcddb..fe327c357a 100644 --- a/apps/web/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap +++ b/apps/web/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap @@ -128,7 +128,7 @@ exports[` should match snapshot 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 not pillify MXIDs 1`] = `"Chat with @user:example.com"`; -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 not pillify room aliases 1`] = `"Visit #room:example.com"`; + +exports[` renders plain-text m.text correctly should pillify a keyword responsible for triggering a notification 1`] = `"foo bar baz"`; + +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`] = `
renders plain-text m.text correctly should pillify a pe
`; +exports[` renders plain-text m.text correctly should pillify a room alias permalink 1`] = `"Visit #room:example.com"`; + +exports[` renders plain-text m.text correctly should pillify an MXID permalink 1`] = `"Chat with Member"`; + exports[` renders plain-text m.text correctly simple message renders as expected 1`] = `
should render widgets 1`] = ` Add extensions