/* Copyright 2024 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import React, { type JSX, useRef, useState, type Dispatch, type SetStateAction } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; import Field from "../elements/Field"; import StyledRadioGroup from "../elements/StyledRadioGroup"; import StyledCheckbox from "../elements/StyledCheckbox"; import { ExportFormat, type ExportFormatKey, ExportType, type ExportTypeKey, textForFormat, textForType, } from "../../../utils/exportUtils/exportUtils"; import withValidation, { type IFieldState, type 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"; import type Exporter from "../../../utils/exportUtils/Exporter"; import Spinner from "../elements/Spinner"; import InfoDialog from "./InfoDialog"; import ChatExport from "../../../customisations/ChatExport"; import { validateNumberInRange } from "../../../utils/validate"; interface IProps { room: Room; onFinished(this: void, doExport?: boolean): void; } interface ExportConfig { exportFormat: ExportFormat; exportType: ExportType; numberOfMessages: number; sizeLimit: number; includeAttachments: boolean; setExportFormat?: Dispatch>; setExportType?: Dispatch>; setAttachments?: Dispatch>; setNumberOfMessages?: Dispatch>; setSizeLimit?: Dispatch>; } /** * Set up form state using "forceRoomExportParameters" or defaults * Form fields configured in ForceRoomExportParameters are not allowed to be edited * Only return change handlers for editable values */ const useExportFormState = (): ExportConfig => { const config = ChatExport.getForceChatExportParameters(); const [exportFormat, setExportFormat] = useState(config.format ?? ExportFormat.Html); const [exportType, setExportType] = useState(config.range ?? ExportType.Timeline); const [includeAttachments, setAttachments] = useState(config.includeAttachments ?? false); const [numberOfMessages, setNumberOfMessages] = useState(config.numberOfMessages ?? 100); const [sizeLimit, setSizeLimit] = useState(config.sizeMb ?? 8); return { exportFormat, exportType, includeAttachments, numberOfMessages, sizeLimit, setExportFormat: !config.format ? setExportFormat : undefined, setExportType: !config.range ? setExportType : undefined, setNumberOfMessages: !config.numberOfMessages ? setNumberOfMessages : undefined, setSizeLimit: !config.sizeMb ? setSizeLimit : undefined, setAttachments: config.includeAttachments === undefined ? setAttachments : undefined, }; }; const ExportDialog: React.FC = ({ room, onFinished }) => { const { exportFormat, exportType, includeAttachments, numberOfMessages, sizeLimit, setExportFormat, setExportType, setNumberOfMessages, setSizeLimit, setAttachments, } = useExportFormState(); const [isExporting, setExporting] = useState(false); const sizeLimitRef = useRef(null); const messageCountRef = useRef(null); const [exportProgressText, setExportProgressText] = useState(_t("export_chat|processing")); const [displayCancel, setCancelWarning] = useState(false); const [exportCancelled, setExportCancelled] = useState(false); const [exportSuccessful, setExportSuccessful] = useState(false); const [exporter, setExporter] = useStateCallback( null, async (exporter: Exporter | null): Promise => { await exporter?.export().then(() => { if (!exportCancelled) setExportSuccessful(true); }); }, ); const startExport = async (): Promise => { const exportOptions = { numberOfMessages, attachmentsIncluded: includeAttachments, maxSize: sizeLimit * 1024 * 1024, }; switch (exportFormat) { case ExportFormat.Html: setExporter(new HTMLExporter(room, ExportType[exportType], exportOptions, setExportProgressText)); break; case ExportFormat.Json: setExporter(new JSONExporter(room, ExportType[exportType], exportOptions, setExportProgressText)); break; case ExportFormat.PlainText: setExporter(new PlainTextExporter(room, ExportType[exportType], exportOptions, setExportProgressText)); break; default: logger.error("Unknown export format"); return; } }; const onExportClick = async (): Promise => { const isValidSize = !setSizeLimit || (await sizeLimitRef.current?.validate({ focused: false, })); if (!isValidSize) { sizeLimitRef.current?.validate({ focused: true }); return; } if (exportType === ExportType.LastNMessages) { const isValidNumberOfMessages = await messageCountRef.current?.validate({ focused: false }); if (!isValidNumberOfMessages) { messageCountRef.current?.validate({ focused: true }); return; } } setExporting(true); await startExport(); }; const validateSize = withValidation({ rules: [ { key: "required", test({ value, allowEmpty }) { return allowEmpty || !!value; }, invalid: () => { const min = 1; const max = 2000; return _t("export_chat|enter_number_between_min_max", { min, max, }); }, }, { key: "number", test: ({ value }) => { const parsedSize = parseInt(value!, 10); return validateNumberInRange(1, 2000)(parsedSize); }, invalid: () => { const min = 1; const max = 2000; return _t("export_chat|size_limit_min_max", { min, max }); }, }, ], }); const onValidateSize = async (fieldState: IFieldState): Promise => { const result = await validateSize(fieldState); return result; }; const validateNumberOfMessages = withValidation({ rules: [ { key: "required", test({ value, allowEmpty }) { return allowEmpty || !!value; }, invalid: () => { const min = 1; const max = 10 ** 8; return _t("export_chat|enter_number_between_min_max", { min, max, }); }, }, { key: "number", test: ({ value }) => { const parsedSize = parseInt(value!, 10); return validateNumberInRange(1, 10 ** 8)(parsedSize); }, invalid: () => { const min = 1; const max = 10 ** 8; return _t("export_chat|num_messages_min_max", { min, max }); }, }, ], }); const onValidateNumberOfMessages = async (fieldState: IFieldState): Promise => { const result = await validateNumberOfMessages(fieldState); return result; }; const onCancel = async (): Promise => { if (isExporting) setCancelWarning(true); else onFinished(false); }; const confirmCancel = async (): Promise => { await exporter?.cancelExport(); setExportCancelled(true); setExporting(false); setExporter(null); }; const exportFormatOptions = Object.values(ExportFormat).map((format) => ({ value: format, label: textForFormat(format), })); const exportTypeOptions = Object.values(ExportType).map((type) => { return ( ); }); let messageCount: JSX.Element | undefined; if (exportType === ExportType.LastNMessages && setNumberOfMessages) { messageCount = ( { setNumberOfMessages(parseInt(e.target.value)); }} /> ); } const sizePostFix = {_t("export_chat|size_limit_postfix")}; if (exportCancelled) { // Display successful cancellation message return ( ); } else if (exportSuccessful) { // Display successful export message return ( ); } else if (displayCancel) { // Display cancel warning return (

{_t("export_chat|confirm_stop")}

setCancelWarning(false)} onPrimaryButtonClick={confirmCancel} />
); } else { // Display export settings return ( {!isExporting ?

{_t("export_chat|select_option")}

: null}
{!!setExportFormat && ( <> {_t("export_chat|format")} setExportFormat(ExportFormat[key])} definitions={exportFormatOptions} /> )} {!!setExportType && ( <> {_t("export_chat|messages")} { setExportType(ExportType[e.target.value as ExportTypeKey]); }} > {exportTypeOptions} {messageCount} )} {setSizeLimit && ( <> {_t("export_chat|size_limit")} setSizeLimit(parseInt(e.target.value))} /> )} {setAttachments && ( <> setAttachments((e.target as HTMLInputElement).checked)} > {_t("export_chat|include_attachments")} )}
{isExporting ? (

{exportProgressText}

) : ( onFinished(false)} /> )}
); } }; export default ExportDialog;