Skip to content

Commit f90c602

Browse files
Half-Shotsnowping
authored andcommitted
Allow reporting a room when rejecting an invite. (element-hq#29570)
* Add report room dialog button/dialog. * Update copy * fixup tests / lint * Fix title in test. * update snapshot * Add unit tests for dialog * lint * First pass at adding a report room on invite. * Use a single line input field for reason to avoid bumping the layout. * Fixups * Embed reason to make it clear on grouping * Revert accidental commit * lint * Add some playwright tests. * tweaks * Make ignored users list more accessible. * i18n * Fix sliding sync test. * Add unit test * Even more unit tests. * move test * Update to match designs. * remove console statements * fix css * tidy up * improve comments * fix css * updates
1 parent 8de6992 commit f90c602

File tree

29 files changed

+840
-323
lines changed

29 files changed

+840
-323
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2025 New Vector 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 { test, expect } from "../../element-web-test";
9+
10+
test.describe("Invites", () => {
11+
test.use({
12+
displayName: "Alice",
13+
botCreateOpts: {
14+
displayName: "Bob",
15+
},
16+
});
17+
18+
test("should render an invite view", { tag: "@screenshot" }, async ({ page, homeserver, user, bot, app }) => {
19+
const roomId = await bot.createRoom({ is_direct: true });
20+
await bot.inviteUser(roomId, user.userId);
21+
await app.viewRoomByName("Bob");
22+
await expect(page.locator(".mx_RoomView")).toMatchScreenshot("Invites_room_view.png");
23+
});
24+
25+
test("should be able to decline an invite", async ({ page, homeserver, user, bot, app }) => {
26+
const roomId = await bot.createRoom({ is_direct: true });
27+
await bot.inviteUser(roomId, user.userId);
28+
await app.viewRoomByName("Bob");
29+
await page.getByRole("button", { name: "Decline", exact: true }).click();
30+
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
31+
await expect(
32+
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Bob", exact: true }),
33+
).not.toBeVisible();
34+
});
35+
36+
test(
37+
"should be able to decline an invite, report the room and ignore the user",
38+
{ tag: "@screenshot" },
39+
async ({ page, homeserver, user, bot, app }) => {
40+
const roomId = await bot.createRoom({ is_direct: true });
41+
await bot.inviteUser(roomId, user.userId);
42+
await app.viewRoomByName("Bob");
43+
await page.getByRole("button", { name: "Decline and block" }).click();
44+
await page.getByLabel("Ignore user").click();
45+
await page.getByLabel("Report room").click();
46+
await page.getByLabel("Reason").fill("Do not want the room");
47+
const roomReported = page.waitForRequest(
48+
(req) =>
49+
req.url().endsWith(`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/report`) &&
50+
req.method() === "POST",
51+
);
52+
await expect(page.getByRole("dialog", { name: "Decline invitation" })).toMatchScreenshot(
53+
"Invites_reject_dialog.png",
54+
);
55+
await page.getByRole("button", { name: "Decline invite" }).click();
56+
57+
// Check room was reported.
58+
await roomReported;
59+
60+
// Check user is ignored.
61+
await app.settings.openUserSettings("Security & Privacy");
62+
const ignoredUsersList = page.getByRole("list", { name: "Ignored users" });
63+
await ignoredUsersList.scrollIntoViewIfNeeded();
64+
await expect(ignoredUsersList.getByRole("listitem", { name: bot.credentials.userId })).toBeVisible();
65+
},
66+
);
67+
});

playwright/e2e/sliding-sync/sliding-sync.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ test.describe("Sliding Sync", () => {
255255
// Select the room to reject
256256
await page.getByRole("treeitem", { name: "Room to Reject" }).click();
257257

258-
// Reject the invite
259-
await page.locator(".mx_RoomView").getByRole("button", { name: "Reject", exact: true }).click();
258+
// Decline the invite
259+
await page.locator(".mx_RoomView").getByRole("button", { name: "Decline", exact: true }).click();
260260

261261
await expect(
262262
page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
-160 Bytes
Loading
33.3 KB
Loading
17.7 KB
Loading

res/css/views/dialogs/_ReportRoomDialog.pcss

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
55
Please see LICENSE files in the repository root for full details.
66
*/
77

8-
.mx_ReportRoomDialog {
8+
.mx_ReportRoomDialog,
9+
.mx_DeclineAndBlockInviteDialog {
910
textarea {
1011
font: var(--cpd-font-body-md-regular);
1112
border: 1px solid var(--cpd-color-border-interactive-primary);
@@ -14,7 +15,26 @@ Please see LICENSE files in the repository root for full details.
1415
padding: var(--cpd-space-3x) var(--cpd-space-4x);
1516
}
1617

17-
label {
18+
/*
19+
Workaround to fix labels appearing with the wrong color.
20+
21+
.mx_Dialog (in res/css/_common.pcss) redefines the body color
22+
as $light-fg-color rather than the standard primary color.
23+
24+
This forces the colour to match the Compound style, but
25+
in the future the Dialogs should not force a color.
26+
*/
27+
form label {
28+
color: var(--cpd-color-text-primary);
29+
}
30+
}
31+
32+
.mx_DeclineAndBlockInviteDialog {
33+
div[aria-disabled="true"] > label {
34+
color: var(--cpd-color-text-secondary);
35+
}
36+
37+
.mx_SettingsFlag_label {
1838
color: var(--cpd-color-text-primary);
1939
font-weight: var(--cpd-font-weight-semibold);
2040
}

res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ Please see LICENSE files in the repository root for full details.
1111
column-gap: $spacing-8;
1212
}
1313

14+
.mx_SecurityUserSettingsTab_ignoredUsers {
15+
padding-left: 0;
16+
margin: 0;
17+
list-style: none;
18+
}
19+
1420
.mx_SecurityUserSettingsTab_ignoredUser {
1521
margin-bottom: $spacing-4;
1622
}

src/components/structures/MatrixChat.tsx

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -711,36 +711,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
711711
case "copy_room":
712712
this.copyRoom(payload.room_id);
713713
break;
714-
case "reject_invite":
715-
Modal.createDialog(QuestionDialog, {
716-
title: _t("reject_invitation_dialog|title"),
717-
description: _t("reject_invitation_dialog|confirmation"),
718-
onFinished: (confirm) => {
719-
if (confirm) {
720-
// FIXME: controller shouldn't be loading a view :(
721-
const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner");
722-
723-
MatrixClientPeg.safeGet()
724-
.leave(payload.room_id)
725-
.then(
726-
() => {
727-
modal.close();
728-
if (this.state.currentRoomId === payload.room_id) {
729-
dis.dispatch({ action: Action.ViewHomePage });
730-
}
731-
},
732-
(err) => {
733-
modal.close();
734-
Modal.createDialog(ErrorDialog, {
735-
title: _t("reject_invitation_dialog|failed"),
736-
description: err.toString(),
737-
});
738-
},
739-
);
740-
}
741-
},
742-
});
743-
break;
744714
case "view_user_info":
745715
this.viewUser(payload.userId, payload.subAction);
746716
break;

src/components/structures/RoomView.tsx

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
134134
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
135135
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
136136
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
137+
import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog";
137138

138139
const DEBUG = false;
139140
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@@ -1732,48 +1733,61 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
17321733
});
17331734
};
17341735

1735-
private onRejectButtonClicked = (): void => {
1736-
const roomId = this.getRoomId();
1737-
if (!roomId) return;
1736+
private onDeclineAndBlockButtonClicked = async (): Promise<void> => {
1737+
if (!this.state.room || !this.context.client) return;
1738+
const [shouldReject, ignoreUser, reportRoom] = await Modal.createDialog(DeclineAndBlockInviteDialog, {
1739+
roomName: this.state.room.name,
1740+
}).finished;
1741+
if (!shouldReject) {
1742+
return;
1743+
}
1744+
17381745
this.setState({
17391746
rejecting: true,
17401747
});
1741-
this.context.client?.leave(roomId).then(
1742-
() => {
1743-
defaultDispatcher.dispatch({ action: Action.ViewHomePage });
1744-
this.setState({
1745-
rejecting: false,
1746-
});
1747-
},
1748-
(error) => {
1749-
logger.error(`Failed to reject invite: ${error}`);
17501748

1751-
const msg = error.message ? error.message : JSON.stringify(error);
1752-
Modal.createDialog(ErrorDialog, {
1753-
title: _t("room|failed_reject_invite"),
1754-
description: msg,
1755-
});
1749+
const actions: Promise<unknown>[] = [];
17561750

1757-
this.setState({
1758-
rejecting: false,
1759-
});
1760-
},
1761-
);
1762-
};
1751+
if (ignoreUser) {
1752+
const myMember = this.state.room.getMember(this.context.client!.getSafeUserId());
1753+
const inviteEvent = myMember!.events.member;
1754+
const ignoredUsers = this.context.client.getIgnoredUsers();
1755+
ignoredUsers.push(inviteEvent!.getSender()!); // de-duped internally in the js-sdk
1756+
actions.push(this.context.client.setIgnoredUsers(ignoredUsers));
1757+
}
17631758

1764-
private onRejectAndIgnoreClick = async (): Promise<void> => {
1765-
this.setState({
1766-
rejecting: true,
1767-
});
1759+
if (reportRoom !== false) {
1760+
actions.push(this.context.client.reportRoom(this.state.room.roomId, reportRoom));
1761+
}
17681762

1763+
actions.push(this.context.client.leave(this.state.room.roomId));
17691764
try {
1770-
const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId());
1771-
const inviteEvent = myMember!.events.member;
1772-
const ignoredUsers = this.context.client!.getIgnoredUsers();
1773-
ignoredUsers.push(inviteEvent!.getSender()!); // de-duped internally in the js-sdk
1774-
await this.context.client!.setIgnoredUsers(ignoredUsers);
1765+
await Promise.all(actions);
1766+
defaultDispatcher.dispatch({ action: Action.ViewHomePage });
1767+
this.setState({
1768+
rejecting: false,
1769+
});
1770+
} catch (error) {
1771+
logger.error(`Failed to reject invite: ${error}`);
1772+
1773+
const msg = error instanceof Error ? error.message : JSON.stringify(error);
1774+
Modal.createDialog(ErrorDialog, {
1775+
title: _t("room|failed_reject_invite"),
1776+
description: msg,
1777+
});
17751778

1776-
await this.context.client!.leave(this.state.roomId!);
1779+
this.setState({
1780+
rejecting: false,
1781+
});
1782+
}
1783+
};
1784+
1785+
private onDeclineButtonClicked = async (): Promise<void> => {
1786+
if (!this.state.room || !this.context.client) {
1787+
return;
1788+
}
1789+
try {
1790+
await this.context.client.leave(this.state.room.roomId);
17771791
defaultDispatcher.dispatch({ action: Action.ViewHomePage });
17781792
this.setState({
17791793
rejecting: false,
@@ -2126,7 +2140,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
21262140
<RoomPreviewBar
21272141
onJoinClick={this.onJoinButtonClicked}
21282142
onForgetClick={this.onForgetClick}
2129-
onRejectClick={this.onRejectThreepidInviteButtonClicked}
2143+
onDeclineClick={this.onRejectThreepidInviteButtonClicked}
21302144
canPreview={false}
21312145
error={this.state.roomLoadError}
21322146
roomAlias={roomAlias}
@@ -2154,7 +2168,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
21542168
<RoomPreviewCard
21552169
room={this.state.room}
21562170
onJoinButtonClicked={this.onJoinButtonClicked}
2157-
onRejectButtonClicked={this.onRejectButtonClicked}
2171+
onRejectButtonClicked={this.onDeclineButtonClicked}
21582172
/>
21592173
</div>
21602174
;
@@ -2196,8 +2210,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
21962210
<RoomPreviewBar
21972211
onJoinClick={this.onJoinButtonClicked}
21982212
onForgetClick={this.onForgetClick}
2199-
onRejectClick={this.onRejectButtonClicked}
2200-
onRejectAndIgnoreClick={this.onRejectAndIgnoreClick}
2213+
onDeclineClick={this.onDeclineButtonClicked}
2214+
onDeclineAndBlockClick={this.onDeclineAndBlockButtonClicked}
2215+
promptRejectionOptions={true}
22012216
inviterName={inviterName}
22022217
canPreview={false}
22032218
joining={this.state.joining}
@@ -2312,7 +2327,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
23122327
<RoomPreviewBar
23132328
onJoinClick={this.onJoinButtonClicked}
23142329
onForgetClick={this.onForgetClick}
2315-
onRejectClick={this.onRejectThreepidInviteButtonClicked}
2330+
onDeclineClick={this.onRejectThreepidInviteButtonClicked}
2331+
promptRejectionOptions={true}
23162332
joining={this.state.joining}
23172333
inviterName={inviterName}
23182334
invitedEmail={invitedEmail}
@@ -2350,7 +2366,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
23502366
onRejectButtonClicked={
23512367
this.props.threepidInvite
23522368
? this.onRejectThreepidInviteButtonClicked
2353-
: this.onRejectButtonClicked
2369+
: this.onDeclineButtonClicked
23542370
}
23552371
/>
23562372
);

0 commit comments

Comments
 (0)