Skip to content

Commit 19001fb

Browse files
committed
Add UnknownIdentityUsersWarningDialog
1 parent aecbdf8 commit 19001fb

File tree

7 files changed

+254
-10
lines changed

7 files changed

+254
-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
@@ -170,6 +170,7 @@
170170
@import "./views/dialogs/_UserSettingsDialog.pcss";
171171
@import "./views/dialogs/_VerifyEMailDialog.pcss";
172172
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss";
173+
@import "./views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss";
173174
@import "./views/dialogs/security/_AccessSecretStorageDialog.pcss";
174175
@import "./views/dialogs/security/_CreateCrossSigningDialog.pcss";
175176
@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: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ 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";
66+
import { AddressType, getAddressType } from "../../../UserAddress.ts";
6467

6568
interface Result {
6669
userId: string;
@@ -161,6 +164,12 @@ interface IInviteDialogState {
161164
dialPadValue: string;
162165
currentTabId: TabId;
163166

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

233-
// These two flags are used for the 'Go' button to communicate what is going on.
242+
unknownIdentityUsers: null,
243+
234244
busy: false,
235245
};
236246
}
@@ -1138,6 +1148,43 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
11381148
);
11391149
}
11401150

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

12301277
const onGoButtonPressed = (): void => {
1231-
this.startDmOrSendInvites().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e));
1278+
this.onGoButtonPressed().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e));
12321279
};
12331280

12341281
return (
@@ -1256,6 +1303,40 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
12561303
* See also: {@link renderCallTransferDialog}.
12571304
*/
12581305
private renderRegularDialog(): React.ReactNode {
1306+
if (this.props.kind !== InviteKind.Dm && this.props.kind !== InviteKind.Invite) {
1307+
throw new Error("Unsupported InviteDialog kind: " + this.props.kind);
1308+
}
1309+
1310+
if (this.state.unknownIdentityUsers !== null) {
1311+
return (
1312+
<UnknownIdentityUsersWarningDialog
1313+
onCancel={this.props.onFinished}
1314+
onContinue={() => {
1315+
this.setState({ unknownIdentityUsers: null });
1316+
this.startDmOrSendInvites().catch((e) =>
1317+
logErrorAndShowErrorDialog("Error processing invites", e),
1318+
);
1319+
}}
1320+
onRemove={() => {
1321+
// Remove the unknown identity users, then return to the previous screen
1322+
const newTargets: Member[] = [];
1323+
for (const target of this.state.targets) {
1324+
if (!this.state.unknownIdentityUsers?.find((m) => m.userId == target.userId)) {
1325+
newTargets.push(target);
1326+
}
1327+
}
1328+
this.setState({
1329+
targets: newTargets,
1330+
unknownIdentityUsers: null,
1331+
});
1332+
}}
1333+
screenName={this.screenName}
1334+
kind={this.props.kind}
1335+
users={this.state.unknownIdentityUsers}
1336+
/>
1337+
);
1338+
}
1339+
12591340
let title;
12601341
if (this.props.kind === InviteKind.Dm) {
12611342
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} key={u.userId} />, []);
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)