Hide the names of banned users behind a spoiler tag (attempt 2) (#32636)

This commit is contained in:
Andy Balaam
2026-03-18 10:47:16 +00:00
committed by GitHub
parent cb9ff9b92b
commit 2128c1884c
18 changed files with 328 additions and 59 deletions
@@ -40,7 +40,7 @@ test.describe("Event List Summary", () => {
);
test(
"should display a single ban message on its own",
"should display a single ban message on its own, with the user hidden",
{ tag: "@screenshot" },
async ({ app, homeserver, page, user }) => {
const { bot, roomId } = await setupRoom(app, homeserver, page, user);
@@ -59,7 +59,7 @@ test.describe("Event List Summary", () => {
// When we ban the bot
await app.client.ban(roomId, bot.credentials.userId);
// Then we say that
// Then we say that, but the name is hidden
await expect(
page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", {
hasText: "banned",
@@ -106,7 +106,7 @@ test.describe("Event List Summary", () => {
);
test(
"should display multiple messages as a group",
"should display multiple messages as a group, and hide banned names",
{ tag: "@screenshot" },
async ({ app, homeserver, page, user }) => {
const { bot, roomId } = await setupRoom(app, homeserver, page, user);
@@ -125,7 +125,7 @@ test.describe("Event List Summary", () => {
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
// Then those actions are gathered into a single summary
// Then those actions are gathered into a single summary, with the name hidden
await expect(
page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", {
hasText: "and joined",
@@ -191,6 +191,9 @@ test.describe("Event List Summary", () => {
// here.
await page.getByRole("button", { name: "expand" }).nth(3).click();
// Make sure the mouse is in a consistent position to avoid flaking
await page.getByRole("textbox").hover();
// Then we see all the individual actions
await expect(
page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", {
@@ -207,7 +210,7 @@ test.describe("Event List Summary", () => {
);
test(
"should display join/ban messages for multiple people as a group",
"should display join/ban messages for multiple people as a group, with banned names hidden",
{ tag: "@screenshot" },
async ({ app, homeserver, page, user }) => {
const { bot, roomId } = await setupRoom(app, homeserver, page, user);
@@ -237,7 +240,7 @@ test.describe("Event List Summary", () => {
await bot.joinRoom(roomId);
await bot2.joinRoom(roomId);
// Then those actions are gathered into a single summary
// Then those actions are gathered into a single summary, with banned names hidden
await expect(
page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", {
hasText: "was removed, was invited, and joined",
@@ -258,7 +261,10 @@ test.describe("Event List Summary", () => {
// here.
await page.getByRole("button", { name: "expand" }).nth(3).click();
// Then we see all the individual actions
// Make sure the mouse is in a consistent position to avoid flaking
await page.getByRole("textbox").hover();
// Then we see all the individual actions, with banned names hidden
await expect(
page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", {
hasText: "removed MyBot2",
@@ -321,9 +327,11 @@ async function replaceBotIds(page: Page, bot: Bot, bot2?: Bot) {
await page.evaluate(
([bot1UserId, bot2UserId]) => {
for (const el of document.querySelectorAll("div.mx_TextualEvent")) {
if ("innerText" in el) {
el.innerText = (el.innerText as any as string).replaceAll(bot1UserId, "<<replaced_bot1_id>>");
el.innerText = (el.innerText as any as string).replaceAll(bot2UserId, "<<replaced_bot2_id>>");
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
while (walker.nextNode()) {
const node = walker.currentNode;
node.textContent = node.textContent.replaceAll(bot1UserId, "<<replaced_bot1_id>>");
node.textContent = node.textContent.replaceAll(bot2UserId, "<<replaced_bot2_id>>");
}
}
},
@@ -182,10 +182,10 @@ test.describe("Timeline", () => {
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", {
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
@@ -218,10 +218,10 @@ test.describe("Timeline", () => {
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", {
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
@@ -260,7 +260,7 @@ test.describe("Timeline", () => {
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", {
// Exclude timestamp from snapshot
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
`,
@@ -280,7 +280,7 @@ test.describe("Timeline", () => {
// Save snapshot of collapsed generic event list summary on bubble layout
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", {
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
`,
@@ -325,10 +325,10 @@ test.describe("Timeline", () => {
{
// Exclude timestamp and read marker from snapshot
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
@@ -447,7 +447,7 @@ test.describe("Timeline", () => {
{
// Exclude timestamp from snapshot of mx_MainSplit
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
`,
@@ -523,10 +523,10 @@ test.describe("Timeline", () => {
const screenshotOptions = {
// Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
@@ -651,10 +651,10 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
@@ -1131,10 +1131,10 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar,.mx_JumpToBottomButton {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
@@ -1259,10 +1259,10 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
@@ -1325,10 +1325,10 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
css: `
.mx_MessageTimestamp {
.mx_MessageTimestamp,.mx_TopUnreadMessagesBar {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
.mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 167 KiB

@@ -118,7 +118,7 @@ Please see LICENSE files in the repository root for full details.
.mx_TextualEvent.mx_GenericEventListSummary_summary {
font: var(--cpd-font-body-sm-regular);
display: inline-flex;
display: inline;
color: $secondary-content;
}
+22 -5
View File
@@ -39,6 +39,7 @@ import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
import { getSenderName } from "./utils/event/getSenderName";
import PosthogTrackers from "./PosthogTrackers.ts";
import { ElementCallEventType } from "./call-types.ts";
import Spoiler from "./components/views/elements/Spoiler.tsx";
function getRoomMemberDisplayname(client: MatrixClient, event: MatrixEvent, userId = event.getSender()): string {
const roomId = event.getRoomId();
@@ -107,7 +108,7 @@ function textForMemberEvent(
client: MatrixClient,
allowJSX: boolean,
showHiddenEvents?: boolean,
): (() => string) | null {
): (() => Renderable) | null {
// XXX: SYJS-16 "sender is sometimes null for join messages"
const senderName = ev.sender?.name || getRoomMemberDisplayname(client, ev);
const targetName = ev.target?.name || getRoomMemberDisplayname(client, ev, ev.getStateKey());
@@ -133,10 +134,26 @@ function textForMemberEvent(
}
}
case KnownMembership.Ban:
return () =>
reason
? _t("timeline|m.room.member|ban_reason", { senderName, targetName, reason })
: _t("timeline|m.room.member|ban", { senderName, targetName });
if (allowJSX) {
return reason
? () =>
_t(
"timeline|m.room.member|ban_reason_spoiler",
{ senderName, reason },
{ user: () => <Spoiler>{targetName}</Spoiler> },
)
: () =>
_t(
"timeline|m.room.member|ban_spoiler",
{ senderName },
{ user: () => <Spoiler>{targetName}</Spoiler> },
);
}
return reason
? () => _t("timeline|m.room.member|ban_reason", { senderName, reason })
: () => _t("timeline|m.room.member|ban", { senderName });
case KnownMembership.Join:
if (prevContent && prevContent.membership === KnownMembership.Join) {
const modDisplayname = getModification(prevContent.displayname, content.displayname);
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type ComponentProps, type ReactNode } from "react";
import React, { type ReactElement, type ComponentProps, type ReactNode } from "react";
import { EventType, type MatrixEvent, MatrixEventEvent, type RoomMember } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { throttle } from "lodash";
@@ -25,6 +25,7 @@ import AccessibleButton from "./AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
import { arrayHasDiff } from "../../../utils/arrays.ts";
import { objectHasDiff } from "../../../utils/objects.ts";
import Spoiler from "./Spoiler.tsx";
const onPinnedMessagesClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
@@ -222,7 +223,15 @@ export default class EventListSummary extends React.Component<Props, State> {
): ReactNode {
const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions];
const nameList = this.renderNameList(userNames);
let spoileredUserNames: ReactElement[];
if (containsBanned(transitions)) {
spoileredUserNames = userNames.map((u) => <Spoiler key={u}>{u}</Spoiler>);
} else {
spoileredUserNames = userNames.map((u) => <>{u}</>);
}
const nameList = this.renderNameList(spoileredUserNames);
const splitTransitions = transitions.split(SEP) as TransitionType[];
@@ -234,7 +243,11 @@ export default class EventListSummary extends React.Component<Props, State> {
const coalescedTransitions = EventListSummary.coalesceRepeatedTransitions(canonicalTransitions);
const descs = coalescedTransitions.map((t) => {
return EventListSummary.getDescriptionForTransition(t.transitionType, userNames.length, t.repeats);
return EventListSummary.getDescriptionForTransition(
t.transitionType,
spoileredUserNames.length,
t.repeats,
);
});
const desc = formatList(descs);
@@ -255,7 +268,7 @@ export default class EventListSummary extends React.Component<Props, State> {
* more items in `users` than `this.props.summaryLength`, which is the number of names
* included before "and [n] others".
*/
private renderNameList(users: string[]): string {
private renderNameList(users: ReactElement[]): ReactElement {
return formatList(users, this.props.summaryLength);
}
@@ -618,3 +631,11 @@ export default class EventListSummary extends React.Component<Props, State> {
);
}
}
/**
* Returns true if the provided list of comma-separated list of transitions
* contains an item "banned".
*/
function containsBanned(transitions: string): boolean {
return transitions.startsWith(TransitionType.Banned) || transitions.includes(`,${TransitionType.Banned}`);
}
+4 -2
View File
@@ -3488,8 +3488,10 @@
"m.room.member": {
"accepted_3pid_invite": "%(targetName)s accepted the invitation for %(displayName)s",
"accepted_invite": "%(targetName)s accepted an invitation",
"ban": "%(senderName)s banned %(targetName)s",
"ban_reason": "%(senderName)s banned %(targetName)s: %(reason)s",
"ban": "%(senderName)s banned a user",
"ban_reason": "%(senderName)s banned a user: %(reason)s",
"ban_reason_spoiler": "%(senderName)s banned <user/>: %(reason)s",
"ban_spoiler": "%(senderName)s banned <user/>",
"change_avatar": "%(senderName)s changed their profile picture",
"change_name": "%(oldDisplayName)s changed their display name to %(displayName)s",
"change_name_avatar": "%(oldDisplayName)s changed their display name and profile picture",
@@ -20,6 +20,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { render } from "jest-matrix-react";
import { type ReactElement } from "react";
import { type Mocked, mocked } from "jest-mock";
import React from "react";
import { hasText, textForEvent } from "../../src/TextForEvent";
import SettingsStore from "../../src/settings/SettingsStore";
@@ -28,6 +29,7 @@ import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import UserIdentifierCustomisations from "../../src/customisations/UserIdentifier";
import { getSenderName } from "../../src/utils/event/getSenderName";
import { ElementCallEventType } from "../../src/call-types";
import Spoiler from "../../src/components/views/elements/Spoiler";
jest.mock("../../src/settings/SettingsStore");
jest.mock("../../src/customisations/UserIdentifier", () => ({
@@ -562,6 +564,50 @@ describe("TextForEvent", () => {
),
).toMatchInlineSnapshot(`"Member rejected the invitation: I don't want to be in this room."`);
});
it("shows single-user bans with a spoiler on display name", () => {
mocked(mockClient.getRoom).mockReturnValue({
getMember: jest.fn().mockImplementation((userId) => {
return { rawDisplayName: userId === "@admin:example.com" ? "Admin" : "Bad User" };
}),
} as unknown as Mocked<Room>);
expect(textForEvent(banEventWithReason(), mockClient, true)).toEqual(
<span>
Admin banned <Spoiler>Bad User</Spoiler>: bad behaviour
</span>,
);
});
it("hides user name for single-user bans with reason when JSX is not allowed", () => {
mocked(mockClient.getRoom).mockReturnValue({
getMember: jest.fn().mockImplementation((userId) => {
return { rawDisplayName: userId === "@admin:example.com" ? "Admin" : "Bad User" };
}),
} as unknown as Mocked<Room>);
expect(textForEvent(banEventWithReason(), mockClient)).toEqual("Admin banned a user: bad behaviour");
});
it("shows single-user bans with a spoiler on user ID", () => {
mocked(mockClient.getRoom).mockReturnValue({
getMember: jest.fn().mockReturnValue({ rawDisplayName: undefined }),
} as unknown as Mocked<Room>);
expect(textForEvent(banEvent(), mockClient, true)).toEqual(
<span>
@admin:example.com banned <Spoiler>@bad_name:bad_server.co</Spoiler>
</span>,
);
});
it("hides user name for single-user bans when JSX is not allowed", () => {
mocked(mockClient.getRoom).mockReturnValue({
getMember: jest.fn().mockReturnValue({ rawDisplayName: undefined }),
} as unknown as Mocked<Room>);
expect(textForEvent(banEvent(), mockClient)).toEqual("@admin:example.com banned a user");
});
});
describe("textForJoinRulesEvent()", () => {
@@ -717,3 +763,26 @@ describe("TextForEvent", () => {
});
});
});
function banEvent(): MatrixEvent {
return new MatrixEvent({
type: "m.room.member",
sender: "@admin:example.com",
content: {
membership: KnownMembership.Ban,
},
state_key: "@bad_name:bad_server.co",
});
}
function banEventWithReason(): MatrixEvent {
return new MatrixEvent({
type: "m.room.member",
sender: "@admin:example.com",
content: {
membership: KnownMembership.Ban,
reason: "bad behaviour",
},
state_key: "@bad_name:bad_server.co",
});
}
@@ -120,7 +120,9 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
@user:id made no changes 100 times
<span>
@user:id made no changes 100 times
</span>
</span>
</div>
</div>
@@ -269,7 +269,12 @@ describe("EventListSummary", function () {
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
// The sequence was summarised correctly
expect(summary).toHaveTextContent("user_1 was unbanned, joined and left 7 times and was invited");
// And there is no spoiler on the user's name since they were not banned
expect(summary).not.toContainHTML("mx_EventTile_spoiler_content");
expect(summary).toMatchSnapshot();
});
@@ -314,9 +319,14 @@ describe("EventListSummary", function () {
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
// The sequence was summarised correctly
expect(summary).toHaveTextContent(
"user_1 was unbanned, joined and left 2 times, was banned, " + "joined and left 3 times and was invited",
"user_1 was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited",
);
// And the banned user's name is hidden within a spoiler
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_1</span>');
expect(summary).toMatchSnapshot();
});
@@ -367,9 +377,14 @@ describe("EventListSummary", function () {
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
// The sequence was summarised correctly
expect(summary).toHaveTextContent(
"user_1 and one other were unbanned, joined and left 2 times and were banned",
);
// And the banned user's name is hidden within a spoiler
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_1</span>');
expect(summary).toMatchSnapshot();
});
@@ -400,9 +415,14 @@ describe("EventListSummary", function () {
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
// The sequence was summarised correctly
expect(summary).toHaveTextContent(
"user_0 and 19 others were unbanned, joined and left 2 times and were banned",
);
// And the banned user's name is hidden within a spoiler
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_0</span>');
expect(summary).toMatchSnapshot();
});
@@ -4,7 +4,20 @@ exports[`EventListSummary correctly identifies transitions 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 was invited, was banned, joined, rejected their invitation, left, had their invitation withdrawn, was unbanned, was removed, left and was removed
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
was invited, was banned, joined, rejected their invitation, left, had their invitation withdrawn, was unbanned, was removed, left and was removed
</span>
</span>
`;
@@ -12,9 +25,26 @@ exports[`EventListSummary correctly orders sequences of transitions by the order
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_2 was unbanned and joined and left 2 times
<span>
user_2
was unbanned and joined and left 2 times
</span>
,
user_1 was unbanned, joined and left 2 times and was banned
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
was unbanned, joined and left 2 times and was banned
</span>
</span>
`;
@@ -22,7 +52,16 @@ exports[`EventListSummary handles a summary length = 2, with 1 "other" 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1, user_2 and one other joined
<span>
<span>
user_1
,
user_2
and one other
</span>
joined
</span>
</span>
`;
@@ -30,7 +69,16 @@ exports[`EventListSummary handles a summary length = 2, with many "others" 1`] =
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_0, user_1 and 18 others joined
<span>
<span>
user_0
,
user_1
and 18 others
</span>
joined
</span>
</span>
`;
@@ -38,7 +86,13 @@ exports[`EventListSummary handles a summary length = 2, with no "others" 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 and user_2 joined 2 times
<span>
user_1
and
user_2
joined 2 times
</span>
</span>
`;
@@ -46,7 +100,11 @@ exports[`EventListSummary handles invitation plurals correctly when there are mu
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 rejected their invitation 2 times
<span>
user_1
rejected their invitation 2 times
</span>
</span>
`;
@@ -54,7 +112,14 @@ exports[`EventListSummary handles invitation plurals correctly when there are mu
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 and one other rejected their invitations and had their invitations withdrawn
<span>
<span>
user_1
and one other
</span>
rejected their invitations and had their invitations withdrawn
</span>
</span>
`;
@@ -62,7 +127,23 @@ exports[`EventListSummary handles many users following the same sequence of memb
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_0 and 19 others were unbanned, joined and left 2 times and were banned
<span>
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_0
</span>
</button>
and 19 others
</span>
were unbanned, joined and left 2 times and were banned
</span>
</span>
`;
@@ -70,7 +151,23 @@ exports[`EventListSummary handles multiple users following the same sequence of
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 and one other were unbanned, joined and left 2 times and were banned
<span>
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
and one other
</span>
were unbanned, joined and left 2 times and were banned
</span>
</span>
`;
@@ -78,7 +175,11 @@ exports[`EventListSummary renders collapsed events if events.length = props.thre
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 joined and left and joined
<span>
user_1
joined and left and joined
</span>
</span>
`;
@@ -111,9 +212,17 @@ exports[`EventListSummary should not blindly group 3pid invites and treat them a
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
n...@d... was invited 2 times
<span>
n...@d...
was invited 2 times
</span>
,
d...@w... was invited
<span>
d...@w...
was invited
</span>
</span>
`;
@@ -121,7 +230,11 @@ exports[`EventListSummary truncates long join,leave repetitions 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 joined and left 7 times
<span>
user_1
joined and left 7 times
</span>
</span>
`;
@@ -129,7 +242,11 @@ exports[`EventListSummary truncates long join,leave repetitions between other ev
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 was unbanned, joined and left 7 times and was invited
<span>
user_1
was unbanned, joined and left 7 times and was invited
</span>
</span>
`;
@@ -137,6 +254,19 @@ exports[`EventListSummary truncates multiple sequences of repetitions with other
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
user_1 was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited
</span>
</span>
`;