Compare commits

...

10 Commits

Author SHA1 Message Date
Robin
9565d2685e Stop using deprecated Element Call URL parameters
These deprecated parameters will be removed very soon (planned for Element Call version 0.15.0) and we no longer have to care about backward compatibility with old versions of Element Call (due to the embedding/bundling work), so now is the right time to migrate.
2025-07-28 20:30:35 +02:00
Richard van der Hoff
7e40e3697f MatrixChat test robustness fixes (#30413)
* MatrixChat-test: clean up better in `afterEach`

Make the MatrixChat tests behave better by letting them finish their work in an
`act` in afterEach. Otherwise we can end up mounting new components during
cleanup, which run tasks in the background

* MatrixChat-test: clean up dispatcher test

This test was kicking off a dispatcher job which would then open a
UserDeviceSettings dialog once the test had finished. That would then throw
exceptions because some of the mock environment had been torn down.

We're just testing that it opens the right dialog, so better to intercept
`createDialog`.

Aso add an `act` to reduce warnings, and replace a `flushPromises` with a
`waitFor` to make the test more robust.
2025-07-28 16:32:53 +00:00
David Langley
beaabd5b44 bump wysiwyg to 2.39.0 (#30421) 2025-07-28 15:21:24 +01:00
Robin
a23a2c03d3 Allow Element Call to send call notifications (#30404)
* Allow Element Call to send call notifications

Currently Element Web is responsible for sending the call notification event, but this is planned to be changed soon. As of the upcoming Element Call 0.14.0 release, it will request the capability to send call notifications itself, and we should auto-approve this capability.

* Add reaction capability missing from test

Element Call does in fact request this one.
2025-07-27 13:37:10 +00:00
Richard van der Hoff
c2c040dd42 Lifecycle: add a bit more logging (#30414)
... to see what exactly it thinks is wrong with the session

This may be useful in debugging
https://github.com/element-hq/element-web/issues/30337 and
https://github.com/element-hq/element-web/issues/29708. but will likely be
useful in any case.
2025-07-25 16:17:19 +01:00
Florian Duros
c98358cb26 Fix e2e shield being invisible in white mode for encrypted room (#30408)
* fix: e2e icon for encrypted room

* test(e2e): add screenshot for grey shield in encrypted room
2025-07-25 10:56:21 +00:00
Richard van der Hoff
d384a9b71b Work around jest bug that swallows console output (#30405)
* Work around jest bug that swallows console output

Hacky workaround for https://github.com/jestjs/jest/issues/15747

* Fix unit test

* Only write logs if there are some to write

* Another test fix
2025-07-25 10:13:52 +00:00
Richard van der Hoff
fc04ad26ce Support EventShieldReason.MISMATCHED_SENDER (#30403)
The js-sdk now exposes a new event shield reason, which we should handle
correctly.
2025-07-25 09:43:40 +00:00
Florian Duros
b5160c47b3 chore: move i18n.tsx into utils folder (#30400) 2025-07-25 08:14:48 +00:00
Florian Duros
3af8273d6b fix: replace hardcoded string in poll history dialog (#30402) 2025-07-24 16:41:01 +00:00
22 changed files with 102 additions and 66 deletions

View File

@@ -5,7 +5,7 @@ import "../res/css/shared.pcss";
import "./preview.css";
import React, { useLayoutEffect } from "react";
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
import { setLanguage } from "../src/shared-components/i18n";
import { setLanguage } from "../src/shared-components/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
export const globalTypes = {

View File

@@ -99,7 +99,7 @@
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^5.0.0",
"@vector-im/compound-web": "^8.1.2",
"@vector-im/matrix-wysiwyg": "2.38.4",
"@vector-im/matrix-wysiwyg": "2.39.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",

View File

@@ -158,6 +158,8 @@ test.describe("Cryptography", function () {
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob);
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon-normal.png");
await testMessages(page, bob, bobRoomId);
await verify(app, bob);

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -52,7 +52,13 @@ Please see LICENSE files in the repository root for full details.
.mx_E2EIcon_normal::after {
mask-image: url("$(res)/img/e2e/normal.svg");
background-color: white;
background-color: var(--cpd-color-icon-tertiary);
}
.mx_E2EIcon_verified {
.mx_E2EIcon_normal::after {
background-color: white;
}
}
.mx_E2EIcon_verified::after {

View File

@@ -621,6 +621,9 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
await getStoredSessionVars();
if (hasAccessToken && !accessToken) {
logger.warn(
"restoreSessionFromStorage: storage indicates we should have an access token, but we do not. Displaying StorageEvictedDialog",
);
await abortLogin();
}
@@ -823,6 +826,7 @@ async function doSetLoggedIn(
// crypto store, we'll be generally confused when handling encrypted data.
// Show a modal recommending a full reset of storage.
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
logger.warn("doSetLoggedIn: StorageManager consistency check failed; displaying StorageEvictedDialog.");
await abortLogin();
}

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
*/
// Import i18n.tsx instead of languageHandler to avoid circular deps
import { _td, type TranslationKey } from "../shared-components/i18n";
import { _td, type TranslationKey } from "../shared-components/utils/i18n";
import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
import { type IBaseSetting } from "../settings/Settings";
import { type KeyCombo } from "../KeyBindingsManager";

View File

@@ -120,8 +120,8 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({
value={filter}
onFilterChange={onFilterChange}
tabs={[
{ id: "ACTIVE", label: "Active polls" },
{ id: "ENDED", label: "Past polls" },
{ id: "ACTIVE", label: _t("right_panel|poll|active_heading") },
{ id: "ENDED", label: _t("right_panel|poll|past_heading") },
]}
/>
{!!pollStartEvents.length && (

View File

@@ -757,6 +757,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified");
break;
case EventShieldReason.MISMATCHED_SENDER:
shieldReasonMessage = _t("encryption|event_shield_reason_mismatched_sender");
break;
default:
shieldReasonMessage = _t("error|unknown");
break;

View File

@@ -934,6 +934,7 @@
"cross_signing_user_warning": "This user has not verified all of their sessions.",
"enter_recovery_key": "Enter recovery key",
"event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.",
"event_shield_reason_mismatched_sender": "The sender of the event does not match the owner of the device that sent it.",
"event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session",
"event_shield_reason_unknown_device": "Encrypted by an unknown or deleted device.",
"event_shield_reason_unsigned_device": "Encrypted by a device not verified by its owner.",

View File

@@ -25,7 +25,7 @@ import {
type IVariables,
KEY_SEPARATOR,
getLangsJson,
} from "./shared-components/i18n";
} from "./shared-components/utils/i18n";
export {
_t,
@@ -40,7 +40,7 @@ export {
normalizeLanguageKey,
getNormalizedLanguageKeys,
substitute,
} from "./shared-components/i18n";
} from "./shared-components/utils/i18n";
const i18nFolder = "i18n/";

View File

@@ -669,12 +669,12 @@ export class ElementCall extends Call {
// Splice together the Element Call URL for this call
const params = new URLSearchParams({
embed: "true", // We're embedding EC within another application
confineToRoom: "true", // Only show the call interface for the configured room
// Template variables are used, so that this can be configured using the widget data.
skipLobby: "$skipLobby", // Skip the lobby in case we show a lobby component of our own.
returnToLobby: "$returnToLobby", // Returns to the lobby (instead of blank screen) when the call ends. (For video rooms)
perParticipantE2EE: "$perParticipantE2EE",
hideHeader: "true", // Hide the header since our room header is enough
header: "none", // Hide the header since our room header is enough
userId: client.getUserId()!,
deviceId: client.getDeviceId()!,
roomId: roomId,

View File

@@ -12,7 +12,7 @@ import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
import { type MediaPreviewConfig } from "../@types/media_preview.ts";
// Import i18n.tsx instead of languageHandler to avoid circular deps
import { _t, _td, type TranslationKey } from "../shared-components/i18n";
import { _t, _td, type TranslationKey } from "../shared-components/utils/i18n";
import DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts";
import {
NotificationBodyEnabledController,

View File

@@ -25,7 +25,7 @@ import React from "react";
import { type TranslationKey as _TranslationKey, KEY_SEPARATOR } from "matrix-web-i18n";
import counterpart from "counterpart";
import type Translations from "../i18n/strings/en_EN.json";
import type Translations from "../../i18n/strings/en_EN.json";
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
import webpackLangJsonUrl from "$webapp/i18n/languages.json";

View File

@@ -168,6 +168,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw,
);
const sendRoomEvents = [EventType.CallNotify, EventType.RTCNotification];
const sendRecvRoomEvents = [
"io.element.call.encryption_keys",
"org.matrix.rageshake_request",
@@ -175,10 +176,10 @@ export class StopGapWidgetDriver extends WidgetDriver {
EventType.RoomRedaction,
"io.element.call.reaction",
];
for (const eventType of sendRecvRoomEvents) {
for (const eventType of [...sendRoomEvents, ...sendRecvRoomEvents])
this.allowedCapabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, eventType).raw);
for (const eventType of sendRecvRoomEvents)
this.allowedCapabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Receive, eventType).raw);
}
const sendRecvToDevice = [
EventType.CallInvite,

View File

@@ -6,12 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { env } from "process";
import "@testing-library/jest-dom";
import "blob-polyfill";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { mocked } from "jest-mock";
import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555
import { PredictableRandom } from "./test-utils/predictableRandom";
import * as rageshake from "../src/rageshake/rageshake";
declare global {
// eslint-disable-next-line no-var
@@ -37,6 +39,23 @@ beforeEach(() => {
});
});
// Somewhat hacky workaround for https://github.com/jestjs/jest/issues/15747: if the GHA reporter is enabled,
// capture logs using the rageshake infrastructure, then dump them out after the test.
if (env["GITHUB_ACTIONS"] !== undefined) {
beforeEach(async () => {
await rageshake.init(/* setUpPersistence = */ false);
});
afterEach(async () => {
const logs = global.mx_rage_logger.flush(/* keeplogs = */ false);
if (logs) {
process.stderr.write(`::group::Console logs from test '${expect.getState().currentTestName}'\n\n`);
process.stderr.write(logs);
process.stderr.write("::endgroup::\n");
}
});
}
// Very carefully enable the mocks for everything else in
// a specific order. We use this order to ensure we properly
// establish an application state that actually works.

View File

@@ -71,6 +71,8 @@ import { SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStor
import { ShareFormat } from "../../../../src/dispatcher/payloads/SharePayload.ts";
import { clearStorage } from "../../../../src/Lifecycle";
import RoomListStore from "../../../../src/stores/room-list/RoomListStore.ts";
import UserSettingsDialog from "../../../../src/components/views/dialogs/UserSettingsDialog.tsx";
import { SdkContextClass } from "../../../../src/contexts/SDKContext.ts";
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
completeAuthorizationCodeGrant: jest.fn(),
@@ -268,6 +270,10 @@ describe("<MatrixChat />", () => {
// (must be sync otherwise the next test will start before it happens)
act(() => defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true));
// that will cause the Login to kick off an update in the background, which we need to allow to finish within
// an `act` to avoid warnings
await flushPromises();
localStorage.clear();
});
@@ -640,22 +646,29 @@ describe("<MatrixChat />", () => {
});
describe("onAction()", () => {
beforeEach(() => {
jest.spyOn(defaultDispatcher, "dispatch").mockClear();
jest.spyOn(defaultDispatcher, "fire").mockClear();
afterEach(() => {
jest.restoreAllMocks();
});
it("should open user device settings", async () => {
it("ViewUserDeviceSettings should open user device settings", async () => {
await getComponentAndWaitForReady();
defaultDispatcher.dispatch({
action: Action.ViewUserDeviceSettings,
});
const createDialog = jest.spyOn(Modal, "createDialog").mockReturnValue({} as any);
await flushPromises();
await act(async () => {
defaultDispatcher.dispatch({
action: Action.ViewUserDeviceSettings,
});
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: Action.ViewUserSettings,
initialTabId: UserTab.SessionManager,
await waitFor(() =>
expect(createDialog).toHaveBeenCalledWith(
UserSettingsDialog,
{ initialTabId: UserTab.SessionManager, sdkContext: expect.any(SdkContextClass) },
/*className=*/ undefined,
/*isPriority=*/ false,
/*isStatic=*/ true,
),
);
});
});
@@ -674,10 +687,6 @@ describe("<MatrixChat />", () => {
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe("forget_room", () => {
it("should dispatch after_forget_room action on successful forget", async () => {
await clearAllModals();

View File

@@ -27,6 +27,7 @@ describe("BugReportDialog", () => {
return render(<BugReportDialog onFinished={onFinished} />);
}
let prevLogger: ConsoleLogger;
beforeEach(() => {
jest.resetAllMocks();
SdkConfig.put({
@@ -48,24 +49,14 @@ describe("BugReportDialog", () => {
consume: jest.fn(),
warn: jest.fn(),
} as unknown as Mocked<ConsoleLogger>;
mockConsoleLogger.flush.mockReturnValue("line 1\nline 2\n");
// @ts-ignore - mock the console logger
prevLogger = global.mx_rage_logger;
global.mx_rage_logger = mockConsoleLogger;
// @ts-ignore
mockConsoleLogger.flush.mockReturnValue([
{
id: "instance-0",
line: "line 1",
},
{
id: "instance-1",
line: "line 2",
},
]);
});
afterEach(() => {
global.mx_rage_logger = prevLogger;
jest.restoreAllMocks();
SdkConfig.reset();
fetchMock.restore();

View File

@@ -306,6 +306,10 @@ describe("EventTile", () => {
[EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
[EventShieldReason.SENT_IN_CLEAR, "Not encrypted"],
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity was reset"],
[
EventShieldReason.MISMATCHED_SENDER,
"The sender of the event does not match the owner of the device that sent it.",
],
])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => {
mxEvent = await mkEncryptedMatrixEvent({
plainContent: { msgtype: "m.text", body: "msg1" },

View File

@@ -92,12 +92,16 @@ describe("StopGapWidgetDriver", () => {
"m.always_on_screen",
"town.robin.msc3846.turn_servers",
"org.matrix.msc2762.timeline:!1:example.org",
"org.matrix.msc2762.send.event:org.matrix.msc4075.call.notify",
"org.matrix.msc2762.send.event:org.matrix.msc4075.rtc.notification",
"org.matrix.msc2762.send.event:org.matrix.rageshake_request",
"org.matrix.msc2762.receive.event:org.matrix.rageshake_request",
"org.matrix.msc2762.send.event:m.reaction",
"org.matrix.msc2762.receive.event:m.reaction",
"org.matrix.msc2762.send.event:m.room.redaction",
"org.matrix.msc2762.receive.event:m.room.redaction",
"org.matrix.msc2762.send.event:io.element.call.reaction",
"org.matrix.msc2762.receive.event:io.element.call.reaction",
"org.matrix.msc2762.receive.state_event:m.room.create",
"org.matrix.msc2762.receive.state_event:m.room.name",
"org.matrix.msc2762.receive.state_event:m.room.member",

View File

@@ -524,25 +524,16 @@ describe("Rageshakes", () => {
consume: jest.fn(),
warn: jest.fn(),
} as unknown as Mocked<ConsoleLogger>;
mockConsoleLogger.flush.mockReturnValue("line 1\nline 2\n");
// @ts-ignore - mock the console logger
const prevLogger = global.mx_rage_logger;
global.mx_rage_logger = mockConsoleLogger;
// @ts-ignore
mockConsoleLogger.flush.mockReturnValue([
{
id: "instance-0",
line: "line 1",
},
{
id: "instance-1",
line: "line 2",
},
]);
const formData = await collectBugReport({ sendLogs: true });
expect(formData.get("compressed-log")).toBeDefined();
try {
const formData = await collectBugReport({ sendLogs: true });
expect(formData.get("compressed-log")).toBeDefined();
} finally {
global.mx_rage_logger = prevLogger;
}
});
describe("A-Element-R label", () => {

View File

@@ -4543,16 +4543,16 @@
classnames "^2.5.1"
vaul "^1.0.0"
"@vector-im/matrix-wysiwyg-wasm@link:../../../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.4-fb0001dea01010a1e3ffc7042596e2d001ce9389-integrity/node_modules/bindings/wysiwyg-wasm":
"@vector-im/matrix-wysiwyg-wasm@link:../../bindings/wysiwyg-wasm":
version "0.0.0"
uid ""
"@vector-im/matrix-wysiwyg@2.38.4":
version "2.38.4"
resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.4.tgz#fb0001dea01010a1e3ffc7042596e2d001ce9389"
integrity sha512-X6ky+1cf33SPdEVd6iTmOKfZZ2mDJv9cz3sHtDhuclS6uitK3QE8td/pmGqBj4ek2Ia4y0mnU61LfxvMry1SMA==
"@vector-im/matrix-wysiwyg@2.39.0":
version "2.39.0"
resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.39.0.tgz#a6238e517f23a2f3025d9c65445914771c63b163"
integrity sha512-OROXnzPcQWrCMoUpIrCKEC4FYU+9SsRomUgu+VbJwWtBDkCbfvLD4z6w/mgiADw3iTUpBPgmcWJoGxesFuB20Q==
dependencies:
"@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.4-fb0001dea01010a1e3ffc7042596e2d001ce9389-integrity/node_modules/bindings/wysiwyg-wasm"
"@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.39.0-a6238e517f23a2f3025d9c65445914771c63b163-integrity/node_modules/bindings/wysiwyg-wasm"
"@vitest/expect@3.2.4":
version "3.2.4"