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 { // 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 { - if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { + if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) { nodes.push(
  • 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 { // 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 { 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 { - 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 { `; } - 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 { - 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 { + protected getDateSeparator(event: MatrixEvent): string { const ts = event.getTs(); const dateSeparator = (
  • - +
  • ); return renderToStaticMarkup(dateSeparator); } - protected async needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent): Promise { - 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 { - 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>/, ""); 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): Promise => { // If the light theme isn't loaded we will have to fetch & parse it manually if (!stylesheets.some(isLightTheme)) { - const href = document.querySelector('link[rel="stylesheet"][href$="theme-light.css"]').href; - stylesheets.push(await getRulesFromCssFile(href)); + const href = document.querySelector('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("", () => { + const roomId = "!aroom:example.com"; + let client: jest.Mocked; + let event: MatrixEvent; + + beforeEach(() => { + client = stubClient() as jest.Mocked; + event = mkMessage({ + event: true, + user: "@user:example.com", + room: "!room:example.com", + msg: "My Great Message", + }); + }); + + async function renderComponent(): Promise { + const result = render(); + 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[` should match the snapshot 1`] = ` +
    +
    +