From aecbdf8d5240d571c7d8c6c3bf6e28f4458c94e3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 15 Apr 2026 16:05:09 +0100 Subject: [PATCH 1/8] InviteDialog: factor out startDmOrSendInvites Factor out the logic of calling `startDm` or `inviteUsers` to a helper function. We're going to need to call this from a second location soon, so this is useful groundwork. --- .../components/views/dialogs/InviteDialog.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/views/dialogs/InviteDialog.tsx b/apps/web/src/components/views/dialogs/InviteDialog.tsx index be208b24a20..9817a12403d 100644 --- a/apps/web/src/components/views/dialogs/InviteDialog.tsx +++ b/apps/web/src/components/views/dialogs/InviteDialog.tsx @@ -444,6 +444,21 @@ export default class InviteDialog extends React.PureComponent { + if (this.props.kind === InviteKind.Dm) { + await this.startDm(); + } else if (this.props.kind === InviteKind.Invite) { + await this.inviteUsers(); + } else { + throw new Error("Unknown InviteKind: " + this.props.kind); + } + } + private transferCall = async (): Promise => { if (this.props.kind !== InviteKind.CallTransfer) return; if (this.state.currentTabId == TabId.UserDirectory) { @@ -1129,8 +1144,6 @@ export default class InviteDialog extends React.PureComponent Promise) | null = null; - const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); const cli = MatrixClientPeg.safeGet(); @@ -1167,7 +1180,6 @@ export default class InviteDialog extends React.PureComponent { + this.startDmOrSendInvites().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e)); + }; + return (

{helpText}

@@ -1223,7 +1238,7 @@ export default class InviteDialog extends React.PureComponent From 392bbc5fc9dd70f1de7c4259c6fbd50dc03ff42e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 16 Apr 2026 11:09:26 +0100 Subject: [PATCH 2/8] Add `UnknownIdentityUsersWarningDialog` --- apps/web/res/css/_common.pcss | 16 ++- apps/web/res/css/_components.pcss | 1 + .../_UnknownIdentityUsersWarningDialog.pcss | 45 +++++++ .../components/views/dialogs/InviteDialog.tsx | 85 +++++++++++- .../views/dialogs/invite/DMRoomTile.tsx | 6 +- .../UnknownIdentityUsersWarningDialog.tsx | 123 ++++++++++++++++++ apps/web/src/i18n/strings/en_EN.json | 8 ++ .../views/dialogs/InviteDialog-test.tsx | 6 + 8 files changed, 280 insertions(+), 10 deletions(-) create mode 100644 apps/web/res/css/views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss create mode 100644 apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx diff --git a/apps/web/res/css/_common.pcss b/apps/web/res/css/_common.pcss index f3a9fdcb944..d55cb076068 100644 --- a/apps/web/res/css/_common.pcss +++ b/apps/web/res/css/_common.pcss @@ -598,6 +598,7 @@ legend { .mx_AccessSecretStorageDialog button, .mx_InviteDialog_section button, .mx_InviteDialog_editor button, + .mx_UnknownIdentityUsersWarningDialog button, [class|="maplibregl"] ), .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton), @@ -625,7 +626,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ):last-child { margin-right: 0px; } @@ -641,7 +643,8 @@ legend { .mx_ShareDialog button, .mx_EncryptionUserSettingsTab button, .mx_InviteDialog_section button, - .mx_InviteDialog_editor button + .mx_InviteDialog_editor button, + .mx_UnknownIdentityUsersWarningDialog button ):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus, @@ -659,7 +662,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); @@ -678,7 +682,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); @@ -701,7 +706,8 @@ legend { .mx_ThemeChoicePanel_CustomTheme button, .mx_UnpinAllDialog button, .mx_ShareDialog button, - .mx_EncryptionUserSettingsTab button + .mx_EncryptionUserSettingsTab button, + .mx_UnknownIdentityUsersWarningDialog button ):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):disabled, diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index c208a71ef7f..6bee80f17d8 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -170,6 +170,7 @@ @import "./views/dialogs/_UserSettingsDialog.pcss"; @import "./views/dialogs/_VerifyEMailDialog.pcss"; @import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss"; +@import "./views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss"; @import "./views/dialogs/security/_AccessSecretStorageDialog.pcss"; @import "./views/dialogs/security/_CreateCrossSigningDialog.pcss"; @import "./views/dialogs/security/_CreateSecretStorageDialog.pcss"; diff --git a/apps/web/res/css/views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss b/apps/web/res/css/views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss new file mode 100644 index 00000000000..085d9dfa5d5 --- /dev/null +++ b/apps/web/res/css/views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss @@ -0,0 +1,45 @@ +/* + 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_UnknownIdentityUsersWarningDialog { + display: flex; + flex-direction: column; + height: 600px; /* Consistency with InviteDialog */ +} + +.mx_UnknownIdentityUsersWarningDialog_headerContainer { + /* Centre the PageHeader component horizontally */ + display: flex; + justify-content: center; + + /* Styling for the regular text inside the header */ + font: var(--cpd-font-body-lg-regular); + + /* Space before the list */ + padding-bottom: var(--cpd-space-6x); +} + +.mx_UnknownIdentityUsersWarningDialog_userList { + width: 100%; + overflow: auto; + + /* Fill available vertical space, but don't allow it to shrink to less than 60px (about the height of a single tile) */ + flex: 1 0 60px; + + /* Remove browser default ul padding/margin */ + padding: 0; + margin: 0; +} + +.mx_UnknownIdentityUsersWarningDialog_buttons { + display: flex; + gap: var(--cpd-space-4x); + + > button { + flex: 1; + } +} diff --git a/apps/web/src/components/views/dialogs/InviteDialog.tsx b/apps/web/src/components/views/dialogs/InviteDialog.tsx index 9817a12403d..f87378aa89c 100644 --- a/apps/web/src/components/views/dialogs/InviteDialog.tsx +++ b/apps/web/src/components/views/dialogs/InviteDialog.tsx @@ -61,6 +61,9 @@ import { type UserProfilesStore } from "../../../stores/UserProfilesStore"; import InviteProgressBody from "./InviteProgressBody.tsx"; import MultiInviter, { type CompletionStates as MultiInviterCompletionStates } from "../../../utils/MultiInviter.ts"; import { DMRoomTile } from "./invite/DMRoomTile.tsx"; +import { logErrorAndShowErrorDialog } from "../../../utils/ErrorUtils.tsx"; +import UnknownIdentityUsersWarningDialog from "./invite/UnknownIdentityUsersWarningDialog.tsx"; +import { AddressType, getAddressType } from "../../../UserAddress.ts"; interface Result { userId: string; @@ -161,6 +164,12 @@ interface IInviteDialogState { dialPadValue: string; currentTabId: TabId; + /** + * If we tried to invite some users whose identity we don't know, we will show a warning. + * This is the list of users. (If it is `null`, we are not showing that warning.) + */ + unknownIdentityUsers: Member[] | null; + /** * True if we are sending the invites. * @@ -230,7 +239,8 @@ export default class InviteDialog extends React.PureComponent { + this.setBusy(true); + + const targets = this.convertFilter(); + const unknownIdentityUsers: Member[] = []; + const cli = MatrixClientPeg.safeGet(); + const crypto = cli.getCrypto(); + if (crypto) { + for (const t of targets) { + const addressType = getAddressType(t.userId); + if ( + addressType !== AddressType.MatrixUserId || + !(await crypto.getUserVerificationStatus(t.userId)).known + ) { + unknownIdentityUsers.push(t); + } + } + } + + // If we have some users with unknown identities, show the warning page. + if (unknownIdentityUsers.length > 0) { + logger.debug( + "InviteDialog: Warning about users with unknown identities:", + unknownIdentityUsers.map((u) => u.userId), + ); + this.setState({ unknownIdentityUsers: unknownIdentityUsers, busy: false }); + } else { + // Otherwise, transition directly to sending the relevant invites. + await this.startDmOrSendInvites(); + } + } + /** * Render content of the "users" that is used for both invites and "start chat". */ @@ -1228,7 +1275,7 @@ export default class InviteDialog extends React.PureComponent { - this.startDmOrSendInvites().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e)); + this.onGoButtonPressed().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e)); }; return ( @@ -1256,6 +1303,40 @@ export default class InviteDialog extends React.PureComponent { + this.setState({ unknownIdentityUsers: null }); + this.startDmOrSendInvites().catch((e) => + logErrorAndShowErrorDialog("Error processing invites", e), + ); + }} + onRemove={() => { + // Remove the unknown identity users, then return to the previous screen + const newTargets: Member[] = []; + for (const target of this.state.targets) { + if (!this.state.unknownIdentityUsers?.find((m) => m.userId == target.userId)) { + newTargets.push(target); + } + } + this.setState({ + targets: newTargets, + unknownIdentityUsers: null, + }); + }} + screenName={this.screenName} + kind={this.props.kind} + users={this.state.unknownIdentityUsers} + /> + ); + } + let title; if (this.props.kind === InviteKind.Dm) { title = _t("space|add_existing_room_space|dm_heading"); diff --git a/apps/web/src/components/views/dialogs/invite/DMRoomTile.tsx b/apps/web/src/components/views/dialogs/invite/DMRoomTile.tsx index 954e792b17f..8998977cf22 100644 --- a/apps/web/src/components/views/dialogs/invite/DMRoomTile.tsx +++ b/apps/web/src/components/views/dialogs/invite/DMRoomTile.tsx @@ -19,8 +19,8 @@ import { Icon as EmailPillAvatarIcon } from "../../../../../res/img/icon-email-p interface IDMRoomTileProps { member: Member; lastActiveTs?: number; - onToggle(member: Member): void; - isSelected: boolean; + onToggle?(member: Member): void; + isSelected?: boolean; } /** A tile representing a single user in the "suggestions"/"recents" section of the invite dialog. */ @@ -30,7 +30,7 @@ export class DMRoomTile extends React.PureComponent { e.preventDefault(); e.stopPropagation(); - this.props.onToggle(this.props.member); + this.props.onToggle?.(this.props.member); }; public render(): React.ReactNode { diff --git a/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx b/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx new file mode 100644 index 00000000000..23c80892f9a --- /dev/null +++ b/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx @@ -0,0 +1,123 @@ +/* + 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, useCallback } from "react"; +import { CheckIcon, CloseIcon, UserAddSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Button, PageHeader } from "@vector-im/compound-web"; + +import { InviteKind } from "../InviteDialogTypes.ts"; +import { type Member } from "../../../../utils/direct-messages.ts"; +import BaseDialog from "../BaseDialog.tsx"; +import { type ScreenName } from "../../../../PosthogTrackers.ts"; +import { DMRoomTile } from "./DMRoomTile.tsx"; +import { _t } from "../../../../languageHandler.tsx"; + +interface Props { + /** Callback that will be called when the 'Continue' or 'Invite' button is clicked. */ + onContinue: () => void; + + /** Callback that will be called when the 'Cancel' button is clicked. Unused unless {@link kind} is {@link InviteKind.Dm}. */ + onCancel: () => void; + + /** Callback that will be called when the 'Remove' button is clicked. Unused unless {@link kind} is {@link InviteKind.Invite}. */ + onRemove: () => void; + + /** Optional Posthog ScreenName to supply during the lifetime of this dialog. */ + screenName: ScreenName | undefined; + + /** The type of invite dialog: whether we are starting a new DM, or inviting users to an existing room */ + kind: InviteKind.Dm | InviteKind.Invite; + + /** The users whose identities we don't know */ + users: Member[]; +} + +/** + * Part of the invite dialog: a screen that appears if there are any users whose cryptographic identity we don't know, + * to confirm that they are the right users. + * + * Figma: https://www.figma.com/design/chAcaQAluTuRg6BsG4Npc0/-3163--Inviting-Unknown-People?node-id=150-17719&t=ISAikbnj97LM4NwT-0 + */ +const UnknownIdentityUsersWarningDialog: React.FC = (props) => { + const userListItem = useCallback((u: Member) => , []); + + let title: string; + let headerText: string; + let buttons: JSX.Element; + + if (props.kind == InviteKind.Invite) { + title = _t("invite|confirm_unknown_users|invite_title"); + headerText = _t("invite|confirm_unknown_users|invite_subtitle"); + buttons = inviteButtons({ + onInvite: props.onContinue, + onRemove: props.onRemove, + }); + } else { + title = + props.users.length == 1 + ? _t("invite|confirm_unknown_users|start_chat_title_one_user") + : _t("invite|confirm_unknown_users|start_chat_title_multiple_users"); + + headerText = + props.users.length == 1 + ? _t("invite|confirm_unknown_users|start_chat_subtitle_one_user") + : _t("invite|confirm_unknown_users|start_chat_subtitle_multiple_users"); + + buttons = dmButtons({ + onCancel: props.onCancel, + onContinue: props.onContinue, + }); + } + + return ( + +
+ +

{headerText}

+
+
+ +
    + {props.users.map(userListItem)} +
+ +
{buttons}
+
+ ); +}; + +function dmButtons(props: { onContinue: () => void; onCancel: () => void }): JSX.Element { + return ( + <> + + + + ); +} + +function inviteButtons(props: { onInvite: () => void; onRemove: () => void }): JSX.Element { + return ( + <> + + + + ); +} + +export default UnknownIdentityUsersWarningDialog; diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json index ec66fdade77..44cf9399ee9 100644 --- a/apps/web/src/i18n/strings/en_EN.json +++ b/apps/web/src/i18n/strings/en_EN.json @@ -1368,6 +1368,14 @@ "impossible_dialog_title": "Integrations not allowed" }, "invite": { + "confirm_unknown_users": { + "invite_subtitle": "You currently don't have any chats with these contacts. Confirm inviting them to this room before continuing.", + "invite_title": "Invite new contacts to this room?", + "start_chat_subtitle_multiple_users": "You currently don't have any chats with these people. Confirm inviting them before continuing.", + "start_chat_subtitle_one_user": "You currently don't have any chats with this person. Confirm inviting them before continuing.", + "start_chat_title_multiple_users": "Start a chat with these new contacts?", + "start_chat_title_one_user": "Start a chat with this new contact?" + }, "email_caption": "Invite by email", "email_limit_one": "Invites by email can only be sent one at a time", "email_use_default_is": "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.", diff --git a/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx index 3f9eb6ac5ca..7d188fcca9e 100644 --- a/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx +++ b/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx @@ -13,6 +13,7 @@ import { RoomType, type MatrixClient, MatrixError, Room } from "matrix-js-sdk/sr import { KnownMembership } from "matrix-js-sdk/src/types"; import { sleep } from "matrix-js-sdk/src/utils"; import { mocked, type Mocked } from "jest-mock"; +import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import InviteDialog from "../../../../../src/components/views/dialogs/InviteDialog"; import { InviteKind } from "../../../../../src/components/views/dialogs/InviteDialogTypes"; @@ -103,6 +104,11 @@ describe("InviteDialog", () => { beforeEach(() => { mockClient = getMockClientWithEventEmitter({ + getCrypto: jest.fn().mockReturnValue({ + getUserVerificationStatus: jest + .fn() + .mockResolvedValue(new UserVerificationStatus(false, false, true, false)), + }), getDomain: jest.fn().mockReturnValue(serverDomain), getUserId: jest.fn().mockReturnValue(bobId), getSafeUserId: jest.fn().mockReturnValue(bobId), From 4dd5c71251d8a1a587151c21e4a706118373b7b2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 17 Apr 2026 13:32:46 +0100 Subject: [PATCH 3/8] Add unit tests --- .../views/dialogs/InviteDialog-test.tsx | 44 +++++++- ...UnknownIdentityUsersWarningDialog-test.tsx | 104 ++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 apps/web/test/unit-tests/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog-test.tsx diff --git a/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx index 7d188fcca9e..4f3a80736c0 100644 --- a/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx +++ b/apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx @@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { fireEvent, render, screen, findByText } from "jest-matrix-react"; +import { findByText, fireEvent, render, screen } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; -import { RoomType, type MatrixClient, MatrixError, Room } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient, MatrixError, Room, RoomType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { sleep } from "matrix-js-sdk/src/utils"; import { mocked, type Mocked } from "jest-mock"; @@ -455,4 +455,44 @@ describe("InviteDialog", () => { await flushPromises(); expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument(); }); + + describe("when inviting a user whose cryptographic identity we do not know", () => { + beforeEach(() => { + mocked(mockClient.getCrypto()!.getUserVerificationStatus).mockImplementation(async (u) => { + return new UserVerificationStatus(false, false, false, false); + }); + }); + + describe.each([InviteKind.Invite, InviteKind.Dm])("with invitekind '%s'", (kind) => { + const goButtonName = kind == InviteKind.Invite ? "Invite" : "Go"; + + beforeEach(() => { + render( + , + ); + }); + + it("should show a warning when inviting by user id", async () => { + await enterIntoSearchField(aliceId); + await userEvent.click(screen.getByRole("button", { name: goButtonName })); + await screen.findByText("Confirm inviting them", { exact: false }); + + expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledTimes(1); + expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).toHaveBeenCalledWith(aliceId); + }); + + it("should show a warning when inviting by email address", async () => { + await enterIntoSearchField("aaa@bbb"); + await userEvent.click(screen.getByRole("button", { name: goButtonName })); + await screen.findByText("Confirm inviting them", { exact: false }); + + // We shouldn't call getUserVerificationStatus on an email address + expect(mocked(mockClient.getCrypto()!.getUserVerificationStatus)).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/apps/web/test/unit-tests/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog-test.tsx new file mode 100644 index 00000000000..f3b7c0476fe --- /dev/null +++ b/apps/web/test/unit-tests/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog-test.tsx @@ -0,0 +1,104 @@ +/* + 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 ComponentProps } from "react"; +import { render, type RenderResult } from "jest-matrix-react"; +import { getAllByRole, getAllByText, getByText } from "@testing-library/dom"; + +import UnknownIdentityUsersWarningDialog from "../../../../../../src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx"; +import { InviteKind } from "../../../../../../src/components/views/dialogs/InviteDialogTypes.ts"; +import { DirectoryMember, ThreepidMember } from "../../../../../../src/utils/direct-messages.ts"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils"; + +describe("UnknownIdentityUsersWarningDialog", () => { + beforeEach(() => { + getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should show entries for each user", () => { + const result = renderComponent({ + users: [ + new DirectoryMember({ user_id: "@alice:example.com" }), + new DirectoryMember({ + user_id: "@bob:example.net", + display_name: "Bob", + avatar_url: "mxc://example.com/abc", + }), + new ThreepidMember("charlie@example.com"), + ], + }); + + const list = result.getByTestId("userlist"); + const entries = getAllByRole(list, "option"); + expect(entries).toHaveLength(3); + + // No displayname so mxid is displayed twice + expect(getAllByText(entries[0], "@alice:example.com")).toHaveLength(2); + + getByText(entries[1], "Bob"); + getByText(entries[2], "charlie@example.com"); + }); + + describe("in DM mode", () => { + const kind = InviteKind.Dm; + + it("shows a 'Continue' button", () => { + const onContinue = jest.fn(); + const result = renderComponent({ kind, onContinue }); + const continueButton = result.getByRole("button", { name: "Continue" }); + continueButton.click(); + expect(onContinue).toHaveBeenCalled(); + }); + + it("shows a 'Cancel' button", () => { + const onCancel = jest.fn(); + const result = renderComponent({ kind, onCancel }); + const cancelButton = result.getByRole("button", { name: "Cancel" }); + cancelButton.click(); + expect(onCancel).toHaveBeenCalled(); + }); + }); + + describe("in Invite mode", () => { + const kind = InviteKind.Invite; + + it("shows an 'Invite' button", () => { + const onContinue = jest.fn(); + const result = renderComponent({ kind, onContinue }); + const continueButton = result.getByRole("button", { name: "Invite" }); + continueButton.click(); + expect(onContinue).toHaveBeenCalled(); + }); + + it("shows a 'Remove' button", () => { + const onRemove = jest.fn(); + const result = renderComponent({ kind, onRemove }); + const removeButton = result.getByRole("button", { name: "Remove" }); + removeButton.click(); + expect(onRemove).toHaveBeenCalled(); + }); + }); +}); + +function renderComponent(props: Partial>): RenderResult { + const props1: ComponentProps = { + onContinue: () => {}, + onCancel: () => {}, + onRemove: () => {}, + screenName: undefined, + kind: InviteKind.Dm, + users: [], + ...props, + }; + return render(); +} From cd13015d4ef6307598d4750a6c385e1614f24ba6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 20 Apr 2026 18:52:44 +0100 Subject: [PATCH 4/8] Update playwright tests --- apps/web/playwright/e2e/crypto/crypto.spec.ts | 3 ++ .../e2e/crypto/history-sharing.spec.ts | 6 ++-- .../e2e/invite/invite-dialog.spec.ts | 27 ++++++++++++++++++ .../playwright/e2e/room/create-room.spec.ts | 3 ++ .../e2e/room/room-status-bar.spec.ts | 4 +++ .../encryption-user-tab/other-devices.spec.ts | 10 +++---- apps/web/playwright/pages/ElementAppPage.ts | 21 ++++++++++++-- .../confirm-chat-with-new-contact-linux.png | Bin 0 -> 29846 bytes .../confirm-invite-new-contact-linux.png | Bin 0 -> 30355 bytes 9 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-chat-with-new-contact-linux.png create mode 100644 apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-invite-new-contact-linux.png diff --git a/apps/web/playwright/e2e/crypto/crypto.spec.ts b/apps/web/playwright/e2e/crypto/crypto.spec.ts index d03fa1454ec..f76f674a408 100644 --- a/apps/web/playwright/e2e/crypto/crypto.spec.ts +++ b/apps/web/playwright/e2e/crypto/crypto.spec.ts @@ -27,6 +27,9 @@ const startDMWithBob = async (page: Page, bob: Bot) => { await page.getByRole("option", { name: bob.credentials.displayName }).click(); await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible(); await page.getByRole("button", { name: "Go" }).click(); + + await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); }; const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => { diff --git a/apps/web/playwright/e2e/crypto/history-sharing.spec.ts b/apps/web/playwright/e2e/crypto/history-sharing.spec.ts index 3974ba79c5f..29676036ba4 100644 --- a/apps/web/playwright/e2e/crypto/history-sharing.spec.ts +++ b/apps/web/playwright/e2e/crypto/history-sharing.spec.ts @@ -49,7 +49,7 @@ test.describe("History sharing", function () { await sendMessageInCurrentRoom(alicePage, "A message from Alice"); // Send the invite to Bob - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -105,7 +105,7 @@ test.describe("History sharing", function () { // Alice invites Bob, and Bob accepts const roomId = await aliceElementApp.getCurrentRoomIdFromUrl(); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); await bobPage.getByRole("option", { name: "TestRoom" }).click(); await bobPage.getByRole("button", { name: "Accept" }).click(); @@ -143,7 +143,7 @@ test.describe("History sharing", function () { await sendMessageInCurrentRoom(bobPage, "Message3: 'shared' visibility, but Bob thinks it is still 'joined'"); // Alice now invites Charlie - await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId, { confirmUnknownUser: true }); await charliePage.getByRole("option", { name: "TestRoom" }).click(); await charliePage.getByRole("button", { name: "Accept" }).click(); diff --git a/apps/web/playwright/e2e/invite/invite-dialog.spec.ts b/apps/web/playwright/e2e/invite/invite-dialog.spec.ts index 42588f37c7b..60fb6bf4afa 100644 --- a/apps/web/playwright/e2e/invite/invite-dialog.spec.ts +++ b/apps/web/playwright/e2e/invite/invite-dialog.spec.ts @@ -9,6 +9,15 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; +/** + * CSS which will hide the mxid in the user list of the "unknown users" confirmation dialog. This is useful because the + * MXID is not stable and the screenshot tests will otherwise fail. + * + * Ideally RichItem would give us a way to do this that doesn't involve gnarly CSS. + */ +const UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS = + '[data-testid="userlist"] li > span:nth-last-child(1) { display: none }'; + test.describe("Invite dialog", function () { test.use({ displayName: "Hanako", @@ -62,6 +71,15 @@ test.describe("Invite dialog", function () { // Invite the bot await other.getByRole("button", { name: "Invite" }).click(); + // Expect a confirmation dialog, screenshot, and dismiss + await expect( + page.locator(".mx_Dialog").getByRole("heading", { name: "Invite new contacts to this room?" }), + ).toBeVisible(); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-invite-new-contact.png", { + css: UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS, + }); + await page.locator(".mx_Dialog").getByRole("button", { name: "Invite" }).click(); + // Assert that the invite dialog disappears await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); @@ -104,6 +122,15 @@ test.describe("Invite dialog", function () { // Open a direct message UI await other.getByRole("button", { name: "Go" }).click(); + // Expect a confirmation dialog, screenshot, and dismiss + await expect( + page.locator(".mx_Dialog").getByRole("heading", { name: "Start a chat with this new contact?" }), + ).toBeVisible(); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-chat-with-new-contact.png", { + css: UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS, + }); + await page.locator(".mx_Dialog").getByRole("button", { name: "Continue" }).click(); + // Assert that the invite dialog disappears await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); diff --git a/apps/web/playwright/e2e/room/create-room.spec.ts b/apps/web/playwright/e2e/room/create-room.spec.ts index 554f972c7d5..fd699dcec87 100644 --- a/apps/web/playwright/e2e/room/create-room.spec.ts +++ b/apps/web/playwright/e2e/room/create-room.spec.ts @@ -57,6 +57,9 @@ test.describe("Create Room", () => { await page.getByRole("button", { name: "Go" }).click(); + await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + await expect(page.getByText("Encryption enabled")).toBeVisible(); await expect(page.getByText("Send your first message to")).toBeVisible(); diff --git a/apps/web/playwright/e2e/room/room-status-bar.spec.ts b/apps/web/playwright/e2e/room/room-status-bar.spec.ts index 78d5c49a300..dc11333753b 100644 --- a/apps/web/playwright/e2e/room/room-status-bar.spec.ts +++ b/apps/web/playwright/e2e/room/room-status-bar.spec.ts @@ -163,6 +163,10 @@ test.describe("Room Status Bar", () => { ).toBeVisible(); await other.getByRole("option", { name: "Alice" }).click(); await other.getByRole("button", { name: "Go" }).click(); + + await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible(); + await page.getByRole("button", { name: "Continue" }).click(); + // Send a message to invite the bots const composer = app.getComposerField(); await composer.fill("Hello"); diff --git a/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts b/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts index 6c20af2d9a5..a46e6eef368 100644 --- a/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts +++ b/apps/web/playwright/e2e/settings/encryption-user-tab/other-devices.spec.ts @@ -33,7 +33,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Create the room and invite bob await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -72,7 +72,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Create the room and invite bob await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -115,7 +115,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Create the room and invite bob await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite and dismisses the warnings. await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -149,7 +149,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Alice creates the room and invite Bob. await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite. await bobPage.getByRole("option", { name: "TestRoom" }).click(); @@ -214,7 +214,7 @@ test.describe("Other people's devices section in Encryption tab", () => { // Alice creates the room and invite Bob. await createRoom(alicePage, "TestRoom", true); - await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true }); // Bob accepts the invite. await bobPage.getByRole("option", { name: "TestRoom" }).click(); diff --git a/apps/web/playwright/pages/ElementAppPage.ts b/apps/web/playwright/pages/ElementAppPage.ts index e5a1aab31c1..1d285070107 100644 --- a/apps/web/playwright/pages/ElementAppPage.ts +++ b/apps/web/playwright/pages/ElementAppPage.ts @@ -233,15 +233,30 @@ export class ElementAppPage { * Open the room info panel, and use it to send an invite to the given user. * * @param userId - The user to invite to the room. + * @param options - Options object */ - public async inviteUserToCurrentRoom(userId: string): Promise { + public async inviteUserToCurrentRoom( + userId: string, + options?: { + /** If true, expect and acknowledge "Confirm inviting new users" page */ + confirmUnknownUser?: boolean; + }, + ): Promise { const rightPanel = await this.openRoomInfoPanel(); await rightPanel.getByRole("menuitem", { name: "Invite" }).click(); - const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input"); + const dialogLocator = this.page.getByRole("dialog"); + const input = dialogLocator.getByTestId("invite-dialog-input"); await input.fill(userId); await input.press("Enter"); - await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click(); + await dialogLocator.getByRole("button", { name: "Invite" }).click(); + + if (options?.confirmUnknownUser) { + await expect( + dialogLocator.getByRole("heading", { name: "Invite new contacts to this room?" }), + ).toBeVisible(); + await dialogLocator.getByRole("button", { name: "Invite" }).click(); + } } /** diff --git a/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-chat-with-new-contact-linux.png b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-chat-with-new-contact-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..4cdb24a1739a897065a2cf730955bb151324b579 GIT binary patch literal 29846 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 aE>M-dGVCDT`%VDPQ0ZtIYE-B_3IA`?zWA2_ literal 0 HcmV?d00001 diff --git a/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-invite-new-contact-linux.png b/apps/web/playwright/snapshots/invite/invite-dialog.spec.ts/confirm-invite-new-contact-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..cf961af67a6476737a1d0b605afcc463a01e0903 GIT binary patch literal 30355 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 g)|_!rH{ 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 literal 0 HcmV?d00001 From 1933c8d1ea016e75d803699206a55af806bd92d6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 22 Apr 2026 11:33:54 +0100 Subject: [PATCH 5/8] Convert if/else to switch statement --- .../UnknownIdentityUsersWarningDialog.tsx | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx b/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx index 23c80892f9a..7f617f17e63 100644 --- a/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx +++ b/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx @@ -49,28 +49,32 @@ const UnknownIdentityUsersWarningDialog: React.FC = (props) => { let headerText: string; let buttons: JSX.Element; - if (props.kind == InviteKind.Invite) { - title = _t("invite|confirm_unknown_users|invite_title"); - headerText = _t("invite|confirm_unknown_users|invite_subtitle"); - buttons = inviteButtons({ - onInvite: props.onContinue, - onRemove: props.onRemove, - }); - } else { - title = - props.users.length == 1 - ? _t("invite|confirm_unknown_users|start_chat_title_one_user") - : _t("invite|confirm_unknown_users|start_chat_title_multiple_users"); - - headerText = - props.users.length == 1 - ? _t("invite|confirm_unknown_users|start_chat_subtitle_one_user") - : _t("invite|confirm_unknown_users|start_chat_subtitle_multiple_users"); - - buttons = dmButtons({ - onCancel: props.onCancel, - onContinue: props.onContinue, - }); + switch (props.kind) { + case InviteKind.Invite: + title = _t("invite|confirm_unknown_users|invite_title"); + headerText = _t("invite|confirm_unknown_users|invite_subtitle"); + buttons = inviteButtons({ + onInvite: props.onContinue, + onRemove: props.onRemove, + }); + break; + + case InviteKind.Dm: + title = + props.users.length == 1 + ? _t("invite|confirm_unknown_users|start_chat_title_one_user") + : _t("invite|confirm_unknown_users|start_chat_title_multiple_users"); + + headerText = + props.users.length == 1 + ? _t("invite|confirm_unknown_users|start_chat_subtitle_one_user") + : _t("invite|confirm_unknown_users|start_chat_subtitle_multiple_users"); + + buttons = dmButtons({ + onCancel: props.onCancel, + onContinue: props.onContinue, + }); + break; } return ( From ba643ca61baa067a31f6c15ff784e75229c91770 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 22 Apr 2026 11:34:15 +0100 Subject: [PATCH 6/8] Convert helper functions to React components --- .../UnknownIdentityUsersWarningDialog.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx b/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx index 7f617f17e63..a2c4b4a01a2 100644 --- a/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx +++ b/apps/web/src/components/views/dialogs/invite/UnknownIdentityUsersWarningDialog.tsx @@ -53,10 +53,7 @@ const UnknownIdentityUsersWarningDialog: React.FC = (props) => { case InviteKind.Invite: title = _t("invite|confirm_unknown_users|invite_title"); headerText = _t("invite|confirm_unknown_users|invite_subtitle"); - buttons = inviteButtons({ - onInvite: props.onContinue, - onRemove: props.onRemove, - }); + buttons = ; break; case InviteKind.Dm: @@ -70,10 +67,7 @@ const UnknownIdentityUsersWarningDialog: React.FC = (props) => { ? _t("invite|confirm_unknown_users|start_chat_subtitle_one_user") : _t("invite|confirm_unknown_users|start_chat_subtitle_multiple_users"); - buttons = dmButtons({ - onCancel: props.onCancel, - onContinue: props.onContinue, - }); + buttons = ; break; } @@ -98,7 +92,7 @@ const UnknownIdentityUsersWarningDialog: React.FC = (props) => { ); }; -function dmButtons(props: { onContinue: () => void; onCancel: () => void }): JSX.Element { +const DmButtons: React.FC<{ onContinue: () => void; onCancel: () => void }> = (props) => { return ( <> ); -} +}; -function inviteButtons(props: { onInvite: () => void; onRemove: () => void }): JSX.Element { +const InviteButtons: React.FC<{ onInvite: () => void; onRemove: () => void }> = (props) => { return ( <> ); -} +}; export default UnknownIdentityUsersWarningDialog; From c8c88a319386d5efd61f81de31ec43c8dd373923 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 22 Apr 2026 11:43:12 +0100 Subject: [PATCH 7/8] Factor out "onRemove" callback --- .../components/views/dialogs/InviteDialog.tsx | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/views/dialogs/InviteDialog.tsx b/apps/web/src/components/views/dialogs/InviteDialog.tsx index f87378aa89c..dfbedd99a2d 100644 --- a/apps/web/src/components/views/dialogs/InviteDialog.tsx +++ b/apps/web/src/components/views/dialogs/InviteDialog.tsx @@ -1297,6 +1297,21 @@ export default class InviteDialog extends React.PureComponent { + // Remove the unknown identity users, then return to the previous screen + const newTargets: Member[] = []; + for (const target of this.state.targets) { + if (!this.state.unknownIdentityUsers?.find((m) => m.userId == target.userId)) { + newTargets.push(target); + } + } + this.setState({ + targets: newTargets, + unknownIdentityUsers: null, + }); + }; + /** * Render the complete dialog, given this is not a call transfer dialog. * @@ -1317,19 +1332,7 @@ export default class InviteDialog extends React.PureComponent { - // Remove the unknown identity users, then return to the previous screen - const newTargets: Member[] = []; - for (const target of this.state.targets) { - if (!this.state.unknownIdentityUsers?.find((m) => m.userId == target.userId)) { - newTargets.push(target); - } - } - this.setState({ - targets: newTargets, - unknownIdentityUsers: null, - }); - }} + onRemove={this.onRemoveUnknownIdentityUsersClicked} screenName={this.screenName} kind={this.props.kind} users={this.state.unknownIdentityUsers} From dcc0bed32842c135cdca7ab3597bf6a86495d5ed Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 22 Apr 2026 11:43:27 +0100 Subject: [PATCH 8/8] Add clarifying comment --- apps/web/src/components/views/dialogs/InviteDialog.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/components/views/dialogs/InviteDialog.tsx b/apps/web/src/components/views/dialogs/InviteDialog.tsx index dfbedd99a2d..a9271292415 100644 --- a/apps/web/src/components/views/dialogs/InviteDialog.tsx +++ b/apps/web/src/components/views/dialogs/InviteDialog.tsx @@ -167,6 +167,8 @@ interface IInviteDialogState { /** * If we tried to invite some users whose identity we don't know, we will show a warning. * This is the list of users. (If it is `null`, we are not showing that warning.) + * + * Will never be the empty list. */ unknownIdentityUsers: Member[] | null;