mirror of
https://github.com/vector-im/element-web.git
synced 2026-04-18 12:01:57 +02:00
Add UnknownIdentityUsersWarningDialog
This commit is contained in:
parent
aecbdf8d52
commit
19001fb04c
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<Props, IInviteDial
|
||||
dialPadValue: "",
|
||||
currentTabId: TabId.UserDirectory,
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
unknownIdentityUsers: null,
|
||||
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
@ -1138,6 +1148,43 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the user pressing the Go/Invite button in the "Start Chat" or "Invite users" view.
|
||||
*
|
||||
* We check if any of the users lack a known cryptographic identity, and show a warning if so.
|
||||
*/
|
||||
private async onGoButtonPressed(): Promise<void> {
|
||||
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<Props, IInviteDial
|
||||
}
|
||||
|
||||
const onGoButtonPressed = (): void => {
|
||||
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<Props, IInviteDial
|
||||
* See also: {@link renderCallTransferDialog}.
|
||||
*/
|
||||
private renderRegularDialog(): React.ReactNode {
|
||||
if (this.props.kind !== InviteKind.Dm && this.props.kind !== InviteKind.Invite) {
|
||||
throw new Error("Unsupported InviteDialog kind: " + this.props.kind);
|
||||
}
|
||||
|
||||
if (this.state.unknownIdentityUsers !== null) {
|
||||
return (
|
||||
<UnknownIdentityUsersWarningDialog
|
||||
onCancel={this.props.onFinished}
|
||||
onContinue={() => {
|
||||
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");
|
||||
|
||||
@ -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<IDMRoomTileProps> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onToggle(this.props.member);
|
||||
this.props.onToggle?.(this.props.member);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
/** Callback that will be called when the 'Continue' button is clicked. */
|
||||
onContinue: () => void;
|
||||
|
||||
/** Callback that will be called when the 'close' or 'Cancel' button is clicked or 'Escape' is pressed. */
|
||||
onCancel: () => void;
|
||||
|
||||
/** Callback that will be called when the 'Remove' button is clicked. */
|
||||
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[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Figma: https://www.figma.com/design/chAcaQAluTuRg6BsG4Npc0/-3163--Inviting-Unknown-People?node-id=150-17719&t=ISAikbnj97LM4NwT-0
|
||||
*/
|
||||
export default function UnknownIdentityUsersWarningDialog(props: Props): JSX.Element {
|
||||
const userListItem = useCallback((u: Member) => <DMRoomTile member={u} key={u.userId} />, []);
|
||||
|
||||
// TODO i18n, plurals, different wording for invites
|
||||
const title = "Start a chat with these new contacts?";
|
||||
const headerText = "You currently don't have any chats with these people. Confirm inviting them before continuing.";
|
||||
|
||||
const buttons =
|
||||
props.kind == InviteKind.Invite
|
||||
? inviteButtons({
|
||||
onInvite: props.onContinue,
|
||||
onRemove: props.onRemove,
|
||||
})
|
||||
: dmButtons({
|
||||
onCancel: props.onCancel,
|
||||
onContinue: props.onContinue,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
onFinished={props.onCancel}
|
||||
className="mx_UnknownIdentityUsersWarningDialog"
|
||||
screenName={props.screenName}
|
||||
>
|
||||
<div className="mx_UnknownIdentityUsersWarningDialog_headerContainer">
|
||||
<PageHeader Icon={UserAddSolidIcon} heading={title}>
|
||||
<p>{headerText}</p>
|
||||
</PageHeader>
|
||||
</div>
|
||||
|
||||
<ul className="mx_UnknownIdentityUsersWarningDialog_userList" role="listbox">
|
||||
{props.users.map(userListItem)}
|
||||
</ul>
|
||||
|
||||
<div className="mx_UnknownIdentityUsersWarningDialog_buttons">{buttons}</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function dmButtons(props: { onContinue: () => void; onCancel: () => void }): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Button size="lg" kind="secondary" onClick={props.onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="lg" kind="primary" onClick={props.onContinue}>
|
||||
Continue
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function inviteButtons(props: { onInvite: () => void; onRemove: () => void }): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Button size="lg" kind="secondary" onClick={props.onRemove} Icon={CloseIcon}>
|
||||
Remove
|
||||
</Button>
|
||||
<Button size="lg" kind="primary" onClick={props.onInvite} Icon={CheckIcon}>
|
||||
Invite
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user