Refactor MKeyVerificationRequest to shared view MVVM (#33461)
* Refactor key verification request to shared view * Fix prettier * add tests to pass coverage
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019, 2020 , 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { EventTileBubble } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface Props {
|
||||
mxEvent: MatrixEvent;
|
||||
timestamp?: JSX.Element;
|
||||
}
|
||||
|
||||
interface MKeyVerificationRequestContent {
|
||||
body?: string;
|
||||
format?: string;
|
||||
formatted_body?: string;
|
||||
from_device: string;
|
||||
methods: Array<string>;
|
||||
msgtype: "m.key.verification.request";
|
||||
to: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event tile created when we receive an m.key.verification.request event.
|
||||
*
|
||||
* Displays a simple message saying that a verification was requested, either by
|
||||
* this user or someone else.
|
||||
*
|
||||
* EventTileFactory has logic meaning we only display this tile if the request
|
||||
* was sent to/from this user.
|
||||
*/
|
||||
const MKeyVerificationRequest: React.FC<Props> = ({ mxEvent, timestamp }) => {
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
if (!client) {
|
||||
throw new Error("Attempting to render verification request without a client context!");
|
||||
}
|
||||
|
||||
const myUserId = client.getSafeUserId();
|
||||
const content: MKeyVerificationRequestContent = mxEvent.getContent();
|
||||
const sender = mxEvent.getSender();
|
||||
const receiver = content.to;
|
||||
const roomId = mxEvent.getRoomId();
|
||||
|
||||
if (!sender) {
|
||||
throw new Error("Verification request did not include a sender!");
|
||||
}
|
||||
if (!roomId) {
|
||||
throw new Error("Verification request did not include a room ID!");
|
||||
}
|
||||
|
||||
let title: string;
|
||||
let subtitle: string;
|
||||
|
||||
const sentByMe = sender === myUserId;
|
||||
if (sentByMe) {
|
||||
title = _t("timeline|m.key.verification.request|you_started");
|
||||
subtitle = userLabelForEventRoom(client, receiver, roomId);
|
||||
} else {
|
||||
const name = getNameForEventRoom(client, sender, roomId);
|
||||
title = _t("timeline|m.key.verification.request|user_wants_to_verify", { name });
|
||||
subtitle = userLabelForEventRoom(client, sender, roomId);
|
||||
}
|
||||
|
||||
return (
|
||||
<EventTileBubble
|
||||
icon={<LockSolidIcon />}
|
||||
className="mx_EventTileBubble mx_cryptoEvent"
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
>
|
||||
{timestamp}
|
||||
</EventTileBubble>
|
||||
);
|
||||
};
|
||||
|
||||
export default MKeyVerificationRequest;
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import {
|
||||
EncryptionEventView,
|
||||
HiddenBodyView,
|
||||
MKeyVerificationRequestView,
|
||||
TextualEventView,
|
||||
useCreateAutoDisposedViewModel,
|
||||
} from "@element-hq/web-shared-components";
|
||||
@@ -37,7 +38,6 @@ import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore";
|
||||
import { ALL_RULE_TYPES } from "../mjolnir/BanList";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
||||
import MKeyVerificationRequest from "../components/views/messages/MKeyVerificationRequest";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
import MJitsiWidgetEvent from "../components/views/messages/MJitsiWidgetEvent";
|
||||
import { hasText } from "../TextForEvent";
|
||||
@@ -47,6 +47,7 @@ import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
||||
import { type IBodyProps } from "../components/views/messages/IBodyProps";
|
||||
import { ModuleApi } from "../modules/Api";
|
||||
import { EncryptionEventViewModel } from "../viewmodels/room/timeline/event-tile/EncryptionEventViewModel";
|
||||
import { MKeyVerificationRequestViewModel } from "../viewmodels/room/timeline/event-tile/MKeyVerificationRequestViewModel";
|
||||
import { TextualEventViewModel } from "../viewmodels/room/timeline/event-tile/TextualEventViewModel";
|
||||
import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
|
||||
import { ElementCallEventType } from "../call-types";
|
||||
@@ -95,7 +96,21 @@ function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||
const EncryptionEventFactory: Factory = (ref, props) => {
|
||||
return <EncryptionEventWrappedView ref={ref} {...props} />;
|
||||
};
|
||||
const VerificationReqFactory: Factory = (_ref, props) => <MKeyVerificationRequest {...props} />;
|
||||
function MKeyVerificationRequestWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||
const cli = useMatrixClientContext();
|
||||
if (!cli) {
|
||||
throw new Error("Attempting to render verification request without a client context!");
|
||||
}
|
||||
|
||||
const vm = useCreateAutoDisposedViewModel(() => new MKeyVerificationRequestViewModel({ mxEvent, cli }));
|
||||
|
||||
useEffect(() => {
|
||||
vm.setEvent(mxEvent);
|
||||
}, [mxEvent, vm]);
|
||||
|
||||
return <MKeyVerificationRequestView vm={vm} ref={ref} className="mx_EventTileBubble mx_cryptoEvent" />;
|
||||
}
|
||||
const VerificationReqFactory: Factory = (ref, props) => <MKeyVerificationRequestWrappedView ref={ref} {...props} />;
|
||||
function HiddenBodyWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||
const vm = useCreateAutoDisposedViewModel(() => new HiddenBodyViewModel({ mxEvent }));
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { type JSX } from "react";
|
||||
import { type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
BaseViewModel,
|
||||
type MKeyVerificationRequestViewModel as MKeyVerificationRequestViewModelInterface,
|
||||
type MKeyVerificationRequestViewSnapshot,
|
||||
} from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { getNameForEventRoom, userLabelForEventRoom } from "../../../../utils/KeyVerificationStateObserver";
|
||||
|
||||
interface MKeyVerificationRequestContent {
|
||||
body?: string;
|
||||
format?: string;
|
||||
formatted_body?: string;
|
||||
from_device: string;
|
||||
methods: string[];
|
||||
msgtype: "m.key.verification.request";
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface MKeyVerificationRequestViewModelProps {
|
||||
/**
|
||||
* Caller-provided client.
|
||||
*/
|
||||
cli: MatrixClient;
|
||||
/**
|
||||
* Verification request message event.
|
||||
*/
|
||||
mxEvent: MatrixEvent;
|
||||
/**
|
||||
* Optional timestamp element rendered in the tile footer slot.
|
||||
*/
|
||||
timestamp?: JSX.Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewModel for key verification request message events.
|
||||
*/
|
||||
export class MKeyVerificationRequestViewModel
|
||||
extends BaseViewModel<MKeyVerificationRequestViewSnapshot, MKeyVerificationRequestViewModelProps>
|
||||
implements MKeyVerificationRequestViewModelInterface
|
||||
{
|
||||
public constructor(props: MKeyVerificationRequestViewModelProps) {
|
||||
super(props, MKeyVerificationRequestViewModel.computeSnapshot(props));
|
||||
}
|
||||
|
||||
public setEvent(mxEvent: MatrixEvent): void {
|
||||
this.props = { ...this.props, mxEvent };
|
||||
this.updateSnapshotFromProps();
|
||||
}
|
||||
|
||||
public setTimestamp(timestamp: JSX.Element | undefined): void {
|
||||
this.props = { ...this.props, timestamp };
|
||||
this.snapshot.merge({ timestamp });
|
||||
}
|
||||
|
||||
private updateSnapshotFromProps(): void {
|
||||
this.snapshot.merge(MKeyVerificationRequestViewModel.computeSnapshot(this.props));
|
||||
}
|
||||
|
||||
private static computeSnapshot({
|
||||
cli,
|
||||
mxEvent,
|
||||
timestamp,
|
||||
}: MKeyVerificationRequestViewModelProps): MKeyVerificationRequestViewSnapshot {
|
||||
const myUserId = cli.getSafeUserId();
|
||||
const content = mxEvent.getContent<MKeyVerificationRequestContent>();
|
||||
const sender = mxEvent.getSender();
|
||||
const roomId = mxEvent.getRoomId();
|
||||
|
||||
if (!sender) {
|
||||
throw new Error("Verification request did not include a sender!");
|
||||
}
|
||||
if (!roomId) {
|
||||
throw new Error("Verification request did not include a room ID!");
|
||||
}
|
||||
|
||||
if (sender === myUserId) {
|
||||
return {
|
||||
title: _t("timeline|m.key.verification.request|you_started"),
|
||||
subtitle: userLabelForEventRoom(cli, content.to, roomId),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
const name = getNameForEventRoom(cli, sender, roomId);
|
||||
return {
|
||||
title: _t("timeline|m.key.verification.request|user_wants_to_verify", { name }),
|
||||
subtitle: userLabelForEventRoom(cli, sender, roomId),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type RenderResult, render } from "jest-matrix-react";
|
||||
import { type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MKeyVerificationRequest from "../../../../../src/components/views/messages/MKeyVerificationRequest";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { filterConsole } from "../../../../test-utils";
|
||||
|
||||
describe("MKeyVerificationRequest", () => {
|
||||
filterConsole(
|
||||
"The above error occurred in the <MKeyVerificationRequest> component",
|
||||
"Error: Attempting to render verification request without a client context!",
|
||||
"Error: Verification request did not include a sender!",
|
||||
"Error: Verification request did not include a room ID!",
|
||||
);
|
||||
|
||||
it("shows an error if not wrapped in a client context", () => {
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request" });
|
||||
const { container } = renderEventNoClient(event);
|
||||
expect(container).toHaveTextContent("Can't load this message");
|
||||
});
|
||||
|
||||
it("shows an error if the event has no sender", () => {
|
||||
const { client } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("Can't load this message");
|
||||
});
|
||||
|
||||
it("shows an error if the event has no room", () => {
|
||||
const { client } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request", sender: "@a:b.co" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("Can't load this message");
|
||||
});
|
||||
|
||||
it("displays a request from me", () => {
|
||||
const { client, myUserId } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request", sender: myUserId, room_id: "!x:y.co" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("You sent a verification request");
|
||||
});
|
||||
|
||||
it("displays a request from someone else to me", () => {
|
||||
const otherUserId = "@other:s.uk";
|
||||
const { client } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request", sender: otherUserId, room_id: "!x:y.co" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("other:s.uk wants to verify");
|
||||
});
|
||||
});
|
||||
|
||||
interface TestTileErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TestTileErrorBoundaryState {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class TestTileErrorBoundary extends React.Component<TestTileErrorBoundaryProps, TestTileErrorBoundaryState> {
|
||||
public constructor(props: TestTileErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromError(error: Error): Partial<TestTileErrorBoundaryState> {
|
||||
return { error };
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.state.error) {
|
||||
return "Can't load this message";
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function renderEventNoClient(event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TestTileErrorBoundary>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</TestTileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
function renderEvent(client: MatrixClient, event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TestTileErrorBoundary>
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</MatrixClientContext.Provider>
|
||||
,
|
||||
</TestTileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
function setup(): { client: MatrixClient; myUserId: string } {
|
||||
const myUserId = "@me:s.co";
|
||||
|
||||
const client = {
|
||||
getSafeUserId: jest.fn().mockReturnValue(myUserId),
|
||||
getRoom: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
return { client, myUserId };
|
||||
}
|
||||
@@ -6,8 +6,10 @@ 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 React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, type MatrixClient, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { EventType, type MatrixClient, MatrixEvent, MsgType, Room, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
JSONEventFactory,
|
||||
@@ -20,9 +22,25 @@ import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { createTestClient, mkEvent } from "../../test-utils";
|
||||
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
|
||||
import { ModuleApi } from "../../../src/modules/Api";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
|
||||
const roomId = "!room:example.com";
|
||||
|
||||
function makeVerificationRequestEvent({ sender, to }: { sender: string; to: string }): MatrixEvent {
|
||||
return mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: sender,
|
||||
room: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.KeyVerificationRequest,
|
||||
from_device: "DEVICE",
|
||||
methods: ["m.sas.v1"],
|
||||
to,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("pickFactory", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
@@ -206,14 +224,30 @@ describe("pickFactory", () => {
|
||||
it("should return a MessageEventFactory for a UTD event", () => {
|
||||
expect(pickFactory(utdEvent, client, false)).toBe(MessageEventFactory);
|
||||
});
|
||||
|
||||
it("should not render key verification requests which do not involve the current user", () => {
|
||||
const event = makeVerificationRequestEvent({
|
||||
sender: "@alice:example.com",
|
||||
to: "@bob:example.com",
|
||||
});
|
||||
|
||||
expect(pickFactory(event, client, false)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderTile", () => {
|
||||
let client: MatrixClient;
|
||||
let originalRenderMessage: typeof ModuleApi.instance.customComponents.renderMessage;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
originalRenderMessage = ModuleApi.instance.customComponents.renderMessage;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ModuleApi.instance.customComponents.renderMessage = originalRenderMessage;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("rendering a tile defers to the module API", () => {
|
||||
@@ -258,4 +292,75 @@ describe("renderTile", () => {
|
||||
mxEvent: messageEvent,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders an incoming key verification request with the wrapped shared-components view", () => {
|
||||
const sender = "@alice:example.com";
|
||||
const room = new Room(roomId, client, client.getSafeUserId());
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId: string) => {
|
||||
if (userId === sender) return { name: "Alice" } as RoomMember;
|
||||
return null;
|
||||
});
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
const verificationRequestEvent = makeVerificationRequestEvent({
|
||||
sender,
|
||||
to: client.getUserId()!,
|
||||
});
|
||||
|
||||
const tile = renderTile(
|
||||
TimelineRenderingType.Room,
|
||||
{ mxEvent: verificationRequestEvent, showHiddenEvents: false },
|
||||
client,
|
||||
);
|
||||
if (!tile) throw new Error("Expected a key verification request tile");
|
||||
|
||||
render(React.createElement(MatrixClientContext.Provider, { value: client }, tile));
|
||||
|
||||
expect(screen.getByText("Alice wants to verify")).toBeInTheDocument();
|
||||
expect(screen.getByText("Alice (@alice:example.com)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an outgoing key verification request with the wrapped shared-components view", () => {
|
||||
const recipient = "@alice:example.com";
|
||||
const room = new Room(roomId, client, client.getSafeUserId());
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId: string) => {
|
||||
if (userId === recipient) return { name: "Alice" } as RoomMember;
|
||||
return null;
|
||||
});
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
const verificationRequestEvent = makeVerificationRequestEvent({
|
||||
sender: client.getUserId()!,
|
||||
to: recipient,
|
||||
});
|
||||
|
||||
const tile = renderTile(
|
||||
TimelineRenderingType.Room,
|
||||
{ mxEvent: verificationRequestEvent, showHiddenEvents: false },
|
||||
client,
|
||||
);
|
||||
if (!tile) throw new Error("Expected a key verification request tile");
|
||||
|
||||
render(React.createElement(MatrixClientContext.Provider, { value: client }, tile));
|
||||
|
||||
expect(screen.getByText("You sent a verification request")).toBeInTheDocument();
|
||||
expect(screen.getByText("Alice (@alice:example.com)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("throws when a key verification request tile is rendered without a client context", () => {
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
const verificationRequestEvent = makeVerificationRequestEvent({
|
||||
sender: client.getUserId()!,
|
||||
to: "@alice:example.com",
|
||||
});
|
||||
|
||||
const tile = renderTile(
|
||||
TimelineRenderingType.Room,
|
||||
{ mxEvent: verificationRequestEvent, showHiddenEvents: false },
|
||||
client,
|
||||
);
|
||||
if (!tile) throw new Error("Expected a key verification request tile");
|
||||
|
||||
expect(() => render(tile)).toThrow("Attempting to render verification request without a client context!");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type MatrixClient, MatrixEvent, type Room, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MKeyVerificationRequestViewModel } from "../../../src/viewmodels/room/timeline/event-tile/MKeyVerificationRequestViewModel";
|
||||
|
||||
describe("MKeyVerificationRequestViewModel", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const myUserId = "@me:example.org";
|
||||
let cli: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = {
|
||||
getMember: jest.fn(),
|
||||
} as unknown as Room;
|
||||
cli = {
|
||||
getSafeUserId: jest.fn().mockReturnValue(myUserId),
|
||||
getRoom: jest.fn().mockReturnValue(room),
|
||||
} as unknown as MatrixClient;
|
||||
});
|
||||
|
||||
const createEvent = (sender?: string, to = myUserId, eventRoomId: string | undefined = roomId): MatrixEvent =>
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
room_id: eventRoomId,
|
||||
sender,
|
||||
content: {
|
||||
msgtype: "m.key.verification.request",
|
||||
from_device: "DEVICE",
|
||||
methods: ["m.sas.v1"],
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
it("throws if the event has no sender", () => {
|
||||
expect(() => new MKeyVerificationRequestViewModel({ cli, mxEvent: createEvent() })).toThrow(
|
||||
"Verification request did not include a sender!",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if the event has no room", () => {
|
||||
expect(
|
||||
() =>
|
||||
new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org", myUserId, ""),
|
||||
}),
|
||||
).toThrow("Verification request did not include a room ID!");
|
||||
});
|
||||
|
||||
it("renders a request sent by me", () => {
|
||||
jest.mocked(room.getMember).mockReturnValue({
|
||||
name: "Alice",
|
||||
} as RoomMember);
|
||||
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent(myUserId, "@alice:example.org"),
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "You sent a verification request",
|
||||
subtitle: "Alice (@alice:example.org)",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a request sent by someone else", () => {
|
||||
jest.mocked(room.getMember).mockReturnValue({
|
||||
name: "Alice",
|
||||
} as RoomMember);
|
||||
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "Alice wants to verify",
|
||||
subtitle: "Alice (@alice:example.org)",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to user ID when no room member is available", () => {
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "@alice:example.org wants to verify",
|
||||
subtitle: "@alice:example.org",
|
||||
});
|
||||
});
|
||||
|
||||
it("updates the snapshot when the event changes", () => {
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setEvent(createEvent(myUserId, "@bob:example.org"));
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "You sent a verification request",
|
||||
subtitle: "@bob:example.org",
|
||||
});
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("updates only timestamp when the timestamp changes", () => {
|
||||
const timestamp = <span>14:56</span>;
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setTimestamp(timestamp);
|
||||
|
||||
expect(vm.getSnapshot().timestamp).toBe(timestamp);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not emit when the event-derived snapshot is unchanged", () => {
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setEvent(createEvent("@alice:example.org"));
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit when the timestamp is unchanged", () => {
|
||||
const timestamp = <span>14:56</span>;
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
timestamp,
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setTimestamp(timestamp);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -40,6 +40,7 @@ export * from "./room/timeline/event-tile/actions/ActionBarView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/DisambiguatedProfile";
|
||||
export * from "./room/timeline/event-tile/EventTileView/EncryptionEventView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/EventTileBubble";
|
||||
export * from "./room/timeline/event-tile/EventTileView/MKeyVerificationRequestView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/PinnedMessageBadge";
|
||||
export * from "./room/timeline/event-tile/EventTileView/TextualEventView";
|
||||
export * from "./room/timeline/event-tile/body/AudioPlayerView";
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.content {
|
||||
svg {
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||
import { MKeyVerificationRequestView, type MKeyVerificationRequestViewSnapshot } from "./MKeyVerificationRequestView";
|
||||
|
||||
type MKeyVerificationRequestViewProps = MKeyVerificationRequestViewSnapshot & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const MKeyVerificationRequestViewWrapperImpl = ({
|
||||
className,
|
||||
...snapshot
|
||||
}: MKeyVerificationRequestViewProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(snapshot, {});
|
||||
|
||||
return <MKeyVerificationRequestView vm={vm} className={className} />;
|
||||
};
|
||||
|
||||
const MKeyVerificationRequestViewWrapper = withViewDocs(
|
||||
MKeyVerificationRequestViewWrapperImpl,
|
||||
MKeyVerificationRequestView,
|
||||
);
|
||||
|
||||
const meta = {
|
||||
title: "Timeline/Timeline Event/MKeyVerificationRequestView",
|
||||
component: MKeyVerificationRequestViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
title: "Alice wants to verify",
|
||||
subtitle: "Alice (@alice:example.org)",
|
||||
className: "",
|
||||
},
|
||||
} satisfies Meta<typeof MKeyVerificationRequestViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Received: Story = {};
|
||||
|
||||
export const SentByMe: Story = {
|
||||
args: {
|
||||
title: "You sent a verification request",
|
||||
subtitle: "Bob (@bob:example.org)",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTimestamp: Story = {
|
||||
args: {
|
||||
timestamp: <span>14:56</span>,
|
||||
},
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import { render, screen } from "@test-utils";
|
||||
import React from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { MockViewModel } from "../../../../../core/viewmodel";
|
||||
import { MKeyVerificationRequestView } from "./MKeyVerificationRequestView";
|
||||
import * as stories from "./MKeyVerificationRequestView.stories";
|
||||
|
||||
const { Received, SentByMe, WithTimestamp } = composeStories(stories);
|
||||
|
||||
describe("MKeyVerificationRequestView", () => {
|
||||
it("renders the Received story", () => {
|
||||
const { container } = render(<Received />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(screen.getByText("Alice wants to verify")).toBeInTheDocument();
|
||||
expect(screen.getByText("Alice (@alice:example.org)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the SentByMe story", () => {
|
||||
const { container } = render(<SentByMe />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(screen.getByText("You sent a verification request")).toBeInTheDocument();
|
||||
expect(screen.getByText("Bob (@bob:example.org)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a timestamp", () => {
|
||||
const { container } = render(<WithTimestamp />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(screen.getByText("14:56")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies a custom className to the root element", () => {
|
||||
const vm = new MockViewModel({
|
||||
title: "Alice wants to verify",
|
||||
subtitle: "Alice (@alice:example.org)",
|
||||
});
|
||||
const { container } = render(<MKeyVerificationRequestView vm={vm} className="custom-verification" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass("custom-verification");
|
||||
});
|
||||
|
||||
it("forwards the provided ref to the root element", () => {
|
||||
const ref = React.createRef<HTMLDivElement>() as React.RefObject<HTMLDivElement>;
|
||||
const vm = new MockViewModel({
|
||||
title: "Alice wants to verify",
|
||||
subtitle: "Alice (@alice:example.org)",
|
||||
});
|
||||
|
||||
render(<MKeyVerificationRequestView vm={vm} ref={ref} />);
|
||||
|
||||
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
||||
expect(ref.current).toHaveTextContent("Alice wants to verify");
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { type JSX } from "react";
|
||||
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||
import { EventTileBubble } from "../EventTileBubble";
|
||||
import styles from "./MKeyVerificationRequestView.module.css";
|
||||
|
||||
export interface MKeyVerificationRequestViewSnapshot {
|
||||
/**
|
||||
* Main title text for the verification request.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Label for the other user involved in the request.
|
||||
*/
|
||||
subtitle: string;
|
||||
/**
|
||||
* Optional timestamp element rendered in the EventTileBubble footer slot.
|
||||
*/
|
||||
timestamp?: JSX.Element;
|
||||
}
|
||||
|
||||
export type MKeyVerificationRequestViewModel = ViewModel<MKeyVerificationRequestViewSnapshot>;
|
||||
|
||||
export interface MKeyVerificationRequestViewProps {
|
||||
/**
|
||||
* ViewModel providing the current verification request snapshot.
|
||||
*/
|
||||
vm: MKeyVerificationRequestViewModel;
|
||||
/**
|
||||
* Optional CSS classes passed through to EventTileBubble.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Optional Ref forwarded to the root DOM element.
|
||||
*/
|
||||
ref?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a timeline bubble describing a key verification request message.
|
||||
*/
|
||||
export function MKeyVerificationRequestView({
|
||||
vm,
|
||||
className,
|
||||
ref,
|
||||
}: Readonly<MKeyVerificationRequestViewProps>): JSX.Element {
|
||||
const { title, subtitle, timestamp } = useViewModel(vm);
|
||||
|
||||
return (
|
||||
<EventTileBubble
|
||||
icon={<LockSolidIcon />}
|
||||
className={classNames(styles.content, className)}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
ref={ref}
|
||||
>
|
||||
{timestamp}
|
||||
</EventTileBubble>
|
||||
);
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MKeyVerificationRequestView > renders a timestamp 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="EventTileBubble-module_container MKeyVerificationRequestView-module_content"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="EventTileBubble-module_title"
|
||||
>
|
||||
Alice wants to verify
|
||||
</div>
|
||||
<div
|
||||
class="EventTileBubble-module_subtitle"
|
||||
>
|
||||
Alice (@alice:example.org)
|
||||
</div>
|
||||
<span>
|
||||
14:56
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`MKeyVerificationRequestView > renders the Received story 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="EventTileBubble-module_container MKeyVerificationRequestView-module_content"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="EventTileBubble-module_title"
|
||||
>
|
||||
Alice wants to verify
|
||||
</div>
|
||||
<div
|
||||
class="EventTileBubble-module_subtitle"
|
||||
>
|
||||
Alice (@alice:example.org)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`MKeyVerificationRequestView > renders the SentByMe story 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="EventTileBubble-module_container MKeyVerificationRequestView-module_content"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="EventTileBubble-module_title"
|
||||
>
|
||||
You sent a verification request
|
||||
</div>
|
||||
<div
|
||||
class="EventTileBubble-module_subtitle"
|
||||
>
|
||||
Bob (@bob:example.org)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
MKeyVerificationRequestView,
|
||||
type MKeyVerificationRequestViewProps,
|
||||
type MKeyVerificationRequestViewSnapshot,
|
||||
type MKeyVerificationRequestViewModel,
|
||||
} from "./MKeyVerificationRequestView";
|
||||
Reference in New Issue
Block a user