diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index c4eb031aa5..ed567c43e1 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -7,13 +7,17 @@ import DialogButtons from "../elements/DialogButtons"; import Field from "../elements/Field"; import StyledRadioGroup from "../elements/StyledRadioGroup"; import StyledCheckbox from "../elements/StyledCheckbox"; -import exportConversationalHistory, { +import { exportFormats, exportTypes, textForFormat, textForType, } from "../../../utils/exportUtils/exportUtils"; import { IFieldState, IValidationResult } from "../elements/Validation"; +import HTMLExporter from "../../../utils/exportUtils/HtmlExport"; +import JSONExporter from "../../../utils/exportUtils/JSONExport"; +import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport"; +import { useStateCallback } from "../../../hooks/useStateCallback"; interface IProps extends IDialogProps { room: Room; @@ -26,6 +30,52 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { const [numberOfMessages, setNumberOfMessages] = useState(100); const [sizeLimit, setSizeLimit] = useState(8); const [sizeLimitRef, messageCountRef] = [useRef(), useRef()]; + const [Exporter, setExporter] = useStateCallback( + null, + async (Exporter: HTMLExporter | PlainTextExporter | JSONExporter) => { + await Exporter?.export(); + }, + ); + + const startExport = async () => { + const exportOptions = { + numberOfMessages, + attachmentsIncluded: includeAttachments, + maxSize: sizeLimit * 1024 * 1024, + }; + switch (exportFormat) { + case exportFormats.HTML: + setExporter( + new HTMLExporter( + room, + exportTypes[exportType], + exportOptions, + ), + ); + break; + case exportFormats.JSON: + setExporter( + new JSONExporter( + room, + exportTypes[exportType], + exportOptions, + ), + ); + break; + case exportFormats.PLAIN_TEXT: + setExporter( + new PlainTextExporter( + room, + exportTypes[exportType], + exportOptions, + ), + ); + break; + default: + console.error("Unknown export format"); + return; + } + }; const onExportClick = async () => { const isValidSize = await sizeLimitRef.current.validate({ @@ -43,16 +93,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { return; } } - await exportConversationalHistory( - room, - exportFormats[exportFormat], - exportTypes[exportType], - { - numberOfMessages, - attachmentsIncluded: includeAttachments, - maxSize: sizeLimit * 1024 * 1024, - }, - ); + await startExport(); }; const onValidateSize = async ({ @@ -118,7 +159,8 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { }; }; - const onCancel = () => { + const onCancel = async () => { + await Exporter?.cancelExport(); onFinished(false); }; diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts index 0834cb7dc8..de9aba0ff7 100644 --- a/src/utils/exportUtils/Exporter.ts +++ b/src/utils/exportUtils/Exporter.ts @@ -21,16 +21,18 @@ export default abstract class Exporter { protected client: MatrixClient; protected writer: WritableStreamDefaultWriter; protected fileStream: WritableStream; + protected cancelled: boolean; protected constructor( protected room: Room, protected exportType: exportTypes, protected exportOptions?: exportOptions, ) { + this.cancelled = false; this.files = []; this.client = MatrixClientPeg.get(); window.addEventListener("beforeunload", this.onBeforeUnload); - window.addEventListener("onunload", this.abortExport); + window.addEventListener("onunload", this.abortWriter); } protected onBeforeUnload(e: BeforeUnloadEvent) { @@ -55,7 +57,8 @@ export default abstract class Exporter { // Create a writable stream to the directory this.fileStream = streamSaver.createWriteStream(filename); - console.info("Generating a ZIP..."); + if (!this.cancelled) console.info("Generating a ZIP..."); + else return this.cleanUp(); this.writer = this.fileStream.getWriter(); const files = this.files; @@ -67,21 +70,37 @@ export default abstract class Exporter { }, }); + if (this.cancelled) return this.cleanUp(); + console.info("Writing to the file system...") const reader = readableZipStream.getReader() await this.pumpToFileStream(reader); } + protected cleanUp() { + console.log("Cleaning up..."); + window.removeEventListener("beforeunload", this.onBeforeUnload); + window.removeEventListener("onunload", this.abortWriter); + return ""; + } + + public async cancelExport() { + console.log("Cancelling export..."); + this.cancelled = true; + await this.abortWriter(); + } + protected async downloadPlainText(fileName: string, text: string): Promise { this.fileStream = streamSaver.createWriteStream(fileName); this.writer = this.fileStream.getWriter() const data = new TextEncoder().encode(text); + if (this.cancelled) return this.cleanUp(); await this.writer.write(data); await this.writer.close(); } - protected async abortExport(): Promise { + protected async abortWriter(): Promise { await this.fileStream?.abort(); await this.writer?.abort(); } @@ -134,6 +153,11 @@ export default abstract class Exporter { const eventsPerCrawl = Math.min(limit, 1000); const res: any = await this.client.createMessagesRequest(this.room.roomId, prevToken, eventsPerCrawl, "b"); + if (this.cancelled) { + this.cleanUp(); + return []; + } + if (res.chunk.length === 0) break; limit -= res.chunk.length; diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index f69cb4fb7f..a62f926097 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -314,7 +314,7 @@ export default class HTMLExporter extends Exporter { let prevEvent = null; for (let i = 0; i < events.length; i++) { const event = events[i]; - console.log("Processing event " + i + " out of " + events.length); + if (this.cancelled) return this.cleanUp(); if (!haveTileForEvent(event)) continue; content += this._wantsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : ""; @@ -349,17 +349,18 @@ export default class HTMLExporter extends Exporter { this.addFile(`icons/${iconName}`, new Blob([exportIcons[iconName]])); } - console.info("HTML creation successful!"); - await this.downloadZIP(); const exportEnd = performance.now(); - console.info("Export successful!") - console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + if (this.cancelled) { + console.info("Export cancelled successfully"); + } else { + console.info("Export successful!") + console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + } - window.removeEventListener("beforeunload", this.onBeforeUnload); - window.removeEventListener("onunload", this.abortExport); + this.cleanUp(); } } diff --git a/src/utils/exportUtils/JSONExport.ts b/src/utils/exportUtils/JSONExport.ts index d25d96b2e4..cbdc78a711 100644 --- a/src/utils/exportUtils/JSONExport.ts +++ b/src/utils/exportUtils/JSONExport.ts @@ -64,6 +64,7 @@ ${json} protected async createOutput(events: MatrixEvent[]) { let content = ""; for (const event of events) { + if (this.cancelled) return this.cleanUp(); if (!haveTileForEvent(event)) continue; content += await this.getJSONString(event); } @@ -93,11 +94,14 @@ ${json} const exportEnd = performance.now(); - console.info("Export successful!") - console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + if (this.cancelled) { + console.info("Export cancelled successfully"); + } else { + console.info("Export successful!") + console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + } - window.removeEventListener("beforeunload", this.onBeforeUnload); - window.removeEventListener("onunload", this.abortExport); + this.cleanUp() } } diff --git a/src/utils/exportUtils/PlainTextExport.ts b/src/utils/exportUtils/PlainTextExport.ts index a207029955..dff4399b95 100644 --- a/src/utils/exportUtils/PlainTextExport.ts +++ b/src/utils/exportUtils/PlainTextExport.ts @@ -81,6 +81,7 @@ export default class PlainTextExporter extends Exporter { protected async createOutput(events: MatrixEvent[]) { let content = ""; for (const event of events) { + if (this.cancelled) return this.cleanUp(); if (!haveTileForEvent(event)) continue; const textForEvent = await this._textForEvent(event); content += textForEvent && `${new Date(event.getTs()).toLocaleString()} - ${textForEvent}\n`; @@ -101,6 +102,8 @@ export default class PlainTextExporter extends Exporter { console.info("Creating output..."); const text = await this.createOutput(res); + if (this.cancelled) return this.cleanUp(); + if (this.files.length) { this.addFile("export.txt", new Blob([text])); await this.downloadZIP(); @@ -111,11 +114,14 @@ export default class PlainTextExporter extends Exporter { const exportEnd = performance.now(); - console.info("Export successful!") - console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + if (this.cancelled) { + console.info("Export cancelled successfully"); + } else { + console.info("Export successful!") + console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); + } - window.removeEventListener("onunload", this.abortExport); - window.removeEventListener("beforeunload", this.onBeforeUnload); + this.cleanUp(); } } diff --git a/src/utils/exportUtils/exportUtils.ts b/src/utils/exportUtils/exportUtils.ts index 132783fdcc..2baff69b36 100644 --- a/src/utils/exportUtils/exportUtils.ts +++ b/src/utils/exportUtils/exportUtils.ts @@ -1,8 +1,4 @@ -import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../languageHandler"; -import HTMLExporter from "./HtmlExport"; -import JSONExporter from "./JSONExport"; -import PlainTextExporter from "./PlainTextExport"; export enum exportFormats { HTML = "HTML", @@ -48,23 +44,3 @@ export interface exportOptions { maxSize: number; } -const exportConversationalHistory = async ( - room: Room, - format: string, - exportType: exportTypes, - exportOptions?: exportOptions, -) => { - switch (format) { - case exportFormats.HTML: - await new HTMLExporter(room, exportType, exportOptions).export(); - break; - case exportFormats.JSON: - await new JSONExporter(room, exportType, exportOptions).export(); - break; - case exportFormats.PLAIN_TEXT: - await new PlainTextExporter(room, exportType, exportOptions).export(); - break; - } -}; - -export default exportConversationalHistory;