You've already forked matrix-react-sdk
mirror of
https://github.com/matrix-org/matrix-react-sdk.git
synced 2025-08-09 08:42:50 +03:00
Improve display of edited events
This commit is contained in:
@@ -14,19 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
import { IEncryptedEventInfo } from "matrix-js-sdk/src/crypto/api";
|
||||
|
||||
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { getRoomContext, mkEvent, mkMessage, stubClient } from "../../../test-utils";
|
||||
import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
|
||||
describe("EventTile", () => {
|
||||
@@ -34,6 +37,7 @@ describe("EventTile", () => {
|
||||
let mxEvent: MatrixEvent;
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
// let changeEvent: (event: MatrixEvent) => void;
|
||||
|
||||
function TestEventTile(props: Partial<EventTileProps>) {
|
||||
@@ -67,7 +71,7 @@ describe("EventTile", () => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
|
||||
room = new Room(ROOM_ID, client, client.getUserId(), {
|
||||
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
@@ -140,18 +144,194 @@ describe("EventTile", () => {
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Total, 3);
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Total, 3);
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Highlight, 1);
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Highlight, 1);
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Event verification", () => {
|
||||
// data for our stubbed getEventEncryptionInfo: a map from event id to result
|
||||
const eventToEncryptionInfoMap = new Map<string, IEncryptedEventInfo>();
|
||||
|
||||
const TRUSTED_DEVICE = DeviceInfo.fromStorage({}, "TRUSTED_DEVICE");
|
||||
const UNTRUSTED_DEVICE = DeviceInfo.fromStorage({}, "UNTRUSTED_DEVICE");
|
||||
|
||||
beforeEach(() => {
|
||||
eventToEncryptionInfoMap.clear();
|
||||
|
||||
// a mocked version of getEventEncryptionInfo which will pick its result from `eventToEncryptionInfoMap`
|
||||
client.getEventEncryptionInfo = (event) => eventToEncryptionInfoMap.get(event.getId()!)!;
|
||||
|
||||
// a mocked version of checkUserTrust which always says the user is trusted (we do our testing via
|
||||
// unverified devices).
|
||||
const trustedUserTrustLevel = new UserTrustLevel(true, true, true);
|
||||
client.checkUserTrust = (_userId) => trustedUserTrustLevel;
|
||||
|
||||
// a version of checkDeviceTrust which says that TRUSTED_DEVICE is trusted, and others are not.
|
||||
const trustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, true, false);
|
||||
const untrustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, false, false);
|
||||
client.checkDeviceTrust = (userId, deviceId) => {
|
||||
if (deviceId === TRUSTED_DEVICE.deviceId) {
|
||||
return trustedDeviceTrustLevel;
|
||||
} else {
|
||||
return untrustedDeviceTrustLevel;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it("shows a warning for an event from an unverified device", async () => {
|
||||
mxEvent = await mkEncryptedEvent({
|
||||
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||
plainType: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
room: room.roomId,
|
||||
});
|
||||
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||
authenticated: true,
|
||||
sender: UNTRUSTED_DEVICE,
|
||||
} as IEncryptedEventInfo);
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(eventTiles).toHaveLength(1);
|
||||
const eventTile = eventTiles[0];
|
||||
|
||||
expect(eventTile.classList).toContain("mx_EventTile_unverified");
|
||||
|
||||
// there should be a warning shield
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
|
||||
"mx_EventTile_e2eIcon_warning",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows no shield for a verified event", async () => {
|
||||
mxEvent = await mkEncryptedEvent({
|
||||
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||
plainType: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
room: room.roomId,
|
||||
});
|
||||
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||
authenticated: true,
|
||||
sender: TRUSTED_DEVICE,
|
||||
} as IEncryptedEventInfo);
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(eventTiles).toHaveLength(1);
|
||||
const eventTile = eventTiles[0];
|
||||
|
||||
expect(eventTile.classList).toContain("mx_EventTile_verified");
|
||||
|
||||
// there should be no warning
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should update the warning when the event is edited", async () => {
|
||||
// we start out with an event from the trusted device
|
||||
mxEvent = await mkEncryptedEvent({
|
||||
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||
plainType: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
room: room.roomId,
|
||||
});
|
||||
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||
authenticated: true,
|
||||
sender: TRUSTED_DEVICE,
|
||||
} as IEncryptedEventInfo);
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(eventTiles).toHaveLength(1);
|
||||
const eventTile = eventTiles[0];
|
||||
|
||||
expect(eventTile.classList).toContain("mx_EventTile_verified");
|
||||
|
||||
// there should be no warning
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
|
||||
|
||||
// then we replace the event with one from the unverified device
|
||||
const replacementEvent = await mkEncryptedEvent({
|
||||
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||
plainType: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
room: room.roomId,
|
||||
});
|
||||
eventToEncryptionInfoMap.set(replacementEvent.getId()!, {
|
||||
authenticated: true,
|
||||
sender: UNTRUSTED_DEVICE,
|
||||
} as IEncryptedEventInfo);
|
||||
|
||||
act(() => {
|
||||
mxEvent.makeReplaced(replacementEvent);
|
||||
});
|
||||
|
||||
// check it was updated
|
||||
expect(eventTile.classList).toContain("mx_EventTile_unverified");
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
|
||||
"mx_EventTile_e2eIcon_warning",
|
||||
);
|
||||
});
|
||||
|
||||
it("should update the warning when the event is replaced with an unencrypted one", async () => {
|
||||
jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true);
|
||||
|
||||
// we start out with an event from the trusted device
|
||||
mxEvent = await mkEncryptedEvent({
|
||||
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||
plainType: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
room: room.roomId,
|
||||
});
|
||||
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||
authenticated: true,
|
||||
sender: TRUSTED_DEVICE,
|
||||
} as IEncryptedEventInfo);
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(eventTiles).toHaveLength(1);
|
||||
const eventTile = eventTiles[0];
|
||||
|
||||
expect(eventTile.classList).toContain("mx_EventTile_verified");
|
||||
|
||||
// there should be no warning
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
|
||||
|
||||
// then we replace the event with an unencrypted one
|
||||
const replacementEvent = await mkMessage({
|
||||
msg: "msg2",
|
||||
user: "@alice:example.org",
|
||||
room: room.roomId,
|
||||
event: true,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
mxEvent.makeReplaced(replacementEvent);
|
||||
});
|
||||
|
||||
// check it was updated
|
||||
expect(eventTile.classList).not.toContain("mx_EventTile_verified");
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
|
||||
"mx_EventTile_e2eIcon_warning",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -38,6 +38,8 @@ import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
||||
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
import { CryptoBackend } from "matrix-js-sdk/src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "matrix-js-sdk/src/@types/crypto";
|
||||
|
||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
|
||||
@@ -315,6 +317,51 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent {
|
||||
return mxEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an m.room.encrypted event
|
||||
*
|
||||
* @param opts - Values for the event
|
||||
* @param opts.room - The ID of the room for the event
|
||||
* @param opts.user - The sender of the event
|
||||
* @param opts.plainType - The type the event will have, once it has been decrypted
|
||||
* @param opts.plainContent - The content the event will have, once it has been decrypted
|
||||
*/
|
||||
export async function mkEncryptedEvent(opts: {
|
||||
room: Room["roomId"];
|
||||
user: User["userId"];
|
||||
plainType: string;
|
||||
plainContent: IContent;
|
||||
}): Promise<MatrixEvent> {
|
||||
// we construct an event which has been decrypted by stubbing out CryptoBackend.decryptEvent and then
|
||||
// calling MatrixEvent.attemptDecryption.
|
||||
|
||||
const mxEvent = mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: opts.room,
|
||||
user: opts.user,
|
||||
event: true,
|
||||
content: {},
|
||||
});
|
||||
|
||||
const decryptionResult: IEventDecryptionResult = {
|
||||
claimedEd25519Key: "",
|
||||
clearEvent: {
|
||||
type: opts.plainType,
|
||||
content: opts.plainContent,
|
||||
},
|
||||
forwardingCurve25519KeyChain: [],
|
||||
senderCurve25519Key: "",
|
||||
untrusted: false,
|
||||
};
|
||||
|
||||
const mockCrypto = {
|
||||
decryptEvent: async (_ev): Promise<IEventDecryptionResult> => decryptionResult,
|
||||
} as CryptoBackend;
|
||||
|
||||
await mxEvent.attemptDecryption(mockCrypto);
|
||||
return mxEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an m.room.member event.
|
||||
* @param {Object} opts Values for the membership.
|
||||
|
Reference in New Issue
Block a user