Skip to content

Commit 97128e2

Browse files
committed
Add UnknownIdentityUsersWarningDialog
1 parent 577ba53 commit 97128e2

File tree

7 files changed

+249
-10
lines changed

7 files changed

+249
-10
lines changed

apps/web/res/css/_common.pcss

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ legend {
598598
.mx_AccessSecretStorageDialog button,
599599
.mx_InviteDialog_section button,
600600
.mx_InviteDialog_editor button,
601+
.mx_UnknownIdentityUsersWarningDialog button,
601602
[class|="maplibregl"]
602603
),
603604
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
@@ -625,7 +626,8 @@ legend {
625626
.mx_ThemeChoicePanel_CustomTheme button,
626627
.mx_UnpinAllDialog button,
627628
.mx_ShareDialog button,
628-
.mx_EncryptionUserSettingsTab button
629+
.mx_EncryptionUserSettingsTab button,
630+
.mx_UnknownIdentityUsersWarningDialog button
629631
):last-child {
630632
margin-right: 0px;
631633
}
@@ -641,7 +643,8 @@ legend {
641643
.mx_ShareDialog button,
642644
.mx_EncryptionUserSettingsTab button,
643645
.mx_InviteDialog_section button,
644-
.mx_InviteDialog_editor button
646+
.mx_InviteDialog_editor button,
647+
.mx_UnknownIdentityUsersWarningDialog button
645648
):focus,
646649
.mx_Dialog input[type="submit"]:focus,
647650
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus,
@@ -659,7 +662,8 @@ legend {
659662
.mx_ThemeChoicePanel_CustomTheme button,
660663
.mx_UnpinAllDialog button,
661664
.mx_ShareDialog button,
662-
.mx_EncryptionUserSettingsTab button
665+
.mx_EncryptionUserSettingsTab button,
666+
.mx_UnknownIdentityUsersWarningDialog button
663667
),
664668
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
665669
color: var(--cpd-color-text-on-solid-primary);
@@ -678,7 +682,8 @@ legend {
678682
.mx_ThemeChoicePanel_CustomTheme button,
679683
.mx_UnpinAllDialog button,
680684
.mx_ShareDialog button,
681-
.mx_EncryptionUserSettingsTab button
685+
.mx_EncryptionUserSettingsTab button,
686+
.mx_UnknownIdentityUsersWarningDialog button
682687
),
683688
.mx_Dialog_buttons input[type="submit"].danger {
684689
background-color: var(--cpd-color-bg-critical-primary);
@@ -701,7 +706,8 @@ legend {
701706
.mx_ThemeChoicePanel_CustomTheme button,
702707
.mx_UnpinAllDialog button,
703708
.mx_ShareDialog button,
704-
.mx_EncryptionUserSettingsTab button
709+
.mx_EncryptionUserSettingsTab button,
710+
.mx_UnknownIdentityUsersWarningDialog button
705711
):disabled,
706712
.mx_Dialog input[type="submit"]:disabled,
707713
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):disabled,

apps/web/res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
@import "./views/dialogs/_UserSettingsDialog.pcss";
170170
@import "./views/dialogs/_VerifyEMailDialog.pcss";
171171
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss";
172+
@import "./views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss";
172173
@import "./views/dialogs/security/_AccessSecretStorageDialog.pcss";
173174
@import "./views/dialogs/security/_CreateCrossSigningDialog.pcss";
174175
@import "./views/dialogs/security/_CreateSecretStorageDialog.pcss";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2026 Element Creations Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
.mx_UnknownIdentityUsersWarningDialog {
9+
display: flex;
10+
flex-direction: column;
11+
height: 600px; /* Consistency with InviteDialog */
12+
}
13+
14+
.mx_UnknownIdentityUsersWarningDialog_headerContainer {
15+
/* Centre the PageHeader component horizontally */
16+
display: flex;
17+
justify-content: center;
18+
19+
/* Styling for the regular text inside the header */
20+
font: var(--cpd-font-body-lg-regular);
21+
22+
/* Space before the list */
23+
padding-bottom: var(--cpd-space-6x);
24+
}
25+
26+
.mx_UnknownIdentityUsersWarningDialog_userList {
27+
width: 100%;
28+
overflow: auto;
29+
30+
/* Fill available vertical space, but don't allow it to shrink to less than 60px (about the height of a single tile) */
31+
flex: 1 0 60px;
32+
33+
/* Remove browser default ul padding/margin */
34+
padding: 0;
35+
margin: 0;
36+
}
37+
38+
.mx_UnknownIdentityUsersWarningDialog_buttons {
39+
display: flex;
40+
gap: var(--cpd-space-4x);
41+
42+
> button {
43+
flex: 1;
44+
}
45+
}

apps/web/src/components/views/dialogs/InviteDialog.tsx

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
6161
import InviteProgressBody from "./InviteProgressBody.tsx";
6262
import MultiInviter, { type CompletionStates as MultiInviterCompletionStates } from "../../../utils/MultiInviter.ts";
6363
import { DMRoomTile } from "./invite/DMRoomTile.tsx";
64+
import { logErrorAndShowErrorDialog } from "../../../utils/ErrorUtils.tsx";
65+
import UnknownIdentityUsersWarningDialog from "./invite/UnknownIdentityUsersWarningDialog.tsx";
6466

6567
interface Result {
6668
userId: string;
@@ -161,6 +163,12 @@ interface IInviteDialogState {
161163
dialPadValue: string;
162164
currentTabId: TabId;
163165

166+
/**
167+
* If we tried to invite some users whose identity we don't know, we will show a warning.
168+
* This is the list of users. (If it is `null`, we are not showing that warning.)
169+
*/
170+
unknownIdentityUsers: Member[] | null;
171+
164172
/**
165173
* True if we are sending the invites.
166174
*
@@ -230,7 +238,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
230238
dialPadValue: "",
231239
currentTabId: TabId.UserDirectory,
232240

233-
// These two flags are used for the 'Go' button to communicate what is going on.
241+
unknownIdentityUsers: null,
242+
234243
busy: false,
235244
};
236245
}
@@ -1138,6 +1147,39 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
11381147
);
11391148
}
11401149

1150+
/**
1151+
* Handle the user pressing the Go/Invite button in the "Start Chat" or "Invite users" view.
1152+
*
1153+
* We check if any of the users lack a known cryptographic identity, and show a warning if so.
1154+
*/
1155+
private async onGoButtonPressed(): Promise<void> {
1156+
this.setBusy(true);
1157+
1158+
const targets = this.convertFilter();
1159+
const unknownIdentityUsers: Member[] = [];
1160+
const cli = MatrixClientPeg.safeGet();
1161+
const crypto = cli.getCrypto();
1162+
if (crypto) {
1163+
for (const t of targets) {
1164+
if (!(await crypto.getUserVerificationStatus(t.userId)).known) {
1165+
unknownIdentityUsers.push(t);
1166+
}
1167+
}
1168+
}
1169+
1170+
// If we have some users with unknown identities, show the warning page.
1171+
if (unknownIdentityUsers.length > 0) {
1172+
logger.debug(
1173+
"InviteDialog: Warning about users with unknown identities:",
1174+
unknownIdentityUsers.map((u) => u.userId),
1175+
);
1176+
this.setState({ unknownIdentityUsers: unknownIdentityUsers, busy: false });
1177+
} else {
1178+
// Otherwise, transition directly to sending the relevant invites.
1179+
await this.startDmOrSendInvites();
1180+
}
1181+
}
1182+
11411183
/**
11421184
* Render content of the "users" that is used for both invites and "start chat".
11431185
*/
@@ -1228,7 +1270,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
12281270
}
12291271

12301272
const onGoButtonPressed = (): void => {
1231-
this.startDmOrSendInvites().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e));
1273+
this.onGoButtonPressed().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e));
12321274
};
12331275

12341276
return (
@@ -1256,6 +1298,40 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
12561298
* See also: {@link renderCallTransferDialog}.
12571299
*/
12581300
private renderRegularDialog(): React.ReactNode {
1301+
if (this.props.kind !== InviteKind.Dm && this.props.kind !== InviteKind.Invite) {
1302+
throw new Error("Unsupported InviteDialog kind: " + this.props.kind);
1303+
}
1304+
1305+
if (this.state.unknownIdentityUsers !== null) {
1306+
return (
1307+
<UnknownIdentityUsersWarningDialog
1308+
onCancel={this.props.onFinished}
1309+
onContinue={() => {
1310+
this.setState({ unknownIdentityUsers: null });
1311+
this.startDmOrSendInvites().catch((e) =>
1312+
logErrorAndShowErrorDialog("Error processing invites", e),
1313+
);
1314+
}}
1315+
onRemove={() => {
1316+
// Remove the unknown identity users, then return to the previous screen
1317+
const newTargets: Member[] = [];
1318+
for (const target of this.state.targets) {
1319+
if (!this.state.unknownIdentityUsers?.find((m) => m.userId == target.userId)) {
1320+
newTargets.push(target);
1321+
}
1322+
}
1323+
this.setState({
1324+
targets: newTargets,
1325+
unknownIdentityUsers: null,
1326+
});
1327+
}}
1328+
screenName={this.screenName}
1329+
kind={this.props.kind}
1330+
users={this.state.unknownIdentityUsers}
1331+
/>
1332+
);
1333+
}
1334+
12591335
let title;
12601336
if (this.props.kind === InviteKind.Dm) {
12611337
title = _t("space|add_existing_room_space|dm_heading");

apps/web/src/components/views/dialogs/invite/DMRoomTile.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { Icon as EmailPillAvatarIcon } from "../../../../../res/img/icon-email-p
1919
interface IDMRoomTileProps {
2020
member: Member;
2121
lastActiveTs?: number;
22-
onToggle(member: Member): void;
23-
isSelected: boolean;
22+
onToggle?(member: Member): void;
23+
isSelected?: boolean;
2424
}
2525

2626
/** 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<IDMRoomTileProps> {
3030
e.preventDefault();
3131
e.stopPropagation();
3232

33-
this.props.onToggle(this.props.member);
33+
this.props.onToggle?.(this.props.member);
3434
};
3535

3636
public render(): React.ReactNode {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2026 Element Creations Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { type JSX, useCallback } from "react";
9+
import { CheckIcon, CloseIcon, UserAddSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
10+
import { Button, PageHeader } from "@vector-im/compound-web";
11+
12+
import { InviteKind } from "../InviteDialogTypes.ts";
13+
import { type Member } from "../../../../utils/direct-messages.ts";
14+
import BaseDialog from "../BaseDialog.tsx";
15+
import { type ScreenName } from "../../../../PosthogTrackers.ts";
16+
import { DMRoomTile } from "./DMRoomTile.tsx";
17+
18+
interface Props {
19+
/** Callback that will be called when the 'Continue' button is clicked. */
20+
onContinue: () => void;
21+
22+
/** Callback that will be called when the 'close' or 'Cancel' button is clicked or 'Escape' is pressed. */
23+
onCancel: () => void;
24+
25+
/** Callback that will be called when the 'Remove' button is clicked. */
26+
onRemove: () => void;
27+
28+
/** Optional Posthog ScreenName to supply during the lifetime of this dialog. */
29+
screenName: ScreenName | undefined;
30+
31+
/** The type of invite dialog: whether we are starting a new DM, or inviting users to an existing room */
32+
kind: InviteKind.Dm | InviteKind.Invite;
33+
34+
/** The users whose identities we don't know */
35+
users: Member[];
36+
}
37+
38+
/**
39+
*
40+
* Figma: https://www.figma.com/design/chAcaQAluTuRg6BsG4Npc0/-3163--Inviting-Unknown-People?node-id=150-17719&t=ISAikbnj97LM4NwT-0
41+
*/
42+
export default function UnknownIdentityUsersWarningDialog(props: Props): JSX.Element {
43+
const userListItem = useCallback((u: Member) => <DMRoomTile member={u} />, []);
44+
45+
// TODO i18n, plurals, different wording for invites
46+
const title = "Start a chat with these new contacts?";
47+
const headerText = "You currently don't have any chats with these people. Confirm inviting them before continuing.";
48+
49+
const buttons =
50+
props.kind == InviteKind.Invite
51+
? inviteButtons({
52+
onInvite: props.onContinue,
53+
onRemove: props.onRemove,
54+
})
55+
: dmButtons({
56+
onCancel: props.onCancel,
57+
onContinue: props.onContinue,
58+
});
59+
60+
return (
61+
<BaseDialog
62+
onFinished={props.onCancel}
63+
className="mx_UnknownIdentityUsersWarningDialog"
64+
screenName={props.screenName}
65+
>
66+
<div className="mx_UnknownIdentityUsersWarningDialog_headerContainer">
67+
<PageHeader Icon={UserAddSolidIcon} heading={title}>
68+
<p>{headerText}</p>
69+
</PageHeader>
70+
</div>
71+
72+
<ul className="mx_UnknownIdentityUsersWarningDialog_userList" role="listbox">
73+
{props.users.map(userListItem)}
74+
</ul>
75+
76+
<div className="mx_UnknownIdentityUsersWarningDialog_buttons">{buttons}</div>
77+
</BaseDialog>
78+
);
79+
}
80+
81+
function dmButtons(props: { onContinue: () => void; onCancel: () => void }): JSX.Element {
82+
return (
83+
<>
84+
<Button size="lg" kind="secondary" onClick={props.onCancel}>
85+
Cancel
86+
</Button>
87+
<Button size="lg" kind="primary" onClick={props.onContinue}>
88+
Continue
89+
</Button>
90+
</>
91+
);
92+
}
93+
94+
function inviteButtons(props: { onInvite: () => void; onRemove: () => void }): JSX.Element {
95+
return (
96+
<>
97+
<Button size="lg" kind="secondary" onClick={props.onRemove} Icon={CloseIcon}>
98+
Remove
99+
</Button>
100+
<Button size="lg" kind="primary" onClick={props.onInvite} Icon={CheckIcon}>
101+
Invite
102+
</Button>
103+
</>
104+
);
105+
}

apps/web/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { RoomType, type MatrixClient, MatrixError, Room } from "matrix-js-sdk/sr
1313
import { KnownMembership } from "matrix-js-sdk/src/types";
1414
import { sleep } from "matrix-js-sdk/src/utils";
1515
import { mocked, type Mocked } from "jest-mock";
16+
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
1617

1718
import InviteDialog from "../../../../../src/components/views/dialogs/InviteDialog";
1819
import { InviteKind } from "../../../../../src/components/views/dialogs/InviteDialogTypes";
@@ -103,6 +104,11 @@ describe("InviteDialog", () => {
103104

104105
beforeEach(() => {
105106
mockClient = getMockClientWithEventEmitter({
107+
getCrypto: jest.fn().mockReturnValue({
108+
getUserVerificationStatus: jest
109+
.fn()
110+
.mockResolvedValue(new UserVerificationStatus(false, false, true, false)),
111+
}),
106112
getDomain: jest.fn().mockReturnValue(serverDomain),
107113
getUserId: jest.fn().mockReturnValue(bobId),
108114
getSafeUserId: jest.fn().mockReturnValue(bobId),

0 commit comments

Comments
 (0)