Confirm before inviting unknown users to a DM/room (#33171)
* 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. * Add `UnknownIdentityUsersWarningDialog` * Add unit tests * Update playwright tests * Convert if/else to switch statement * Convert helper functions to React components * Factor out "onRemove" callback * Add clarifying comment
This commit is contained in:
@@ -27,6 +27,9 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
|
|||||||
await page.getByRole("option", { name: bob.credentials.displayName }).click();
|
await page.getByRole("option", { name: bob.credentials.displayName }).click();
|
||||||
await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible();
|
await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Go" }).click();
|
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) => {
|
const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test.describe("History sharing", function () {
|
|||||||
await sendMessageInCurrentRoom(alicePage, "A message from Alice");
|
await sendMessageInCurrentRoom(alicePage, "A message from Alice");
|
||||||
|
|
||||||
// Send the invite to Bob
|
// Send the invite to Bob
|
||||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
|
||||||
|
|
||||||
// Bob accepts the invite
|
// Bob accepts the invite
|
||||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
||||||
@@ -105,7 +105,7 @@ test.describe("History sharing", function () {
|
|||||||
|
|
||||||
// Alice invites Bob, and Bob accepts
|
// Alice invites Bob, and Bob accepts
|
||||||
const roomId = await aliceElementApp.getCurrentRoomIdFromUrl();
|
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("option", { name: "TestRoom" }).click();
|
||||||
await bobPage.getByRole("button", { name: "Accept" }).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'");
|
await sendMessageInCurrentRoom(bobPage, "Message3: 'shared' visibility, but Bob thinks it is still 'joined'");
|
||||||
|
|
||||||
// Alice now invites Charlie
|
// 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("option", { name: "TestRoom" }).click();
|
||||||
await charliePage.getByRole("button", { name: "Accept" }).click();
|
await charliePage.getByRole("button", { name: "Accept" }).click();
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
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.describe("Invite dialog", function () {
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Hanako",
|
displayName: "Hanako",
|
||||||
@@ -62,6 +71,15 @@ test.describe("Invite dialog", function () {
|
|||||||
// Invite the bot
|
// Invite the bot
|
||||||
await other.getByRole("button", { name: "Invite" }).click();
|
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
|
// Assert that the invite dialog disappears
|
||||||
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
|
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
|
||||||
|
|
||||||
@@ -104,6 +122,15 @@ test.describe("Invite dialog", function () {
|
|||||||
// Open a direct message UI
|
// Open a direct message UI
|
||||||
await other.getByRole("button", { name: "Go" }).click();
|
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
|
// Assert that the invite dialog disappears
|
||||||
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
|
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ test.describe("Create Room", () => {
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Go" }).click();
|
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("Encryption enabled")).toBeVisible();
|
||||||
await expect(page.getByText("Send your first message to")).toBeVisible();
|
await expect(page.getByText("Send your first message to")).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ test.describe("Room Status Bar", () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await other.getByRole("option", { name: "Alice" }).click();
|
await other.getByRole("option", { name: "Alice" }).click();
|
||||||
await other.getByRole("button", { name: "Go" }).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
|
// Send a message to invite the bots
|
||||||
const composer = app.getComposerField();
|
const composer = app.getComposerField();
|
||||||
await composer.fill("Hello");
|
await composer.fill("Hello");
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ test.describe("Other people's devices section in Encryption tab", () => {
|
|||||||
|
|
||||||
// Create the room and invite bob
|
// Create the room and invite bob
|
||||||
await createRoom(alicePage, "TestRoom", true);
|
await createRoom(alicePage, "TestRoom", true);
|
||||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
|
||||||
|
|
||||||
// Bob accepts the invite
|
// Bob accepts the invite
|
||||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
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
|
// Create the room and invite bob
|
||||||
await createRoom(alicePage, "TestRoom", true);
|
await createRoom(alicePage, "TestRoom", true);
|
||||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
|
||||||
|
|
||||||
// Bob accepts the invite
|
// Bob accepts the invite
|
||||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
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
|
// Create the room and invite bob
|
||||||
await createRoom(alicePage, "TestRoom", true);
|
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.
|
// Bob accepts the invite and dismisses the warnings.
|
||||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
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.
|
// Alice creates the room and invite Bob.
|
||||||
await createRoom(alicePage, "TestRoom", true);
|
await createRoom(alicePage, "TestRoom", true);
|
||||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
|
||||||
|
|
||||||
// Bob accepts the invite.
|
// Bob accepts the invite.
|
||||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
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.
|
// Alice creates the room and invite Bob.
|
||||||
await createRoom(alicePage, "TestRoom", true);
|
await createRoom(alicePage, "TestRoom", true);
|
||||||
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
|
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
|
||||||
|
|
||||||
// Bob accepts the invite.
|
// Bob accepts the invite.
|
||||||
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
await bobPage.getByRole("option", { name: "TestRoom" }).click();
|
||||||
|
|||||||
@@ -233,15 +233,30 @@ export class ElementAppPage {
|
|||||||
* Open the room info panel, and use it to send an invite to the given user.
|
* 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 userId - The user to invite to the room.
|
||||||
|
* @param options - Options object
|
||||||
*/
|
*/
|
||||||
public async inviteUserToCurrentRoom(userId: string): Promise<void> {
|
public async inviteUserToCurrentRoom(
|
||||||
|
userId: string,
|
||||||
|
options?: {
|
||||||
|
/** If true, expect and acknowledge "Confirm inviting new users" page */
|
||||||
|
confirmUnknownUser?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
const rightPanel = await this.openRoomInfoPanel();
|
const rightPanel = await this.openRoomInfoPanel();
|
||||||
await rightPanel.getByRole("menuitem", { name: "Invite" }).click();
|
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.fill(userId);
|
||||||
await input.press("Enter");
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -598,6 +598,7 @@ legend {
|
|||||||
.mx_AccessSecretStorageDialog button,
|
.mx_AccessSecretStorageDialog button,
|
||||||
.mx_InviteDialog_section button,
|
.mx_InviteDialog_section button,
|
||||||
.mx_InviteDialog_editor button,
|
.mx_InviteDialog_editor button,
|
||||||
|
.mx_UnknownIdentityUsersWarningDialog button,
|
||||||
[class|="maplibregl"]
|
[class|="maplibregl"]
|
||||||
),
|
),
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
|
||||||
@@ -625,7 +626,8 @@ legend {
|
|||||||
.mx_ThemeChoicePanel_CustomTheme button,
|
.mx_ThemeChoicePanel_CustomTheme button,
|
||||||
.mx_UnpinAllDialog button,
|
.mx_UnpinAllDialog button,
|
||||||
.mx_ShareDialog button,
|
.mx_ShareDialog button,
|
||||||
.mx_EncryptionUserSettingsTab button
|
.mx_EncryptionUserSettingsTab button,
|
||||||
|
.mx_UnknownIdentityUsersWarningDialog button
|
||||||
):last-child {
|
):last-child {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
@@ -641,7 +643,8 @@ legend {
|
|||||||
.mx_ShareDialog button,
|
.mx_ShareDialog button,
|
||||||
.mx_EncryptionUserSettingsTab button,
|
.mx_EncryptionUserSettingsTab button,
|
||||||
.mx_InviteDialog_section button,
|
.mx_InviteDialog_section button,
|
||||||
.mx_InviteDialog_editor button
|
.mx_InviteDialog_editor button,
|
||||||
|
.mx_UnknownIdentityUsersWarningDialog button
|
||||||
):focus,
|
):focus,
|
||||||
.mx_Dialog input[type="submit"]:focus,
|
.mx_Dialog input[type="submit"]:focus,
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus,
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus,
|
||||||
@@ -659,7 +662,8 @@ legend {
|
|||||||
.mx_ThemeChoicePanel_CustomTheme button,
|
.mx_ThemeChoicePanel_CustomTheme button,
|
||||||
.mx_UnpinAllDialog button,
|
.mx_UnpinAllDialog button,
|
||||||
.mx_ShareDialog button,
|
.mx_ShareDialog button,
|
||||||
.mx_EncryptionUserSettingsTab button
|
.mx_EncryptionUserSettingsTab button,
|
||||||
|
.mx_UnknownIdentityUsersWarningDialog button
|
||||||
),
|
),
|
||||||
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
||||||
color: var(--cpd-color-text-on-solid-primary);
|
color: var(--cpd-color-text-on-solid-primary);
|
||||||
@@ -678,7 +682,8 @@ legend {
|
|||||||
.mx_ThemeChoicePanel_CustomTheme button,
|
.mx_ThemeChoicePanel_CustomTheme button,
|
||||||
.mx_UnpinAllDialog button,
|
.mx_UnpinAllDialog button,
|
||||||
.mx_ShareDialog button,
|
.mx_ShareDialog button,
|
||||||
.mx_EncryptionUserSettingsTab button
|
.mx_EncryptionUserSettingsTab button,
|
||||||
|
.mx_UnknownIdentityUsersWarningDialog button
|
||||||
),
|
),
|
||||||
.mx_Dialog_buttons input[type="submit"].danger {
|
.mx_Dialog_buttons input[type="submit"].danger {
|
||||||
background-color: var(--cpd-color-bg-critical-primary);
|
background-color: var(--cpd-color-bg-critical-primary);
|
||||||
@@ -701,7 +706,8 @@ legend {
|
|||||||
.mx_ThemeChoicePanel_CustomTheme button,
|
.mx_ThemeChoicePanel_CustomTheme button,
|
||||||
.mx_UnpinAllDialog button,
|
.mx_UnpinAllDialog button,
|
||||||
.mx_ShareDialog button,
|
.mx_ShareDialog button,
|
||||||
.mx_EncryptionUserSettingsTab button
|
.mx_EncryptionUserSettingsTab button,
|
||||||
|
.mx_UnknownIdentityUsersWarningDialog button
|
||||||
):disabled,
|
):disabled,
|
||||||
.mx_Dialog input[type="submit"]:disabled,
|
.mx_Dialog input[type="submit"]:disabled,
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):disabled,
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):disabled,
|
||||||
|
|||||||
@@ -171,6 +171,7 @@
|
|||||||
@import "./views/dialogs/_UserSettingsDialog.pcss";
|
@import "./views/dialogs/_UserSettingsDialog.pcss";
|
||||||
@import "./views/dialogs/_VerifyEMailDialog.pcss";
|
@import "./views/dialogs/_VerifyEMailDialog.pcss";
|
||||||
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss";
|
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss";
|
||||||
|
@import "./views/dialogs/invite/_UnknownIdentityUsersWarningDialog.pcss";
|
||||||
@import "./views/dialogs/security/_AccessSecretStorageDialog.pcss";
|
@import "./views/dialogs/security/_AccessSecretStorageDialog.pcss";
|
||||||
@import "./views/dialogs/security/_CreateCrossSigningDialog.pcss";
|
@import "./views/dialogs/security/_CreateCrossSigningDialog.pcss";
|
||||||
@import "./views/dialogs/security/_CreateSecretStorageDialog.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 InviteProgressBody from "./InviteProgressBody.tsx";
|
||||||
import MultiInviter, { type CompletionStates as MultiInviterCompletionStates } from "../../../utils/MultiInviter.ts";
|
import MultiInviter, { type CompletionStates as MultiInviterCompletionStates } from "../../../utils/MultiInviter.ts";
|
||||||
import { DMRoomTile } from "./invite/DMRoomTile.tsx";
|
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 {
|
interface Result {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -161,6 +164,14 @@ interface IInviteDialogState {
|
|||||||
dialPadValue: string;
|
dialPadValue: string;
|
||||||
currentTabId: TabId;
|
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.)
|
||||||
|
*
|
||||||
|
* Will never be the empty list.
|
||||||
|
*/
|
||||||
|
unknownIdentityUsers: Member[] | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if we are sending the invites.
|
* True if we are sending the invites.
|
||||||
*
|
*
|
||||||
@@ -230,7 +241,8 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||||||
dialPadValue: "",
|
dialPadValue: "",
|
||||||
currentTabId: TabId.UserDirectory,
|
currentTabId: TabId.UserDirectory,
|
||||||
|
|
||||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
unknownIdentityUsers: null,
|
||||||
|
|
||||||
busy: false,
|
busy: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -444,6 +456,21 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the process of actually sending invites or creating a DM.
|
||||||
|
*
|
||||||
|
* Called once we have shown the user all the necessary warnings.
|
||||||
|
*/
|
||||||
|
private async startDmOrSendInvites(): Promise<void> {
|
||||||
|
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<void> => {
|
private transferCall = async (): Promise<void> => {
|
||||||
if (this.props.kind !== InviteKind.CallTransfer) return;
|
if (this.props.kind !== InviteKind.CallTransfer) return;
|
||||||
if (this.state.currentTabId == TabId.UserDirectory) {
|
if (this.state.currentTabId == TabId.UserDirectory) {
|
||||||
@@ -1123,14 +1150,49 @@ 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".
|
* Render content of the "users" that is used for both invites and "start chat".
|
||||||
*/
|
*/
|
||||||
private renderMainTab(): JSX.Element {
|
private renderMainTab(): JSX.Element {
|
||||||
let helpText;
|
let helpText;
|
||||||
let buttonText;
|
let buttonText;
|
||||||
let goButtonFn: (() => Promise<void>) | null = null;
|
|
||||||
|
|
||||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
@@ -1167,7 +1229,6 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||||||
}
|
}
|
||||||
|
|
||||||
buttonText = _t("action|go");
|
buttonText = _t("action|go");
|
||||||
goButtonFn = this.startDm;
|
|
||||||
} else if (this.props.kind === InviteKind.Invite) {
|
} else if (this.props.kind === InviteKind.Invite) {
|
||||||
const roomId = this.props.roomId;
|
const roomId = this.props.roomId;
|
||||||
const room = MatrixClientPeg.get()?.getRoom(roomId);
|
const room = MatrixClientPeg.get()?.getRoom(roomId);
|
||||||
@@ -1211,11 +1272,14 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||||||
);
|
);
|
||||||
|
|
||||||
buttonText = _t("action|invite");
|
buttonText = _t("action|invite");
|
||||||
goButtonFn = this.inviteUsers;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown InviteDialog kind: " + this.props.kind);
|
throw new Error("Unknown InviteDialog kind: " + this.props.kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onGoButtonPressed = (): void => {
|
||||||
|
this.onGoButtonPressed().catch((e) => logErrorAndShowErrorDialog("Error processing invites", e));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<p className="mx_InviteDialog_helpText">{helpText}</p>
|
<p className="mx_InviteDialog_helpText">{helpText}</p>
|
||||||
@@ -1223,7 +1287,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary"
|
kind="primary"
|
||||||
onClick={goButtonFn}
|
onClick={onGoButtonPressed}
|
||||||
className="mx_InviteDialog_goButton"
|
className="mx_InviteDialog_goButton"
|
||||||
disabled={this.state.busy || !this.hasSelection()}
|
disabled={this.state.busy || !this.hasSelection()}
|
||||||
>
|
>
|
||||||
@@ -1235,12 +1299,49 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Callback function, which handles the user clicking "Remove" on the {@link UnknwownIdentityUsersWarningDialog}. */
|
||||||
|
private onRemoveUnknownIdentityUsersClicked = (): void => {
|
||||||
|
// 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.
|
* Render the complete dialog, given this is not a call transfer dialog.
|
||||||
*
|
*
|
||||||
* See also: {@link renderCallTransferDialog}.
|
* See also: {@link renderCallTransferDialog}.
|
||||||
*/
|
*/
|
||||||
private renderRegularDialog(): React.ReactNode {
|
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={this.onRemoveUnknownIdentityUsersClicked}
|
||||||
|
screenName={this.screenName}
|
||||||
|
kind={this.props.kind}
|
||||||
|
users={this.state.unknownIdentityUsers}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
if (this.props.kind === InviteKind.Dm) {
|
if (this.props.kind === InviteKind.Dm) {
|
||||||
title = _t("space|add_existing_room_space|dm_heading");
|
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 {
|
interface IDMRoomTileProps {
|
||||||
member: Member;
|
member: Member;
|
||||||
lastActiveTs?: number;
|
lastActiveTs?: number;
|
||||||
onToggle(member: Member): void;
|
onToggle?(member: Member): void;
|
||||||
isSelected: boolean;
|
isSelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A tile representing a single user in the "suggestions"/"recents" section of the invite dialog. */
|
/** 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.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
this.props.onToggle(this.props.member);
|
this.props.onToggle?.(this.props.member);
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
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> = (props) => {
|
||||||
|
const userListItem = useCallback((u: Member) => <DMRoomTile member={u} key={u.userId} />, []);
|
||||||
|
|
||||||
|
let title: string;
|
||||||
|
let headerText: string;
|
||||||
|
let buttons: JSX.Element;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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" data-testid="userlist">
|
||||||
|
{props.users.map(userListItem)}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mx_UnknownIdentityUsersWarningDialog_buttons">{buttons}</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DmButtons: React.FC<{ onContinue: () => void; onCancel: () => void }> = (props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button size="lg" kind="secondary" onClick={props.onCancel}>
|
||||||
|
{_t("action|cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" kind="primary" onClick={props.onContinue}>
|
||||||
|
{_t("action|continue")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InviteButtons: React.FC<{ onInvite: () => void; onRemove: () => void }> = (props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button size="lg" kind="secondary" onClick={props.onRemove} Icon={CloseIcon}>
|
||||||
|
{_t("action|remove")}
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" kind="primary" onClick={props.onInvite} Icon={CheckIcon}>
|
||||||
|
{_t("action|invite")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnknownIdentityUsersWarningDialog;
|
||||||
@@ -1368,6 +1368,14 @@
|
|||||||
"impossible_dialog_title": "Integrations not allowed"
|
"impossible_dialog_title": "Integrations not allowed"
|
||||||
},
|
},
|
||||||
"invite": {
|
"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_caption": "Invite by email",
|
||||||
"email_limit_one": "Invites by email can only be sent one at a time",
|
"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. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.",
|
"email_use_default_is": "Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.",
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
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 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 { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
import { mocked, type Mocked } from "jest-mock";
|
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 InviteDialog from "../../../../../src/components/views/dialogs/InviteDialog";
|
||||||
import { InviteKind } from "../../../../../src/components/views/dialogs/InviteDialogTypes";
|
import { InviteKind } from "../../../../../src/components/views/dialogs/InviteDialogTypes";
|
||||||
@@ -103,6 +104,11 @@ describe("InviteDialog", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockClient = getMockClientWithEventEmitter({
|
mockClient = getMockClientWithEventEmitter({
|
||||||
|
getCrypto: jest.fn().mockReturnValue({
|
||||||
|
getUserVerificationStatus: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(new UserVerificationStatus(false, false, true, false)),
|
||||||
|
}),
|
||||||
getDomain: jest.fn().mockReturnValue(serverDomain),
|
getDomain: jest.fn().mockReturnValue(serverDomain),
|
||||||
getUserId: jest.fn().mockReturnValue(bobId),
|
getUserId: jest.fn().mockReturnValue(bobId),
|
||||||
getSafeUserId: jest.fn().mockReturnValue(bobId),
|
getSafeUserId: jest.fn().mockReturnValue(bobId),
|
||||||
@@ -449,4 +455,44 @@ describe("InviteDialog", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument();
|
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(
|
||||||
|
<InviteDialog
|
||||||
|
kind={kind as InviteKind.Invite | InviteKind.Dm}
|
||||||
|
roomId={roomId}
|
||||||
|
onFinished={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+104
@@ -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<ComponentProps<typeof UnknownIdentityUsersWarningDialog>>): RenderResult {
|
||||||
|
const props1: ComponentProps<typeof UnknownIdentityUsersWarningDialog> = {
|
||||||
|
onContinue: () => {},
|
||||||
|
onCancel: () => {},
|
||||||
|
onRemove: () => {},
|
||||||
|
screenName: undefined,
|
||||||
|
kind: InviteKind.Dm,
|
||||||
|
users: [],
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
return render(<UnknownIdentityUsersWarningDialog {...props1} />);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user