Compare commits

...

5 Commits

Author SHA1 Message Date
RiotRobot
87d40ab0e0 v1.11.112 2025-09-16 11:52:32 +00:00
Michael Telatynski
8e9a43d70c Merge commit from fork
* Validate room upgrade relationships properly

Ensures only correctly related rooms are left when leaving the latest version of a room.
Ensures the room list does not wrongly hide rooms which have not yet been upgraded.
Ensures the breadcrumbs store finds the correct latest version of a room for a given stored room.

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 12:43:49 +01:00
RiotRobot
9a11a80483 Upgrade dependency to matrix-js-sdk@38.2.0 2025-09-16 11:41:51 +00:00
ElementRobot
8d07e797c5 Hold Electron toasts until after the client starts (#30768) (#30769)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 12:05:08 +01:00
ElementRobot
2e8e6e92cc Add mechanism for Electron to render toasts (#30765) (#30767)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 09:41:11 +01:00
14 changed files with 141 additions and 38 deletions

View File

@@ -1,3 +1,8 @@
Changes in [1.11.112](https://github.com/element-hq/element-web/releases/tag/v1.11.112) (2025-09-16)
====================================================================================================
Fix [CVE-2025-59161](https://www.cve.org/CVERecord?id=CVE-2025-59161) / [GHSA-m6c8-98f4-75rr](https://github.com/element-hq/element-web/security/advisories/GHSA-m6c8-98f4-75rr)
Changes in [1.11.111](https://github.com/element-hq/element-web/releases/tag/v1.11.111) (2025-09-10)
====================================================================================================
## ✨ Features

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.111",
"version": "1.11.112",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -134,7 +134,7 @@
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "38.1.0",
"matrix-js-sdk": "38.2.0",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",

View File

@@ -68,7 +68,8 @@ type ElectronChannel =
| "openDesktopCapturerSourcePicker"
| "userAccessToken"
| "homeserverUrl"
| "serverSupportedVersions";
| "serverSupportedVersions"
| "showToast";
declare global {
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},

View File

@@ -143,7 +143,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, false, msc3946ProcessDynamicPredecessor);
const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, true, msc3946ProcessDynamicPredecessor);
if (history && history.length > 1) {
room = history[history.length - 1]; // Last room is most recent in history

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { logger } from "matrix-js-sdk/src/logger";
import { EventType, KnownMembership } from "matrix-js-sdk/src/matrix";
import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix";
import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix";
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
import type { ActionPayload } from "../../dispatcher/payloads";
import type { FilterKey } from "./skip-list/filters";
@@ -250,12 +250,15 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
// the dead room in the list.
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
const roomState: RoomState = payload.room.currentState;
const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor);
if (predecessor) {
const prevRoom = this.matrixClient?.getRoom(predecessor.roomId);
if (prevRoom) this.roomSkipList.removeRoom(prevRoom);
else logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`);
const room: Room = payload.room;
const roomUpgradeHistory = room.client.getRoomUpgradeHistory(
room.roomId,
true,
this.msc3946ProcessDynamicPredecessor,
);
const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room));
for (const predecessor of predecessors) {
this.roomSkipList.removeRoom(predecessor);
}
}

View File

@@ -6,7 +6,7 @@ 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 { type MatrixClient, type Room, type RoomState, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix";
import { type MatrixClient, type Room, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import SettingsStore from "../../settings/SettingsStore";
@@ -308,24 +308,22 @@ export class RoomListStoreClass extends AsyncStoreWithClient<EmptyObject> implem
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
const newMembership = getEffectiveMembershipTag(membershipPayload.room, membershipPayload.membership);
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
// the dead room in the list.
const roomState: RoomState = membershipPayload.room.currentState;
const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor);
if (predecessor) {
const prevRoom = this.matrixClient?.getRoom(predecessor.roomId);
if (prevRoom) {
const isSticky = this.algorithm.stickyRoom === prevRoom;
if (isSticky) {
this.algorithm.setStickyRoom(null);
}
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
// avoid redundant updates.
this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
} else {
logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`);
// If we're joining an upgraded room, we'll want to make sure we don't proliferate the dead room in the list.
const room: Room = membershipPayload.room;
const roomUpgradeHistory = room.client.getRoomUpgradeHistory(
room.roomId,
true,
this.msc3946ProcessDynamicPredecessor,
);
const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room));
for (const predecessor of predecessors) {
const isSticky = this.algorithm.stickyRoom === predecessor;
if (isSticky) {
this.algorithm.setStickyRoom(null);
}
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
// avoid redundant updates.
this.algorithm.handleRoomUpdate(predecessor, RoomUpdateCause.RoomRemoved);
}
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);

View File

@@ -40,7 +40,7 @@ export async function leaveRoomBehaviour(
let leavingAllVersions = true;
const history = matrixClient.getRoomUpgradeHistory(
roomId,
false,
true,
SettingsStore.getValue("feature_dynamic_room_predecessors"),
);
if (history && history.length > 0) {

View File

@@ -18,6 +18,7 @@ import {
} from "matrix-js-sdk/src/matrix";
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { uniqueId } from "lodash";
import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform";
import type BaseEventIndexManager from "../../indexing/BaseEventIndexManager";
@@ -43,6 +44,7 @@ import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager";
import { _t } from "../../languageHandler";
import { BadgeOverlayRenderer } from "../../favicon";
import GenericToast from "../../components/views/toasts/GenericToast.tsx";
interface SquirrelUpdate {
releaseNotes: string;
@@ -95,6 +97,7 @@ export default class ElectronPlatform extends BasePlatform {
private badgeOverlayRenderer?: BadgeOverlayRenderer;
private config!: IConfigOptions;
private supportedSettings?: Record<string, boolean>;
private clientStartedPromiseWithResolvers = Promise.withResolvers<void>();
public constructor() {
super();
@@ -182,6 +185,27 @@ export default class ElectronPlatform extends BasePlatform {
await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" });
});
this.electron.on("showToast", async (ev, { title, description, priority = 40 }) => {
await this.clientStartedPromiseWithResolvers.promise;
const key = uniqueId("electron_showToast_");
const onPrimaryClick = (): void => {
ToastStore.sharedInstance().dismissToast(key);
};
ToastStore.sharedInstance().addOrReplaceToast({
key,
title,
props: {
description,
primaryLabel: _t("action|dismiss"),
onPrimaryClick,
},
component: GenericToast,
priority,
});
});
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
this.initialised = this.initialise();
@@ -193,6 +217,10 @@ export default class ElectronPlatform extends BasePlatform {
if (["call_state"].includes(payload.action)) {
this.electron.send("app_onAction", payload);
}
if (payload.action === "client_started") {
this.clientStartedPromiseWithResolvers.resolve();
}
}
private async initialise(): Promise<void> {

View File

@@ -112,7 +112,7 @@ describe("BreadcrumbsStore", () => {
await dispatchJoinRoom(room.roomId);
// We pass the value of the dynamic predecessor setting through
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false);
});
});
@@ -134,7 +134,7 @@ describe("BreadcrumbsStore", () => {
await dispatchJoinRoom(room.roomId);
// We pass the value of the dynamic predecessor setting through
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true);
});
});

View File

@@ -28,6 +28,7 @@ import SettingsStore from "../../../../src/settings/SettingsStore";
import * as utils from "../../../../src/utils/notifications";
import * as roomMute from "../../../../src/stores/room-list/utils/roomMute";
import { Action } from "../../../../src/dispatcher/actions";
import { mocked } from "jest-mock";
describe("RoomListStoreV3", () => {
async function getRoomListStore() {
@@ -197,6 +198,9 @@ describe("RoomListStoreV3", () => {
const oldRoom = rooms[32];
// Create a new room with a predecessor event that points to oldRoom
const newRoom = new Room("!foonew:matrix.org", client, client.getSafeUserId(), {});
mocked(client.getRoomUpgradeHistory).mockImplementation((roomId) =>
roomId === newRoom.roomId ? [oldRoom, newRoom] : [],
);
const createWithPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: "@foo:foo.org",
@@ -227,6 +231,41 @@ describe("RoomListStoreV3", () => {
expect(roomIds).toContain(newRoom.roomId);
});
it("should not remove predecessor room based on non-reciprocated relationship", async () => {
const { store, rooms, client, dispatcher } = await getRoomListStore();
const oldRoom = rooms[32];
// Create a new room with a predecessor event that points to oldRoom, but oldRoom does not point back
const newRoom = new Room("!nefarious:matrix.org", client, client.getSafeUserId(), {});
const createWithPredecessor = new MatrixEvent({
type: EventType.RoomCreate,
sender: "@foo:foo.org",
room_id: newRoom.roomId,
content: {
predecessor: { room_id: oldRoom.roomId, event_id: "tombstone_event_id" },
},
event_id: "$create",
state_key: "",
});
upsertRoomStateEvents(newRoom, [createWithPredecessor]);
const fn = jest.fn();
store.on(LISTS_UPDATE_EVENT, fn);
dispatcher.dispatch(
{
action: "MatrixActions.Room.myMembership",
oldMembership: KnownMembership.Invite,
membership: KnownMembership.Join,
room: newRoom,
},
true,
);
expect(fn).toHaveBeenCalled();
const roomIds = store.getSortedRooms().map((r) => r.roomId);
expect(roomIds).toContain(oldRoom.roomId);
expect(roomIds).toContain(newRoom.roomId);
});
it("Rooms are re-inserted on m.direct event", async () => {
const { store, dispatcher, client } = await getRoomListStore();

View File

@@ -115,6 +115,10 @@ describe("RoomListStore", () => {
// Given a store we can spy on
const { store, handleRoomUpdate } = createStore();
mocked(client.getRoomUpgradeHistory).mockImplementation((roomId) =>
roomId === roomWithCreatePredecessor.roomId ? [oldRoom, roomWithCreatePredecessor] : [],
);
// When we tell it we joined a new room that has an old room as
// predecessor in the create event
const payload = {

View File

@@ -129,7 +129,7 @@ describe("leaveRoomBehaviour", () => {
it("Passes through the dynamic predecessor setting", async () => {
await leaveRoomBehaviour(client, room.roomId);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false);
});
});
@@ -143,7 +143,7 @@ describe("leaveRoomBehaviour", () => {
it("Passes through the dynamic predecessor setting", async () => {
await leaveRoomBehaviour(client, room.roomId);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true);
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true);
});
});
});

View File

@@ -21,6 +21,7 @@ import DesktopCapturerSourcePicker from "../../../../src/components/views/elemen
import ElectronPlatform from "../../../../src/vector/platform/ElectronPlatform";
import { setupLanguageMock } from "../../../setup/setupLanguage";
import { stubClient } from "../../../test-utils";
import ToastStore from "../../../../src/stores/ToastStore.ts";
jest.mock("../../../../src/rageshake/rageshake", () => ({
flush: jest.fn(),
@@ -127,6 +128,30 @@ describe("ElectronPlatform", () => {
expect(plat.ipc.call).toHaveBeenCalledWith("callDisplayMediaCallback", "source");
});
it("should show a toast when showToast is fired", async () => {
new ElectronPlatform();
dispatcher.dispatch(
{
action: "client_started",
},
true,
);
const spy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast");
const [event, handler] = getElectronEventHandlerCall("showToast")!;
handler({} as any, { title: "title", description: "description" });
expect(event).toBeTruthy();
await waitFor(() =>
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
title: "title",
props: expect.objectContaining({ description: "description" }),
}),
),
);
});
describe("updates", () => {
it("dispatches on check updates action", () => {
new ElectronPlatform();

View File

@@ -11008,10 +11008,10 @@ 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@38.1.0:
version "38.1.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-38.1.0.tgz#76e569e0daa3d78e545d77d85de73f2f8abae4c9"
integrity sha512-yej0LgzPwBkQGI5XcBX8IXyXj/CHEgp7xBIj099wQWDLhK8iN2vDRs/yxZ+E4t38FDhavxqugU0TYDNoCSb1+w==
matrix-js-sdk@38.2.0:
version "38.2.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-38.2.0.tgz#bcda87ef767897e52afa31465a84b5bdacc80163"
integrity sha512-R3jzK8rDGi/3OXOax8jFFyxblCG9KTT5yuXAbvnZCGcpTm8lZ4mHQAn5UydVD8qiyUMNMpaaMd6/k7N+5I/yaQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^15.1.0"