Skip to content

Commit 8244607

Browse files
committed
Hide the names of banned users behind a spoiler tag
1 parent 8d7c5bb commit 8244607

File tree

7 files changed

+144
-13
lines changed

7 files changed

+144
-13
lines changed

src/TextForEvent.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
3939
import { getSenderName } from "./utils/event/getSenderName";
4040
import PosthogTrackers from "./PosthogTrackers.ts";
4141
import { ElementCallEventType } from "./call-types.ts";
42+
import Spoiler from "./components/views/elements/Spoiler.tsx";
4243

4344
function getRoomMemberDisplayname(client: MatrixClient, event: MatrixEvent, userId = event.getSender()): string {
4445
const roomId = event.getRoomId();
@@ -107,7 +108,7 @@ function textForMemberEvent(
107108
client: MatrixClient,
108109
allowJSX: boolean,
109110
showHiddenEvents?: boolean,
110-
): (() => string) | null {
111+
): (() => Renderable) | null {
111112
// XXX: SYJS-16 "sender is sometimes null for join messages"
112113
const senderName = ev.sender?.name || getRoomMemberDisplayname(client, ev);
113114
const targetName = ev.target?.name || getRoomMemberDisplayname(client, ev, ev.getStateKey());
@@ -133,10 +134,26 @@ function textForMemberEvent(
133134
}
134135
}
135136
case KnownMembership.Ban:
136-
return () =>
137-
reason
138-
? _t("timeline|m.room.member|ban_reason", { senderName, targetName, reason })
139-
: _t("timeline|m.room.member|ban", { senderName, targetName });
137+
if (allowJSX) {
138+
return reason
139+
? () =>
140+
_t(
141+
"timeline|m.room.member|ban_reason_spoiler",
142+
{ senderName, reason },
143+
{ user: () => <Spoiler>{targetName}</Spoiler> },
144+
)
145+
: () =>
146+
_t(
147+
"timeline|m.room.member|ban_spoiler",
148+
{ senderName },
149+
{ user: () => <Spoiler>{targetName}</Spoiler> },
150+
);
151+
}
152+
153+
return reason
154+
? () => _t("timeline|m.room.member|ban_reason", { senderName, reason })
155+
: () => _t("timeline|m.room.member|ban", { senderName });
156+
140157
case KnownMembership.Join:
141158
if (prevContent && prevContent.membership === KnownMembership.Join) {
142159
const modDisplayname = getModification(prevContent.displayname, content.displayname);

src/components/views/elements/EventListSummary.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
88
Please see LICENSE files in the repository root for full details.
99
*/
1010

11-
import React, { type ComponentProps, type ReactNode } from "react";
11+
import React, { type ReactElement, type ComponentProps, type ReactNode } from "react";
1212
import { EventType, type MatrixEvent, MatrixEventEvent, type RoomMember } from "matrix-js-sdk/src/matrix";
1313
import { KnownMembership } from "matrix-js-sdk/src/types";
1414
import { throttle } from "lodash";
@@ -25,6 +25,7 @@ import AccessibleButton from "./AccessibleButton";
2525
import RoomContext from "../../../contexts/RoomContext";
2626
import { arrayHasDiff } from "../../../utils/arrays.ts";
2727
import { objectHasDiff } from "../../../utils/objects.ts";
28+
import Spoiler from "./Spoiler.tsx";
2829

2930
const onPinnedMessagesClick = (): void => {
3031
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
@@ -222,7 +223,15 @@ export default class EventListSummary extends React.Component<Props, State> {
222223
): ReactNode {
223224
const summaries = orderedTransitionSequences.map((transitions) => {
224225
const userNames = eventAggregates[transitions];
225-
const nameList = this.renderNameList(userNames);
226+
let spoileredUserNames: ReactElement[];
227+
228+
if (containsBanned(transitions)) {
229+
spoileredUserNames = userNames.map((u) => <Spoiler key={u}>{u}</Spoiler>);
230+
} else {
231+
spoileredUserNames = userNames.map((u) => <>{u}</>);
232+
}
233+
234+
const nameList = this.renderNameList(spoileredUserNames);
226235

227236
const splitTransitions = transitions.split(SEP) as TransitionType[];
228237

@@ -234,7 +243,11 @@ export default class EventListSummary extends React.Component<Props, State> {
234243
const coalescedTransitions = EventListSummary.coalesceRepeatedTransitions(canonicalTransitions);
235244

236245
const descs = coalescedTransitions.map((t) => {
237-
return EventListSummary.getDescriptionForTransition(t.transitionType, userNames.length, t.repeats);
246+
return EventListSummary.getDescriptionForTransition(
247+
t.transitionType,
248+
spoileredUserNames.length,
249+
t.repeats,
250+
);
238251
});
239252

240253
const desc = formatList(descs);
@@ -255,7 +268,7 @@ export default class EventListSummary extends React.Component<Props, State> {
255268
* more items in `users` than `this.props.summaryLength`, which is the number of names
256269
* included before "and [n] others".
257270
*/
258-
private renderNameList(users: string[]): string {
271+
private renderNameList(users: ReactElement[]): ReactElement {
259272
return formatList(users, this.props.summaryLength);
260273
}
261274

@@ -618,3 +631,11 @@ export default class EventListSummary extends React.Component<Props, State> {
618631
);
619632
}
620633
}
634+
635+
/**
636+
* Returns true if the provided list comma-separated list of transitions
637+
* contains an item "banned".
638+
*/
639+
function containsBanned(transitions: string): boolean {
640+
return transitions.startsWith(TransitionType.Banned) || transitions.includes(`,${TransitionType.Banned}`);
641+
}

src/i18n/strings/en_EN.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3483,8 +3483,10 @@
34833483
"m.room.member": {
34843484
"accepted_3pid_invite": "%(targetName)s accepted the invitation for %(displayName)s",
34853485
"accepted_invite": "%(targetName)s accepted an invitation",
3486-
"ban": "%(senderName)s banned %(targetName)s",
3487-
"ban_reason": "%(senderName)s banned %(targetName)s: %(reason)s",
3486+
"ban": "%(senderName)s banned a user",
3487+
"ban_reason": "%(senderName)s banned a user: %(reason)s",
3488+
"ban_reason_spoiler": "%(senderName)s banned <user/>: %(reason)s",
3489+
"ban_spoiler": "%(senderName)s banned <user/>",
34883490
"change_avatar": "%(senderName)s changed their profile picture",
34893491
"change_name": "%(oldDisplayName)s changed their display name to %(displayName)s",
34903492
"change_name_avatar": "%(oldDisplayName)s changed their display name and profile picture",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
2020
import { render } from "jest-matrix-react";
2121
import { type ReactElement } from "react";
2222
import { type Mocked, mocked } from "jest-mock";
23+
import React from "react";
2324

2425
import { hasText, textForEvent } from "../../src/TextForEvent";
2526
import SettingsStore from "../../src/settings/SettingsStore";
@@ -28,6 +29,7 @@ import { MatrixClientPeg } from "../../src/MatrixClientPeg";
2829
import UserIdentifierCustomisations from "../../src/customisations/UserIdentifier";
2930
import { getSenderName } from "../../src/utils/event/getSenderName";
3031
import { ElementCallEventType } from "../../src/call-types";
32+
import Spoiler from "../../src/components/views/elements/Spoiler";
3133

3234
jest.mock("../../src/settings/SettingsStore");
3335
jest.mock("../../src/customisations/UserIdentifier", () => ({
@@ -562,6 +564,50 @@ describe("TextForEvent", () => {
562564
),
563565
).toMatchInlineSnapshot(`"Member rejected the invitation: I don't want to be in this room."`);
564566
});
567+
568+
it("shows single-user bans with a spoiler on display name", () => {
569+
mocked(mockClient.getRoom).mockReturnValue({
570+
getMember: jest.fn().mockImplementation((userId) => {
571+
return { rawDisplayName: userId === "@admin:example.com" ? "Admin" : "Bad User" };
572+
}),
573+
} as unknown as Mocked<Room>);
574+
575+
expect(textForEvent(banEventWithReason(), mockClient, true)).toEqual(
576+
<span>
577+
Admin banned <Spoiler>Bad User</Spoiler>: bad behaviour
578+
</span>,
579+
);
580+
});
581+
582+
it("hides user name for single-user bans with reason when JSX is not allowed", () => {
583+
mocked(mockClient.getRoom).mockReturnValue({
584+
getMember: jest.fn().mockImplementation((userId) => {
585+
return { rawDisplayName: userId === "@admin:example.com" ? "Admin" : "Bad User" };
586+
}),
587+
} as unknown as Mocked<Room>);
588+
589+
expect(textForEvent(banEventWithReason(), mockClient)).toEqual("Admin banned a user: bad behaviour");
590+
});
591+
592+
it("shows single-user bans with a spoiler on user ID", () => {
593+
mocked(mockClient.getRoom).mockReturnValue({
594+
getMember: jest.fn().mockReturnValue({ rawDisplayName: undefined }),
595+
} as unknown as Mocked<Room>);
596+
597+
expect(textForEvent(banEvent(), mockClient, true)).toEqual(
598+
<span>
599+
@admin:example.com banned <Spoiler>@bad_name:bad_server.co</Spoiler>
600+
</span>,
601+
);
602+
});
603+
604+
it("hides user name for single-user bans when JSX is not allowed", () => {
605+
mocked(mockClient.getRoom).mockReturnValue({
606+
getMember: jest.fn().mockReturnValue({ rawDisplayName: undefined }),
607+
} as unknown as Mocked<Room>);
608+
609+
expect(textForEvent(banEvent(), mockClient)).toEqual("@admin:example.com banned a user");
610+
});
565611
});
566612

567613
describe("textForJoinRulesEvent()", () => {
@@ -717,3 +763,26 @@ describe("TextForEvent", () => {
717763
});
718764
});
719765
});
766+
767+
function banEvent(): MatrixEvent {
768+
return new MatrixEvent({
769+
type: "m.room.member",
770+
sender: "@admin:example.com",
771+
content: {
772+
membership: KnownMembership.Ban,
773+
},
774+
state_key: "@bad_name:bad_server.co",
775+
});
776+
}
777+
778+
function banEventWithReason(): MatrixEvent {
779+
return new MatrixEvent({
780+
type: "m.room.member",
781+
sender: "@admin:example.com",
782+
content: {
783+
membership: KnownMembership.Ban,
784+
reason: "bad behaviour",
785+
},
786+
state_key: "@bad_name:bad_server.co",
787+
});
788+
}

test/unit-tests/__snapshots__/TextForEvent-test.ts.snap renamed to test/unit-tests/__snapshots__/TextForEvent-test.tsx.snap

File renamed without changes.

test/unit-tests/components/structures/__snapshots__/MessagePanel-test.tsx.snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
120120
<span
121121
class="mx_TextualEvent mx_GenericEventListSummary_summary"
122122
>
123-
@user:id made no changes 100 times
123+
<span>
124+
@user:id made no changes 100 times
125+
</span>
124126
</span>
125127
</div>
126128
</div>

test/unit-tests/components/views/elements/EventListSummary-test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,12 @@ describe("EventListSummary", function () {
265265

266266
const { container } = renderComponent(props);
267267
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
268+
269+
// The sequence was summarised correctly
268270
expect(summary).toHaveTextContent("user_1 was unbanned, joined and left 7 times and was invited");
271+
272+
// And there is no spoiler on the user's name since they were not banned
273+
expect(summary).not.toContainHTML("mx_EventTile_spoiler_content");
269274
});
270275

271276
it("truncates multiple sequences of repetitions with other events between", function () {
@@ -309,9 +314,14 @@ describe("EventListSummary", function () {
309314

310315
const { container } = renderComponent(props);
311316
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
317+
318+
// The sequence was summarised correctly
312319
expect(summary).toHaveTextContent(
313-
"user_1 was unbanned, joined and left 2 times, was banned, " + "joined and left 3 times and was invited",
320+
"user_1 was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited",
314321
);
322+
323+
// And the banned user's name is hidden within a spoiler
324+
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_1</span>');
315325
});
316326

317327
it("handles multiple users following the same sequence of memberships", function () {
@@ -361,9 +371,14 @@ describe("EventListSummary", function () {
361371

362372
const { container } = renderComponent(props);
363373
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
374+
375+
// The sequence was summarised correctly
364376
expect(summary).toHaveTextContent(
365377
"user_1 and one other were unbanned, joined and left 2 times and were banned",
366378
);
379+
380+
// And the banned user's name is hidden within a spoiler
381+
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_1</span>');
367382
});
368383

369384
it("handles many users following the same sequence of memberships", function () {
@@ -393,9 +408,14 @@ describe("EventListSummary", function () {
393408

394409
const { container } = renderComponent(props);
395410
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
411+
412+
// The sequence was summarised correctly
396413
expect(summary).toHaveTextContent(
397414
"user_0 and 19 others were unbanned, joined and left 2 times and were banned",
398415
);
416+
417+
// And the banned user's name is hidden within a spoiler
418+
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_0</span>');
399419
});
400420

401421
it("correctly orders sequences of transitions by the order of their first event", function () {

0 commit comments

Comments
 (0)