From 4c1e4f5127bd7fc8a3ab852d630242a704ffbda2 Mon Sep 17 00:00:00 2001 From: Clark Fischer <439978+clarkf@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:31:32 +0000 Subject: [PATCH] Fix "[object Promise]" appearing in HTML exports (#9975) Fixes https://github.com/vector-im/element-web/issues/24272 --- src/DateUtils.ts | 2 +- src/components/structures/MessagePanel.tsx | 4 +- .../dialogs/MessageEditHistoryDialog.tsx | 2 +- .../views/rooms/SearchResultTile.tsx | 7 +- src/utils/exportUtils/HtmlExport.tsx | 43 ++- src/utils/exportUtils/exportCSS.ts | 4 +- .../dialogs/MessageEditHistoryDialog-test.tsx | 83 +++++ .../MessageEditHistoryDialog-test.tsx.snap | 322 ++++++++++++++++++ .../views/rooms/SearchResultTile-test.tsx | 110 +++--- test/test-utils/test-utils.ts | 4 + test/utils/exportUtils/HTMLExport-test.ts | 286 +++++++++++++++- .../__snapshots__/HTMLExport-test.ts.snap | 86 +++++ test/utils/exportUtils/exportCSS-test.ts | 26 ++ 13 files changed, 895 insertions(+), 84 deletions(-) create mode 100644 test/components/views/dialogs/MessageEditHistoryDialog-test.tsx create mode 100644 test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap create mode 100644 test/utils/exportUtils/exportCSS-test.ts diff --git a/src/DateUtils.ts b/src/DateUtils.ts index 5973a7c5f2..c1aa69aacd 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -175,7 +175,7 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean { return prevDate.getFullYear() === nextDate.getFullYear(); } -export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { +export function wantsDateSeparator(prevEventDate: Date | undefined, nextEventDate: Date | undefined): boolean { if (!nextEventDate || !prevEventDate) { return false; } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 98e8f79ec7..2dd432cb92 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -72,7 +72,7 @@ const groupedStateEvents = [ // check if there is a previous event and it has the same sender as this event // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL export function shouldFormContinuation( - prevEvent: MatrixEvent, + prevEvent: MatrixEvent | null, mxEvent: MatrixEvent, showHiddenEvents: boolean, threadsEnabled: boolean, @@ -821,7 +821,7 @@ export default class MessagePanel extends React.Component<IProps, IState> { // here. return !this.props.canBackPaginate; } - return wantsDateSeparator(prevEvent.getDate(), nextEventDate); + return wantsDateSeparator(prevEvent.getDate() || undefined, nextEventDate); } // Get a list of read receipts that should be shown next to this event diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx index 943e7f58d2..8775b4eb5c 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx +++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx @@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps } const baseEventId = this.props.mxEvent.getId(); allEvents.forEach((e, i) => { - if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { + if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) { nodes.push( <li key={e.getTs() + "~"}> <DateSeparator roomId={e.getRoomId()} ts={e.getTs()} /> diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 067cbaee38..3ec68b989f 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -84,7 +84,7 @@ export default class SearchResultTile extends React.Component<IProps> { // is this a continuation of the previous message? const continuation = prevEv && - !wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) && + !wantsDateSeparator(prevEv.getDate() || undefined, mxEv.getDate() || undefined) && shouldFormContinuation( prevEv, mxEv, @@ -96,7 +96,10 @@ export default class SearchResultTile extends React.Component<IProps> { let lastInSection = true; const nextEv = timeline[j + 1]; if (nextEv) { - const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate()); + const willWantDateSeparator = wantsDateSeparator( + mxEv.getDate() || undefined, + nextEv.getDate() || undefined, + ); lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEv.getSender() || diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 6b4240375c..e915d18025 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021, 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ export default class HTMLExporter extends Exporter { } protected async getRoomAvatar(): Promise<ReactNode> { - let blob: Blob; + let blob: Blob | undefined = undefined; const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop"); const avatarPath = "room.png"; if (avatarUrl) { @@ -85,7 +85,7 @@ export default class HTMLExporter extends Exporter { height={32} name={this.room.name} title={this.room.name} - url={blob ? avatarPath : null} + url={blob ? avatarPath : ""} resizeMethod="crop" /> ); @@ -96,9 +96,9 @@ export default class HTMLExporter extends Exporter { const roomAvatar = await this.getRoomAvatar(); const exportDate = formatFullDateNoDayNoTime(new Date()); const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender(); - const creatorName = this.room?.getMember(creator)?.rawDisplayName || creator; - const exporter = this.client.getUserId(); - const exporterName = this.room?.getMember(exporter)?.rawDisplayName; + const creatorName = (creator ? this.room.getMember(creator)?.rawDisplayName : creator) || creator; + const exporter = this.client.getUserId()!; + const exporterName = this.room.getMember(exporter)?.rawDisplayName; const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || ""; const createdText = _t("%(creatorName)s created this room.", { creatorName, @@ -217,20 +217,19 @@ export default class HTMLExporter extends Exporter { </html>`; } - protected getAvatarURL(event: MatrixEvent): string { + protected getAvatarURL(event: MatrixEvent): string | undefined { const member = event.sender; - return ( - member.getMxcAvatarUrl() && mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(30, 30, "crop") - ); + const avatarUrl = member?.getMxcAvatarUrl(); + return avatarUrl ? mediaFromMxc(avatarUrl).getThumbnailOfSourceHttp(30, 30, "crop") : undefined; } protected async saveAvatarIfNeeded(event: MatrixEvent): Promise<void> { - const member = event.sender; + const member = event.sender!; if (!this.avatars.has(member.userId)) { try { const avatarUrl = this.getAvatarURL(event); this.avatars.set(member.userId, true); - const image = await fetch(avatarUrl); + const image = await fetch(avatarUrl!); const blob = await image.blob(); this.addFile(`users/${member.userId.replace(/:/g, "-")}.png`, blob); } catch (err) { @@ -239,19 +238,19 @@ export default class HTMLExporter extends Exporter { } } - protected async getDateSeparator(event: MatrixEvent): Promise<string> { + protected getDateSeparator(event: MatrixEvent): string { const ts = event.getTs(); const dateSeparator = ( <li key={ts}> - <DateSeparator forExport={true} key={ts} roomId={event.getRoomId()} ts={ts} /> + <DateSeparator forExport={true} key={ts} roomId={event.getRoomId()!} ts={ts} /> </li> ); return renderToStaticMarkup(dateSeparator); } - protected async needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent): Promise<boolean> { - if (prevEvent == null) return true; - return wantsDateSeparator(prevEvent.getDate(), event.getDate()); + protected needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent | null): boolean { + if (!prevEvent) return true; + return wantsDateSeparator(prevEvent.getDate() || undefined, event.getDate() || undefined); } public getEventTile(mxEv: MatrixEvent, continuation: boolean): JSX.Element { @@ -264,9 +263,7 @@ export default class HTMLExporter extends Exporter { isRedacted={mxEv.isRedacted()} replacingEventId={mxEv.replacingEventId()} forExport={true} - readReceipts={null} alwaysShowTimestamps={true} - readReceiptMap={null} showUrlPreview={false} checkUnmounting={() => false} isTwelveHour={false} @@ -275,7 +272,6 @@ export default class HTMLExporter extends Exporter { permalinkCreator={this.permalinkCreator} lastSuccessful={false} isSelectedEvent={false} - getRelationsForEvent={null} showReactions={false} layout={Layout.Group} showReadReceipts={false} @@ -286,7 +282,8 @@ export default class HTMLExporter extends Exporter { } protected async getEventTileMarkup(mxEv: MatrixEvent, continuation: boolean, filePath?: string): Promise<string> { - const hasAvatar = !!this.getAvatarURL(mxEv); + const avatarUrl = this.getAvatarURL(mxEv); + const hasAvatar = !!avatarUrl; if (hasAvatar) await this.saveAvatarIfNeeded(mxEv); const EventTile = this.getEventTile(mxEv, continuation); let eventTileMarkup: string; @@ -312,8 +309,8 @@ export default class HTMLExporter extends Exporter { eventTileMarkup = eventTileMarkup.replace(/<span class="mx_MFileBody_info_icon".*?>.*?<\/span>/, ""); if (hasAvatar) { eventTileMarkup = eventTileMarkup.replace( - encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, "&"), - `users/${mxEv.sender.userId.replace(/:/g, "-")}.png`, + encodeURI(avatarUrl).replace(/&/g, "&"), + `users/${mxEv.sender!.userId.replace(/:/g, "-")}.png`, ); } return eventTileMarkup; diff --git a/src/utils/exportUtils/exportCSS.ts b/src/utils/exportUtils/exportCSS.ts index f92e339b02..2a6a098a14 100644 --- a/src/utils/exportUtils/exportCSS.ts +++ b/src/utils/exportUtils/exportCSS.ts @@ -58,8 +58,8 @@ const getExportCSS = async (usedClasses: Set<string>): Promise<string> => { // If the light theme isn't loaded we will have to fetch & parse it manually if (!stylesheets.some(isLightTheme)) { - const href = document.querySelector<HTMLLinkElement>('link[rel="stylesheet"][href$="theme-light.css"]').href; - stylesheets.push(await getRulesFromCssFile(href)); + const href = document.querySelector<HTMLLinkElement>('link[rel="stylesheet"][href$="theme-light.css"]')?.href; + if (href) stylesheets.push(await getRulesFromCssFile(href)); } let css = ""; diff --git a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx new file mode 100644 index 0000000000..cadb92e488 --- /dev/null +++ b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx @@ -0,0 +1,83 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render, RenderResult } from "@testing-library/react"; +import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { flushPromises, mkMessage, stubClient } from "../../../test-utils"; +import MessageEditHistoryDialog from "../../../../src/components/views/dialogs/MessageEditHistoryDialog"; + +describe("<MessageEditHistory />", () => { + const roomId = "!aroom:example.com"; + let client: jest.Mocked<MatrixClient>; + let event: MatrixEvent; + + beforeEach(() => { + client = stubClient() as jest.Mocked<MatrixClient>; + event = mkMessage({ + event: true, + user: "@user:example.com", + room: "!room:example.com", + msg: "My Great Message", + }); + }); + + async function renderComponent(): Promise<RenderResult> { + const result = render(<MessageEditHistoryDialog mxEvent={event} onFinished={jest.fn()} />); + await flushPromises(); + return result; + } + + function mockEdits(...edits: { msg: string; ts: number | undefined }[]) { + client.relations.mockImplementation(() => + Promise.resolve({ + events: edits.map( + (e) => + new MatrixEvent({ + type: EventType.RoomMessage, + room_id: roomId, + origin_server_ts: e.ts, + content: { + body: e.msg, + }, + }), + ), + }), + ); + } + + it("should match the snapshot", async () => { + mockEdits({ msg: "My Great Massage", ts: 1234 }); + + const { container } = await renderComponent(); + + expect(container).toMatchSnapshot(); + }); + + it("should support events with ", async () => { + mockEdits( + { msg: "My Great Massage", ts: undefined }, + { msg: "My Great Massage?", ts: undefined }, + { msg: "My Great Missage", ts: undefined }, + ); + + const { container } = await renderComponent(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap new file mode 100644 index 0000000000..0eb2683003 --- /dev/null +++ b/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap @@ -0,0 +1,322 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<MessageEditHistory /> should match the snapshot 1`] = ` +<div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-labelledby="mx_BaseDialog_title" + class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header mx_Dialog_headerWithCancel" + > + <h2 + class="mx_Heading_h2 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Message edits + </h2> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel" + tabindex="-1" + > + <div + class="mx_RoomView_messageListWrapper" + > + <ol + aria-live="polite" + class="mx_RoomView_MessageList" + > + <ul + class="mx_MessageEditHistoryDialog_edits" + > + <li> + <div + aria-label="Thu, Jan 1 1970" + class="mx_DateSeparator" + role="separator" + tabindex="-1" + > + <hr + role="none" + /> + <h2 + aria-hidden="true" + > + Thu, Jan 1 1970 + </h2> + <hr + role="none" + /> + </div> + </li> + <li> + <div + class="mx_EventTile" + > + <div + class="mx_EventTile_line" + > + <span + class="mx_MessageTimestamp" + > + 00:00 + </span> + <div + class="mx_EventTile_content" + > + <span + class="mx_EventTile_body" + dir="auto" + > + My Great Massage + </span> + </div> + <div + class="mx_MessageActionBar" + > + <div + class="mx_AccessibleButton" + role="button" + tabindex="0" + > + Remove + </div> + </div> + </div> + </div> + </li> + </ul> + </ol> + </div> + </div> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</div> +`; + +exports[`<MessageEditHistory /> should support events with 1`] = ` +<div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-labelledby="mx_BaseDialog_title" + class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header mx_Dialog_headerWithCancel" + > + <h2 + class="mx_Heading_h2 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Message edits + </h2> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel" + tabindex="-1" + > + <div + class="mx_RoomView_messageListWrapper" + > + <ol + aria-live="polite" + class="mx_RoomView_MessageList" + > + <ul + class="mx_MessageEditHistoryDialog_edits" + > + <li> + <div + aria-label=", NaN NaN" + class="mx_DateSeparator" + role="separator" + tabindex="-1" + > + <hr + role="none" + /> + <h2 + aria-hidden="true" + > + , NaN NaN + </h2> + <hr + role="none" + /> + </div> + </li> + <li> + <div + class="mx_EventTile" + > + <div + class="mx_EventTile_line" + > + <span + class="mx_MessageTimestamp" + > + NaN:NaN + </span> + <div + class="mx_EventTile_content" + > + <span + class="mx_EventTile_body markdown-body" + dir="auto" + > + <span> + My Great Massage + <span + class="mx_EditHistoryMessage_deletion" + > + ? + </span> + </span> + </span> + </div> + <div + class="mx_MessageActionBar" + > + <div + class="mx_AccessibleButton" + role="button" + tabindex="0" + > + Remove + </div> + </div> + </div> + </div> + </li> + <li> + <div + class="mx_EventTile" + > + <div + class="mx_EventTile_line" + > + <span + class="mx_MessageTimestamp" + > + NaN:NaN + </span> + <div + class="mx_EventTile_content" + > + <span + class="mx_EventTile_body markdown-body" + dir="auto" + > + <span> + My Great M + <span + class="mx_EditHistoryMessage_deletion" + > + i + </span> + <span + class="mx_EditHistoryMessage_insertion" + > + a + </span> + ssage + <span + class="mx_EditHistoryMessage_insertion" + > + ? + </span> + </span> + </span> + </div> + <div + class="mx_MessageActionBar" + > + <div + class="mx_AccessibleButton" + role="button" + tabindex="0" + > + Remove + </div> + </div> + </div> + </div> + </li> + <li> + <div + class="mx_EventTile" + > + <div + class="mx_EventTile_line" + > + <span + class="mx_MessageTimestamp" + > + NaN:NaN + </span> + <div + class="mx_EventTile_content" + > + <span + class="mx_EventTile_body" + dir="auto" + > + My Great Missage + </span> + </div> + <div + class="mx_MessageActionBar" + > + <div + class="mx_AccessibleButton" + role="button" + tabindex="0" + > + Remove + </div> + </div> + </div> + </div> + </li> + </ul> + </ol> + </div> + </div> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</div> +`; diff --git a/test/components/views/rooms/SearchResultTile-test.tsx b/test/components/views/rooms/SearchResultTile-test.tsx index 59f6381f8c..10983e32e4 100644 --- a/test/components/views/rooms/SearchResultTile-test.tsx +++ b/test/components/views/rooms/SearchResultTile-test.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ limitations under the License. import * as React from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import { render } from "@testing-library/react"; +import { render, type RenderResult } from "@testing-library/react"; import { Room } from "matrix-js-sdk/src/models/room"; import { stubClient } from "../../../test-utils"; @@ -26,6 +26,8 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; const ROOM_ID = "!qPewotXpIctQySfjSy:localhost"; +type Props = React.ComponentPropsWithoutRef<typeof SearchResultTile>; + describe("SearchResultTile", () => { beforeAll(() => { stubClient(); @@ -35,50 +37,72 @@ describe("SearchResultTile", () => { jest.spyOn(cli, "getRoom").mockReturnValue(room); }); + function renderComponent(props: Partial<Props>): RenderResult { + return render(<SearchResultTile timeline={[]} ourEventsIndexes={[1]} {...props} />); + } + it("Sets up appropriate callEventGrouper for m.call. events", () => { - const { container } = render( - <SearchResultTile - timeline={[ - new MatrixEvent({ - type: EventType.CallInvite, - sender: "@user1:server", - room_id: ROOM_ID, - origin_server_ts: 1432735824652, - content: { call_id: "call.1" }, - event_id: "$1:server", - }), - new MatrixEvent({ - content: { - body: "This is an example text message", - format: "org.matrix.custom.html", - formatted_body: "<b>This is an example text message</b>", - msgtype: "m.text", - }, - event_id: "$144429830826TWwbB:localhost", - origin_server_ts: 1432735824653, - room_id: ROOM_ID, - sender: "@example:example.org", - type: "m.room.message", - unsigned: { - age: 1234, - }, - }), - new MatrixEvent({ - type: EventType.CallAnswer, - sender: "@user2:server", - room_id: ROOM_ID, - origin_server_ts: 1432735824654, - content: { call_id: "call.1" }, - event_id: "$2:server", - }), - ]} - ourEventsIndexes={[1]} - />, - ); + const { container } = renderComponent({ + timeline: [ + new MatrixEvent({ + type: EventType.CallInvite, + sender: "@user1:server", + room_id: ROOM_ID, + origin_server_ts: 1432735824652, + content: { call_id: "call.1" }, + event_id: "$1:server", + }), + new MatrixEvent({ + content: { + body: "This is an example text message", + format: "org.matrix.custom.html", + formatted_body: "<b>This is an example text message</b>", + msgtype: "m.text", + }, + event_id: "$144429830826TWwbB:localhost", + origin_server_ts: 1432735824653, + room_id: ROOM_ID, + sender: "@example:example.org", + type: "m.room.message", + unsigned: { + age: 1234, + }, + }), + new MatrixEvent({ + type: EventType.CallAnswer, + sender: "@user2:server", + room_id: ROOM_ID, + origin_server_ts: 1432735824654, + content: { call_id: "call.1" }, + event_id: "$2:server", + }), + ], + }); const tiles = container.querySelectorAll<HTMLElement>(".mx_EventTile"); expect(tiles.length).toEqual(2); - expect(tiles[0].dataset.eventId).toBe("$1:server"); - expect(tiles[1].dataset.eventId).toBe("$144429830826TWwbB:localhost"); + expect(tiles[0]!.dataset.eventId).toBe("$1:server"); + expect(tiles[1]!.dataset.eventId).toBe("$144429830826TWwbB:localhost"); + }); + + it("supports events with missing timestamps", () => { + const { container } = renderComponent({ + timeline: [...Array(20)].map( + (_, i) => + new MatrixEvent({ + type: EventType.RoomMessage, + sender: "@user1:server", + room_id: ROOM_ID, + content: { body: `Message #${i}` }, + event_id: `$${i}:server`, + origin_server_ts: undefined, + }), + ), + }); + + const separators = container.querySelectorAll(".mx_DateSeparator"); + // One separator is always rendered at the top, we don't want any + // between messages. + expect(separators.length).toBe(1); }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 5938f89cfe..59b07a4eb2 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -208,6 +208,10 @@ export function createTestClient(): MatrixClient { setPassword: jest.fn().mockRejectedValue({}), groupCallEventHandler: { groupCalls: new Map<string, GroupCall>() }, redactEvent: jest.fn(), + + createMessagesRequest: jest.fn().mockResolvedValue({ + chunk: [], + }), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); diff --git a/test/utils/exportUtils/HTMLExport-test.ts b/test/utils/exportUtils/HTMLExport-test.ts index 062988f7f2..607dfbdbdc 100644 --- a/test/utils/exportUtils/HTMLExport-test.ts +++ b/test/utils/exportUtils/HTMLExport-test.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,26 +14,101 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { mocked } from "jest-mock"; +import { EventType, IRoomEvent, MatrixClient, MatrixEvent, MsgType, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import fetchMock from "fetch-mock-jest"; -import { createTestClient, mkStubRoom, REPEATABLE_DATE } from "../../test-utils"; +import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils"; import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils"; import SdkConfig from "../../../src/SdkConfig"; import HTMLExporter from "../../../src/utils/exportUtils/HtmlExport"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; +import { mediaFromMxc } from "../../../src/customisations/Media"; + +jest.mock("jszip"); + +const EVENT_MESSAGE: IRoomEvent = { + event_id: "$1", + type: EventType.RoomMessage, + sender: "@bob:example.com", + origin_server_ts: 0, + content: { + msgtype: "m.text", + body: "Message", + avatar_url: "mxc://example.org/avatar.bmp", + }, +}; + +const EVENT_ATTACHMENT: IRoomEvent = { + event_id: "$2", + type: EventType.RoomMessage, + sender: "@alice:example.com", + origin_server_ts: 1, + content: { + msgtype: MsgType.File, + body: "hello.txt", + filename: "hello.txt", + url: "mxc://example.org/test-id", + }, +}; describe("HTMLExport", () => { + let client: jest.Mocked<MatrixClient>; + let room: Room; + + filterConsole( + "Starting export", + "events in", // Fetched # events in # seconds + "events so far", + "Export successful!", + "does not have an m.room.create event", + "Creating HTML", + "Generating a ZIP", + "Cleaning up", + ); + beforeEach(() => { jest.useFakeTimers(); jest.setSystemTime(REPEATABLE_DATE); + + client = stubClient() as jest.Mocked<MatrixClient>; + DMRoomMap.makeShared(); + + room = new Room("!myroom:example.org", client, "@me:example.org"); + client.getRoom.mockReturnValue(room); }); - afterEach(() => { - mocked(SdkConfig.get).mockRestore(); - }); + function mockMessages(...events: IRoomEvent[]): void { + client.createMessagesRequest.mockImplementation((_roomId, fromStr, limit = 30) => { + const from = fromStr === null ? 0 : parseInt(fromStr); + const chunk = events.slice(from, limit); + return Promise.resolve({ + chunk, + from: from.toString(), + to: (from + limit).toString(), + }); + }); + } + + /** Retrieve a map of files within the zip. */ + function getFiles(exporter: HTMLExporter): { [filename: string]: Blob } { + //@ts-ignore private access + const files = exporter.files; + return files.reduce((d, f) => ({ ...d, [f.name]: f.blob }), {}); + } + + function getMessageFile(exporter: HTMLExporter): Blob { + const files = getFiles(exporter); + return files["messages.html"]!; + } + + /** set a mock fetch response for an MXC */ + function mockMxc(mxc: string, body: string) { + const media = mediaFromMxc(mxc, client); + fetchMock.get(media.srcHttp, body); + } it("should have an SDK-branded destination file name", () => { const roomName = "My / Test / Room: Welcome"; - const client = createTestClient(); const stubOptions: IExportOptions = { attachmentsIncluded: false, maxSize: 50000000, @@ -43,10 +118,201 @@ describe("HTMLExport", () => { expect(exporter.destinationFileName).toMatchSnapshot(); - jest.spyOn(SdkConfig, "get").mockImplementation(() => { - return { brand: "BrandedChat/WithSlashes/ForFun" }; - }); + SdkConfig.put({ brand: "BrandedChat/WithSlashes/ForFun" }); expect(exporter.destinationFileName).toMatchSnapshot(); }); + + it("should export", async () => { + const events = [...Array(50)].map<IRoomEvent>((_, i) => ({ + event_id: `${i}`, + type: EventType.RoomMessage, + sender: `@user${i}:example.com`, + origin_server_ts: 5_000 + i * 1000, + content: { + msgtype: "m.text", + body: `Message #${i}`, + }, + })); + mockMessages(...events); + + const exporter = new HTMLExporter( + room, + ExportType.LastNMessages, + { + attachmentsIncluded: false, + maxSize: 1_024 * 1_024, + numberOfMessages: events.length, + }, + () => {}, + ); + + await exporter.export(); + + const file = getMessageFile(exporter); + expect(await file.text()).toMatchSnapshot(); + }); + + it("should include the room's avatar", async () => { + mockMessages(EVENT_MESSAGE); + + const mxc = "mxc://www.example.com/avatars/nice-room.jpeg"; + const avatar = "011011000110111101101100"; + jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(mxc); + mockMxc(mxc, avatar); + + const exporter = new HTMLExporter( + room, + ExportType.Timeline, + { + attachmentsIncluded: false, + maxSize: 1_024 * 1_024, + }, + () => {}, + ); + + await exporter.export(); + + const files = getFiles(exporter); + expect(await files["room.png"]!.text()).toBe(avatar); + }); + + it("should include the creation event", async () => { + const creator = "@bob:example.com"; + mockMessages(EVENT_MESSAGE); + room.currentState.setStateEvents([ + new MatrixEvent({ + type: EventType.RoomCreate, + event_id: "$00001", + room_id: room.roomId, + sender: creator, + origin_server_ts: 0, + content: {}, + state_key: "", + }), + ]); + + const exporter = new HTMLExporter( + room, + ExportType.Timeline, + { + attachmentsIncluded: false, + maxSize: 1_024 * 1_024, + }, + () => {}, + ); + + await exporter.export(); + + expect(await getMessageFile(exporter).text()).toContain(`${creator} created this room.`); + }); + + it("should include the topic", async () => { + const topic = ":^-) (-^:"; + mockMessages(EVENT_MESSAGE); + room.currentState.setStateEvents([ + new MatrixEvent({ + type: EventType.RoomTopic, + event_id: "$00001", + room_id: room.roomId, + sender: "@alice:example.com", + origin_server_ts: 0, + content: { topic }, + state_key: "", + }), + ]); + + const exporter = new HTMLExporter( + room, + ExportType.Timeline, + { + attachmentsIncluded: false, + maxSize: 1_024 * 1_024, + }, + () => {}, + ); + + await exporter.export(); + + expect(await getMessageFile(exporter).text()).toContain(`Topic: ${topic}`); + }); + + it("should include avatars", async () => { + mockMessages(EVENT_MESSAGE); + + jest.spyOn(RoomMember.prototype, "getMxcAvatarUrl").mockReturnValue("mxc://example.org/avatar.bmp"); + + const avatarContent = "this is a bitmap all the pixels are red :^-)"; + mockMxc("mxc://example.org/avatar.bmp", avatarContent); + + const exporter = new HTMLExporter( + room, + ExportType.Timeline, + { + attachmentsIncluded: false, + maxSize: 1_024 * 1_024, + }, + () => {}, + ); + + await exporter.export(); + + // Ensure that the avatar is present + const files = getFiles(exporter); + const file = files["users/@bob-example.com.png"]; + expect(file).not.toBeUndefined(); + + // Ensure it has the expected content + expect(await file.text()).toBe(avatarContent); + }); + + it("should include attachments", async () => { + mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT); + const attachmentBody = "Lorem ipsum dolor sit amet"; + + mockMxc("mxc://example.org/test-id", attachmentBody); + + const exporter = new HTMLExporter( + room, + ExportType.Timeline, + { + attachmentsIncluded: true, + maxSize: 1_024 * 1_024, + }, + () => {}, + ); + + await exporter.export(); + + // Ensure that the attachment is present + const files = getFiles(exporter); + const file = files["files/hello-1-1-1970 at 12-00-00 AM.txt"]; + expect(file).not.toBeUndefined(); + + // Ensure that the attachment has the expected content + const text = await file.text(); + expect(text).toBe(attachmentBody); + }); + + it("should omit attachments", async () => { + mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT); + + const exporter = new HTMLExporter( + room, + ExportType.Timeline, + { + attachmentsIncluded: false, + maxSize: 1_024 * 1_024, + }, + () => {}, + ); + + await exporter.export(); + + // Ensure that the attachment is present + const files = getFiles(exporter); + for (const fileName of Object.keys(files)) { + expect(fileName).not.toMatch(/^files\/hello/); + } + }); }); diff --git a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index 5cfed9ef90..c47170d3ed 100644 --- a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -1,5 +1,91 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`HTMLExport should export 1`] = ` +" + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link href="css/style.css" rel="stylesheet" /> + <script src="js/script.js"></script> + <title>Exported Data</title> + </head> + <body style="height: 100vh;"> + <section + id="matrixchat" + style="height: 100%; overflow: auto" + class="notranslate" + > + <div class="mx_MatrixChat_wrapper" aria-hidden="false"> + <div class="mx_MatrixChat"> + <main class="mx_RoomView"> + <div class="mx_RoomHeader light-panel"> + <div class="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel"> + <div class="mx_RoomHeader_avatar"> + <div class="mx_DecoratedRoomAvatar"> + <span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size:20.8px;width:32px;line-height:32px">!</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="!myroom:example.org" style="width:32px;height:32px" aria-hidden="true" data-testid="avatar-img"/></span> + </div> + </div> + <div class="mx_RoomHeader_name"> + <div + dir="auto" + class="mx_RoomHeader_nametext" + title="!myroom:example.org" + > + !myroom:example.org + </div> + </div> + <div class="mx_RoomHeader_topic" dir="auto"> </div> + </div> + </div> + <div class="mx_MainSplit"> + <div class="mx_RoomView_body"> + <div + class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled" + > + <div + class=" + mx_AutoHideScrollbar + mx_ScrollPanel + mx_RoomView_messagePanel + " + > + <div class="mx_RoomView_messageListWrapper"> + <ol + class="mx_RoomView_MessageList" + aria-live="polite" + role="list" + > + <div class="mx_NewRoomIntro"> + <span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size:20.8px;width:32px;line-height:32px">!</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="!myroom:example.org" style="width:32px;height:32px" aria-hidden="true" data-testid="avatar-img"/></span> + <h2> !myroom:example.org </h2> + <p> created this room. <br/><br/> <p><span>This is the start of export of <b>!myroom:example.org</b>. Exported by <a href="https://matrix.to/#/@userId:matrix.org" target="_blank" rel="noopener noreferrer"><b>@userId:matrix.org</b></a> at 2022/11/17.</span></p> </p> + <br/> + <p> </p> + </div> + <li><div class="mx_DateSeparator" role="separator" tabindex="-1" aria-label="Thu, Jan 1 1970"><hr role="none"/><h2 aria-hidden="true">Thu, Jan 1 1970</h2><hr role="none"/></div></li><div class="mx_Export_EventWrapper" id="49"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="49" data-layout="group" data-self="false" data-event-id="49" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user49:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user49:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/49" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:54" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #49</span></div></div></li></div><div class="mx_Export_EventWrapper" id="48"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="48" data-layout="group" data-self="false" data-event-id="48" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color7 mx_DisambiguatedProfile_displayName" dir="auto">@user48:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user48:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/48" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:53" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #48</span></div></div></li></div><div class="mx_Export_EventWrapper" id="47"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="47" data-layout="group" data-self="false" data-event-id="47" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color8 mx_DisambiguatedProfile_displayName" dir="auto">@user47:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user47:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/47" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:52" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #47</span></div></div></li></div><div class="mx_Export_EventWrapper" id="46"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="46" data-layout="group" data-self="false" data-event-id="46" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color1 mx_DisambiguatedProfile_displayName" dir="auto">@user46:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user46:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/46" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:51" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #46</span></div></div></li></div><div class="mx_Export_EventWrapper" id="45"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="45" data-layout="group" data-self="false" data-event-id="45" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color8 mx_DisambiguatedProfile_displayName" dir="auto">@user45:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user45:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/45" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:50" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #45</span></div></div></li></div><div class="mx_Export_EventWrapper" id="44"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="44" data-layout="group" data-self="false" data-event-id="44" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color7 mx_DisambiguatedProfile_displayName" dir="auto">@user44:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user44:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/44" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:49" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #44</span></div></div></li></div><div class="mx_Export_EventWrapper" id="43"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="43" data-layout="group" data-self="false" data-event-id="43" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user43:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user43:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/43" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:48" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #43</span></div></div></li></div><div class="mx_Export_EventWrapper" id="42"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="42" data-layout="group" data-self="false" data-event-id="42" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color5 mx_DisambiguatedProfile_displayName" dir="auto">@user42:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user42:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/42" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:47" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #42</span></div></div></li></div><div class="mx_Export_EventWrapper" id="41"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="41" data-layout="group" data-self="false" data-event-id="41" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color4 mx_DisambiguatedProfile_displayName" dir="auto">@user41:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user41:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/41" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:46" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #41</span></div></div></li></div><div class="mx_Export_EventWrapper" id="40"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="40" data-layout="group" data-self="false" data-event-id="40" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color3 mx_DisambiguatedProfile_displayName" dir="auto">@user40:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user40:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/40" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:45" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #40</span></div></div></li></div><div class="mx_Export_EventWrapper" id="39"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="39" data-layout="group" data-self="false" data-event-id="39" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color5 mx_DisambiguatedProfile_displayName" dir="auto">@user39:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user39:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/39" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:44" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #39</span></div></div></li></div><div class="mx_Export_EventWrapper" id="38"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="38" data-layout="group" data-self="false" data-event-id="38" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user38:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user38:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/38" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:43" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #38</span></div></div></li></div><div class="mx_Export_EventWrapper" id="37"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="37" data-layout="group" data-self="false" data-event-id="37" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color3 mx_DisambiguatedProfile_displayName" dir="auto">@user37:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user37:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/37" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:42" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #37</span></div></div></li></div><div class="mx_Export_EventWrapper" id="36"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="36" data-layout="group" data-self="false" data-event-id="36" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color2 mx_DisambiguatedProfile_displayName" dir="auto">@user36:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user36:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/36" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:41" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #36</span></div></div></li></div><div class="mx_Export_EventWrapper" id="35"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="35" data-layout="group" data-self="false" data-event-id="35" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color1 mx_DisambiguatedProfile_displayName" dir="auto">@user35:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user35:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/35" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:40" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #35</span></div></div></li></div><div class="mx_Export_EventWrapper" id="34"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="34" data-layout="group" data-self="false" data-event-id="34" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color8 mx_DisambiguatedProfile_displayName" dir="auto">@user34:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user34:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/34" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:39" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #34</span></div></div></li></div><div class="mx_Export_EventWrapper" id="33"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="33" data-layout="group" data-self="false" data-event-id="33" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color7 mx_DisambiguatedProfile_displayName" dir="auto">@user33:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user33:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/33" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:38" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #33</span></div></div></li></div><div class="mx_Export_EventWrapper" id="32"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="32" data-layout="group" data-self="false" data-event-id="32" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user32:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user32:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/32" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:37" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #32</span></div></div></li></div><div class="mx_Export_EventWrapper" id="31"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="31" data-layout="group" data-self="false" data-event-id="31" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color5 mx_DisambiguatedProfile_displayName" dir="auto">@user31:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user31:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/31" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:36" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #31</span></div></div></li></div><div class="mx_Export_EventWrapper" id="30"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="30" data-layout="group" data-self="false" data-event-id="30" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user30:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user30:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/30" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:35" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #30</span></div></div></li></div><div class="mx_Export_EventWrapper" id="29"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="29" data-layout="group" data-self="false" data-event-id="29" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user29:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user29:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/29" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:34" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #29</span></div></div></li></div><div class="mx_Export_EventWrapper" id="28"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="28" data-layout="group" data-self="false" data-event-id="28" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color5 mx_DisambiguatedProfile_displayName" dir="auto">@user28:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user28:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/28" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:33" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #28</span></div></div></li></div><div class="mx_Export_EventWrapper" id="27"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="27" data-layout="group" data-self="false" data-event-id="27" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color4 mx_DisambiguatedProfile_displayName" dir="auto">@user27:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user27:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/27" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:32" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #27</span></div></div></li></div><div class="mx_Export_EventWrapper" id="26"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="26" data-layout="group" data-self="false" data-event-id="26" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color3 mx_DisambiguatedProfile_displayName" dir="auto">@user26:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user26:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/26" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:31" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #26</span></div></div></li></div><div class="mx_Export_EventWrapper" id="25"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="25" data-layout="group" data-self="false" data-event-id="25" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color2 mx_DisambiguatedProfile_displayName" dir="auto">@user25:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user25:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/25" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:30" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #25</span></div></div></li></div><div class="mx_Export_EventWrapper" id="24"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="24" data-layout="group" data-self="false" data-event-id="24" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color1 mx_DisambiguatedProfile_displayName" dir="auto">@user24:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user24:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/24" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:29" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #24</span></div></div></li></div><div class="mx_Export_EventWrapper" id="23"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="23" data-layout="group" data-self="false" data-event-id="23" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color8 mx_DisambiguatedProfile_displayName" dir="auto">@user23:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user23:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/23" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:28" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #23</span></div></div></li></div><div class="mx_Export_EventWrapper" id="22"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="22" data-layout="group" data-self="false" data-event-id="22" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color7 mx_DisambiguatedProfile_displayName" dir="auto">@user22:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user22:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/22" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:27" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #22</span></div></div></li></div><div class="mx_Export_EventWrapper" id="21"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="21" data-layout="group" data-self="false" data-event-id="21" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color4 mx_DisambiguatedProfile_displayName" dir="auto">@user21:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user21:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/21" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:26" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #21</span></div></div></li></div><div class="mx_Export_EventWrapper" id="20"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="20" data-layout="group" data-self="false" data-event-id="20" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color5 mx_DisambiguatedProfile_displayName" dir="auto">@user20:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user20:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/20" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:25" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #20</span></div></div></li></div><div class="mx_Export_EventWrapper" id="19"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="19" data-layout="group" data-self="false" data-event-id="19" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color7 mx_DisambiguatedProfile_displayName" dir="auto">@user19:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user19:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/19" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:24" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #19</span></div></div></li></div><div class="mx_Export_EventWrapper" id="18"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="18" data-layout="group" data-self="false" data-event-id="18" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user18:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user18:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/18" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:23" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #18</span></div></div></li></div><div class="mx_Export_EventWrapper" id="17"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="17" data-layout="group" data-self="false" data-event-id="17" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color5 mx_DisambiguatedProfile_displayName" dir="auto">@user17:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user17:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/17" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:22" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #17</span></div></div></li></div><div class="mx_Export_EventWrapper" id="16"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="16" data-layout="group" data-self="false" data-event-id="16" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color4 mx_DisambiguatedProfile_displayName" dir="auto">@user16:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user16:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/16" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:21" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #16</span></div></div></li></div><div class="mx_Export_EventWrapper" id="15"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="15" data-layout="group" data-self="false" data-event-id="15" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color3 mx_DisambiguatedProfile_displayName" dir="auto">@user15:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user15:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/15" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:20" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #15</span></div></div></li></div><div class="mx_Export_EventWrapper" id="14"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="14" data-layout="group" data-self="false" data-event-id="14" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color2 mx_DisambiguatedProfile_displayName" dir="auto">@user14:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user14:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/14" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:19" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #14</span></div></div></li></div><div class="mx_Export_EventWrapper" id="13"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="13" data-layout="group" data-self="false" data-event-id="13" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color1 mx_DisambiguatedProfile_displayName" dir="auto">@user13:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user13:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/13" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:18" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #13</span></div></div></li></div><div class="mx_Export_EventWrapper" id="12"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="12" data-layout="group" data-self="false" data-event-id="12" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color2 mx_DisambiguatedProfile_displayName" dir="auto">@user12:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user12:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/12" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:17" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #12</span></div></div></li></div><div class="mx_Export_EventWrapper" id="11"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="11" data-layout="group" data-self="false" data-event-id="11" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color3 mx_DisambiguatedProfile_displayName" dir="auto">@user11:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user11:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/11" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:16" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #11</span></div></div></li></div><div class="mx_Export_EventWrapper" id="10"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="10" data-layout="group" data-self="false" data-event-id="10" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color4 mx_DisambiguatedProfile_displayName" dir="auto">@user10:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user10:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/10" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:15" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #10</span></div></div></li></div><div class="mx_Export_EventWrapper" id="9"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="9" data-layout="group" data-self="false" data-event-id="9" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color8 mx_DisambiguatedProfile_displayName" dir="auto">@user9:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user9:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/9" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:14" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #9</span></div></div></li></div><div class="mx_Export_EventWrapper" id="8"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="8" data-layout="group" data-self="false" data-event-id="8" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color1 mx_DisambiguatedProfile_displayName" dir="auto">@user8:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user8:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/8" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:13" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #8</span></div></div></li></div><div class="mx_Export_EventWrapper" id="7"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="7" data-layout="group" data-self="false" data-event-id="7" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color2 mx_DisambiguatedProfile_displayName" dir="auto">@user7:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user7:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/7" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:12" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #7</span></div></div></li></div><div class="mx_Export_EventWrapper" id="6"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="6" data-layout="group" data-self="false" data-event-id="6" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color3 mx_DisambiguatedProfile_displayName" dir="auto">@user6:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user6:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/6" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:11" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #6</span></div></div></li></div><div class="mx_Export_EventWrapper" id="5"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="5" data-layout="group" data-self="false" data-event-id="5" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color6 mx_DisambiguatedProfile_displayName" dir="auto">@user5:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user5:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/5" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:10" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #5</span></div></div></li></div><div class="mx_Export_EventWrapper" id="4"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="4" data-layout="group" data-self="false" data-event-id="4" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color5 mx_DisambiguatedProfile_displayName" dir="auto">@user4:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user4:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/4" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:09" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #4</span></div></div></li></div><div class="mx_Export_EventWrapper" id="3"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="3" data-layout="group" data-self="false" data-event-id="3" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color4 mx_DisambiguatedProfile_displayName" dir="auto">@user3:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user3:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/3" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:08" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #3</span></div></div></li></div><div class="mx_Export_EventWrapper" id="2"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="2" data-layout="group" data-self="false" data-event-id="2" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color3 mx_DisambiguatedProfile_displayName" dir="auto">@user2:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user2:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/2" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:07" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #2</span></div></div></li></div><div class="mx_Export_EventWrapper" id="1"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="1" data-layout="group" data-self="false" data-event-id="1" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color2 mx_DisambiguatedProfile_displayName" dir="auto">@user1:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user1:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/1" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:06" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #1</span></div></div></li></div><div class="mx_Export_EventWrapper" id="0"><li class="mx_EventTile" tabindex="-1" aria-live="off" aria-atomic="true" data-scroll-tokens="0" data-layout="group" data-self="false" data-event-id="0" data-has-reply="false"><div class="mx_DisambiguatedProfile"><span class="mx_Username_color1 mx_DisambiguatedProfile_displayName" dir="auto">@user0:example.com</span></div><div class="mx_EventTile_avatar"><span aria-label="Avatar" aria-live="off" role="button" tabindex="0" class="mx_AccessibleButton mx_BaseAvatar"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size: 19.5px; width: 30px; line-height: 30px;">U</span><img class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="@user0:example.com" style="width: 30px; height: 30px;" aria-hidden="true" data-testid="avatar-img"></span></div><div class="mx_EventTile_line"><a href="https://matrix.to/#/!myroom:example.org/0" aria-label="00:00"><span class="mx_MessageTimestamp" title="Thu, Jan 1 1970 00:00:05" aria-hidden="true">00:00</span></a><div class="mx_MTextBody mx_EventTile_content"><span class="mx_EventTile_body" dir="auto">Message #0</span></div></div></li></div> + </ol> + </div> + </div> + </div> + <div class="mx_RoomView_statusArea"> + <div class="mx_RoomView_statusAreaBox"> + <div class="mx_RoomView_statusAreaBox_line"></div> + </div> + </div> + </div> + </div> + </main> + </div> + </div> + </section> + <div id="snackbar"/> + </body> + </html>" +`; + exports[`HTMLExport should have an SDK-branded destination file name 1`] = `"Element - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.zip"`; exports[`HTMLExport should have an SDK-branded destination file name 2`] = `"BrandedChatWithSlashesForFun - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.zip"`; diff --git a/test/utils/exportUtils/exportCSS-test.ts b/test/utils/exportUtils/exportCSS-test.ts new file mode 100644 index 0000000000..d2f3f14fbb --- /dev/null +++ b/test/utils/exportUtils/exportCSS-test.ts @@ -0,0 +1,26 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import getExportCSS from "../../../src/utils/exportUtils/exportCSS"; + +describe("exportCSS", () => { + describe("getExportCSS", () => { + it("supports documents missing stylesheets", async () => { + const css = await getExportCSS(new Set()); + expect(css).not.toContain("color-scheme: light"); + }); + }); +});