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/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml index a64bc3241b..40855b85c6 100644 --- a/.github/actions/download-verify-element-tarball/action.yml +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -31,7 +31,9 @@ runs: - name: Move webapp to out-file-path shell: bash - run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }} + run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp "$OUT_PATH" + env: + OUT_PATH: ${{ inputs.out-file-path }} - name: Clean up temp directory shell: bash diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 0182b45351..0ae746d438 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -18,8 +18,6 @@ on: push: # We do not build on push to develop as the merge_group check handles that branches: [staging, master] - repository_dispatch: - types: [element-web-notify] # support triggering from other workflows workflow_call: @@ -188,6 +186,7 @@ jobs: uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main # zizmor: ignore[unpinned-uses] with: webapp-artifact: webapp + reporter: blob prepare_ed: name: "Prepare Element Desktop" @@ -246,6 +245,7 @@ jobs: needs: - playwright_ew - downstream-modules + - prepare_ed - build_ed_windows - build_ed_linux - build_ed_macos diff --git a/.github/workflows/shared-component-visual-tests-netlify.yaml b/.github/workflows/shared-component-visual-tests-netlify.yaml index 1f9ae76826..e4b830406d 100644 --- a/.github/workflows/shared-component-visual-tests-netlify.yaml +++ b/.github/workflows/shared-component-visual-tests-netlify.yaml @@ -2,7 +2,9 @@ # It uploads the received images and diffs to netlify, printing the URLs to the console name: Upload Shared Component Visual Test Diffs on: - workflow_run: + # Privilege escalation necessary to deploy to Netlify + # 🚨 We must not execute any checked out code here. + workflow_run: # zizmor: ignore[dangerous-triggers] workflows: ["Shared Component Visual Tests"] types: - completed diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 73efd48ba3..e934f05ad1 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,6 +1,8 @@ name: SonarQube on: - workflow_run: + # Privilege escalation necessary to call upon SonarCloud + # 🚨 We must not execute any checked out code here. + workflow_run: # zizmor: ignore[dangerous-triggers] workflows: ["Tests"] types: - completed diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index f3052ff373..3dd7da0e39 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -5,8 +5,6 @@ on: branches: [develop, master] merge_group: types: [checks_requested] - repository_dispatch: - types: [element-web-notify] concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dbec96db01..60730451f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,8 +5,6 @@ on: types: [checks_requested] push: branches: [develop, master] - repository_dispatch: - types: [element-web-notify] workflow_call: inputs: disable_coverage: diff --git a/apps/web/playwright/e2e/crypto/toasts.spec.ts b/apps/web/playwright/e2e/crypto/toasts.spec.ts index 876000009b..9ce1b8a5ae 100644 --- a/apps/web/playwright/e2e/crypto/toasts.spec.ts +++ b/apps/web/playwright/e2e/crypto/toasts.spec.ts @@ -43,11 +43,7 @@ test.describe("Key storage out of sync toast", () => { }); 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/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 2a998720e9..ee058b786a 100644 Binary files a/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index 1197cbabe6..bdca70276d 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -70,6 +70,7 @@ @import "./structures/_MatrixChat.pcss"; @import "./structures/_MessagePanel.pcss"; @import "./structures/_NonUrgentToastContainer.pcss"; +@import "./structures/_PictureInPictureDragger.pcss"; @import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_RightPanel.pcss"; @import "./structures/_RoomSearch.pcss"; @@ -375,7 +376,6 @@ @import "./views/voip/_DialPad.pcss"; @import "./views/voip/_DialPadContextMenu.pcss"; @import "./views/voip/_DialPadModal.pcss"; -@import "./views/voip/_LegacyCallPreview.pcss"; @import "./views/voip/_LegacyCallView.pcss"; @import "./views/voip/_LegacyCallViewForRoom.pcss"; @import "./views/voip/_LegacyCallViewHeader.pcss"; diff --git a/apps/web/res/css/structures/_PictureInPictureDragger.pcss b/apps/web/res/css/structures/_PictureInPictureDragger.pcss new file mode 100644 index 0000000000..d6effd3b20 --- /dev/null +++ b/apps/web/res/css/structures/_PictureInPictureDragger.pcss @@ -0,0 +1,20 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +.mx_PictureInPictureDragger { + cursor: grab; + user-select: none; + left: 0; + position: fixed; + top: 0; + /* Display above any widget elements */ + z-index: 102; +} + +.mx_PictureInPictureDragger:active { + cursor: grabbing; +} diff --git a/apps/web/res/css/views/rooms/_EventTile.pcss b/apps/web/res/css/views/rooms/_EventTile.pcss index 5bea427961..63d1bdaee2 100644 --- a/apps/web/res/css/views/rooms/_EventTile.pcss +++ b/apps/web/res/css/views/rooms/_EventTile.pcss @@ -1378,6 +1378,10 @@ $left-gutter: 64px; display: flex; } +.mx_EventTile_annotatedInline { + display: inline-flex; +} + .mx_EventTile_footer { display: flex; gap: var(--cpd-space-2x); diff --git a/apps/web/res/css/views/voip/_LegacyCallPreview.pcss b/apps/web/res/css/views/voip/_LegacyCallPreview.pcss deleted file mode 100644 index 3a8bf5af9f..0000000000 --- a/apps/web/res/css/views/voip/_LegacyCallPreview.pcss +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021 Šimon Brandner - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_LegacyCallPreview { - align-items: flex-end; - display: flex; - flex-direction: column; - gap: $spacing-16; - left: 0; - position: fixed; - top: 0; - /* Display above any widget elements */ - z-index: 102; - - .mx_VideoFeed_remote.mx_VideoFeed_voice { - min-height: 150px; - } - - .mx_VideoFeed_local { - border-radius: 8px; - overflow: hidden; - } -} diff --git a/apps/web/src/RoomInvite.tsx b/apps/web/src/RoomInvite.tsx index feefdf7244..0b789a5766 100644 --- a/apps/web/src/RoomInvite.tsx +++ b/apps/web/src/RoomInvite.tsx @@ -7,9 +7,10 @@ Please see LICENSE files in the repository root for full details. */ import React, { type ComponentProps } from "react"; -import { EventType, type MatrixClient, type MatrixEvent, type Room, type User } from "matrix-js-sdk/src/matrix"; +import { EventType, type MatrixEvent, type Room, type User } from "matrix-js-sdk/src/matrix"; -import MultiInviter, { type CompletionStates, type MultiInviterOptions } from "./utils/MultiInviter"; +import type MultiInviter from "./utils/MultiInviter"; +import { type CompletionStates } from "./utils/MultiInviter"; import Modal from "./Modal"; import { _t } from "./languageHandler"; import InviteDialog from "./components/views/dialogs/InviteDialog"; @@ -19,33 +20,6 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import { InviteKind } from "./components/views/dialogs/InviteDialogTypes"; import { type Member } from "./utils/direct-messages"; -export interface IInviteResult { - states: CompletionStates; - inviter: MultiInviter; -} - -/** - * Invites multiple addresses to a room. - * - * Simpler interface to {@link MultiInviter}. - * - * Any failures are returned via the `states` in the result. - * - * @param {string} roomId The ID of the room to invite to - * @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids. - * @param options Options object. - * @returns {Promise} Promise - */ -export async function inviteMultipleToRoom( - client: MatrixClient, - roomId: string, - addresses: string[], - options: MultiInviterOptions = {}, -): Promise { - 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/audio/VoiceRecording.ts b/apps/web/src/audio/VoiceRecording.ts index 705b96375a..44a016b995 100644 --- a/apps/web/src/audio/VoiceRecording.ts +++ b/apps/web/src/audio/VoiceRecording.ts @@ -103,10 +103,14 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private async makeRecorder(): Promise { try { + const requestedDeviceId = MediaDeviceHandler.getAudioInput(); + const deviceIdConstraint = + requestedDeviceId && requestedDeviceId !== "default" ? { deviceId: { exact: requestedDeviceId } } : {}; + this.recorderStream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: CHANNELS, - deviceId: MediaDeviceHandler.getAudioInput(), + ...deviceIdConstraint, autoGainControl: { ideal: MediaDeviceHandler.getAudioAutoGainControl() }, echoCancellation: { ideal: MediaDeviceHandler.getAudioEchoCancellation() }, noiseSuppression: { ideal: MediaDeviceHandler.getAudioNoiseSuppression() }, diff --git a/apps/web/src/components/structures/PictureInPictureDragger.tsx b/apps/web/src/components/structures/PictureInPictureDragger.tsx index 2cadc59a7b..d9f472c311 100644 --- a/apps/web/src/components/structures/PictureInPictureDragger.tsx +++ b/apps/web/src/components/structures/PictureInPictureDragger.tsx @@ -37,9 +37,7 @@ interface IChildrenOptions { } interface IProps { - className?: string; children: Array; - draggable: boolean; onDoubleClick?: () => void; onMove?: () => void; } @@ -181,9 +179,6 @@ export default class PictureInPictureDragger extends React.Component { }; private onStartMoving = (event: React.MouseEvent | MouseEvent): void => { - event.preventDefault(); - event.stopPropagation(); - this.mouseHeld = true; this.startingPositionX = event.clientX; this.startingPositionY = event.clientY; @@ -217,9 +212,6 @@ export default class PictureInPictureDragger extends React.Component { private onEndMoving = (event: MouseEvent): void => { if (!this.mouseHeld) return; - event.preventDefault(); - event.stopPropagation(); - this.mouseHeld = false; // Delaying this to the next event loop tick is necessary for click // event cancellation to work @@ -250,7 +242,7 @@ export default class PictureInPictureDragger extends React.Component { return (