diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts index 44bb7b6a88..177735f8d9 100644 --- a/src/utils/exportUtils/Exporter.ts +++ b/src/utils/exportUtils/Exporter.ts @@ -1,11 +1,10 @@ -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { exportTypes } from "./exportUtils"; -import { exportOptions } from "./exportUtils"; -import { decryptFile } from "../DecryptFile"; -import { mediaFromContent } from "../../customisations/Media"; -import { formatFullDateNoDay } from "../../DateUtils"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixClientPeg} from "../../MatrixClientPeg"; +import {exportOptions, exportTypes} from "./exportUtils"; +import {decryptFile} from "../DecryptFile"; +import {mediaFromContent} from "../../customisations/Media"; +import {formatFullDateNoDay} from "../../DateUtils"; export default abstract class Exporter { protected constructor( @@ -141,6 +140,11 @@ export default abstract class Exporter { return fileDirectory + "/" + fileName + '-' + fileDate + fileExt; } + protected isReply(mxEvent) { + const relatesTo = mxEvent.getContent()["m.relates_to"]; + return !!(relatesTo && relatesTo["m.in_reply_to"]); + } + protected isAttachment(mxEv: MatrixEvent) { const attachmentTypes = ["m.sticker", "m.image", "m.file", "m.video", "m.audio"]; return mxEv.getType() === attachmentTypes[0] || attachmentTypes.includes(mxEv.getContent().msgtype); diff --git a/src/utils/exportUtils/PlainTextExport.ts b/src/utils/exportUtils/PlainTextExport.ts index 6d29a4bb6d..72ab0f122f 100644 --- a/src/utils/exportUtils/PlainTextExport.ts +++ b/src/utils/exportUtils/PlainTextExport.ts @@ -28,11 +28,49 @@ export default class PlainTextExporter extends Exporter { return e.returnValue = "Are you sure you want to exit during this export?"; } + protected textForReplyEvent = (ev : MatrixEvent) => { + const REPLY_REGEX = /> <(.*?)>(.*?)\n\n(.*)/; + const REPLY_SOURCE_MAX_LENGTH = 32; + const content = ev.getContent(); + + const match = REPLY_REGEX.exec(content.body); + + let rplSource: string; + const rplName = match[1]; + const rplText = match[3]; + const sourceMatch = REPLY_REGEX.exec(content.body); + rplSource = sourceMatch && sourceMatch.length === 4 ? sourceMatch[3] : content.body; + rplSource = rplSource.substring(0, REPLY_SOURCE_MAX_LENGTH); + // Get the first non-blank line from the source. + const lines = rplSource.split('\n').filter((line) => !/^\s*$/.test(line)) + if (lines.length > 0) { + // Cut to a maximum length. + rplSource = lines[0].substring(0, REPLY_SOURCE_MAX_LENGTH); + // Ellipsis if needed. + if (lines[0].length > REPLY_SOURCE_MAX_LENGTH) { + rplSource = rplSource + "..."; + } + // Wrap in formatting + rplSource = ` "${rplSource}"`; + } else { + // Don't show a source because we couldn't format one. + rplSource = ""; + } + + return `<${rplName}${rplSource}> ${rplText}`; + } + + protected _textForEvent = (mxEv: MatrixEvent) => { + const senderDisplayName = mxEv.sender && mxEv.sender.name ? mxEv.sender.name : mxEv.getSender(); + if (this.isReply(mxEv)) return senderDisplayName + ": " + this.textForReplyEvent(mxEv); + else return textForEvent(mxEv); + } + protected async createOutput(events: MatrixEvent[]) { let content = ""; for (const event of events) { if (!haveTileForEvent(event)) continue; - content += `${new Date(event.getTs()).toLocaleString()} - ${textForEvent(event)}\n`; + content += `${new Date(event.getTs()).toLocaleString()} - ${this._textForEvent(event)}\n`; } return content; }