Files
ThreadNet-Web/apps/web/src/components/views/dialogs/ExportDialog.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

416 lines
15 KiB
TypeScript
Raw Normal View History

2021-07-26 23:40:27 +05:30
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
2021-07-26 23:40:27 +05:30
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
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2021-07-26 23:40:27 +05:30
*/
import React, { type JSX, useRef, useState, type Dispatch, type SetStateAction } from "react";
2025-02-05 13:25:06 +00:00
import { type Room } from "matrix-js-sdk/src/matrix";
2021-10-22 17:23:32 -05:00
import { logger } from "matrix-js-sdk/src/logger";
2021-06-26 13:04:10 +05:30
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";
2023-02-13 11:39:16 +00:00
import {
ExportFormat,
2025-02-05 13:25:06 +00:00
type ExportFormatKey,
2023-02-13 11:39:16 +00:00
ExportType,
2025-02-05 13:25:06 +00:00
type ExportTypeKey,
2023-02-13 11:39:16 +00:00
textForFormat,
textForType,
} from "../../../utils/exportUtils/exportUtils";
2025-02-05 13:25:06 +00:00
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
2021-06-27 20:55:54 +05:30
import HTMLExporter from "../../../utils/exportUtils/HtmlExport";
import JSONExporter from "../../../utils/exportUtils/JSONExport";
import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport";
import { useStateCallback } from "../../../hooks/useStateCallback";
2025-02-05 13:25:06 +00:00
import type Exporter from "../../../utils/exportUtils/Exporter";
import Spinner from "../elements/Spinner";
import InfoDialog from "./InfoDialog";
2022-01-31 12:54:14 +01:00
import ChatExport from "../../../customisations/ChatExport";
import { validateNumberInRange } from "../../../utils/validate";
interface IProps {
room: Room;
onFinished(this: void, doExport?: boolean): void;
}
2022-01-31 12:54:14 +01:00
interface ExportConfig {
exportFormat: ExportFormat;
exportType: ExportType;
numberOfMessages: number;
sizeLimit: number;
includeAttachments: boolean;
setExportFormat?: Dispatch<SetStateAction<ExportFormat>>;
setExportType?: Dispatch<SetStateAction<ExportType>>;
setAttachments?: Dispatch<SetStateAction<boolean>>;
setNumberOfMessages?: Dispatch<SetStateAction<number>>;
setSizeLimit?: Dispatch<SetStateAction<number>>;
}
/**
* 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<number>(config.numberOfMessages ?? 100);
const [sizeLimit, setSizeLimit] = useState<number>(config.sizeMb ?? 8);
2022-01-31 12:54:14 +01:00
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,
};
};
2021-06-26 13:04:10 +05:30
const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
2022-01-31 12:54:14 +01:00
const {
exportFormat,
exportType,
includeAttachments,
numberOfMessages,
sizeLimit,
setExportFormat,
setExportType,
setNumberOfMessages,
setSizeLimit,
setAttachments,
} = useExportFormState();
2021-06-27 22:18:43 +05:30
const [isExporting, setExporting] = useState(false);
const sizeLimitRef = useRef<Field>(null);
const messageCountRef = useRef<Field>(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<Exporter | null>(
null,
async (exporter: Exporter | null): Promise<void> => {
await exporter?.export().then(() => {
if (!exportCancelled) setExportSuccessful(true);
});
},
);
2021-06-27 20:55:54 +05:30
const startExport = async (): Promise<void> => {
2021-06-27 20:55:54 +05:30
const exportOptions = {
numberOfMessages,
attachmentsIncluded: includeAttachments,
maxSize: sizeLimit * 1024 * 1024,
};
switch (exportFormat) {
2021-08-13 08:30:50 +05:30
case ExportFormat.Html:
setExporter(new HTMLExporter(room, ExportType[exportType], exportOptions, setExportProgressText));
2021-06-27 20:55:54 +05:30
break;
2021-08-13 08:30:50 +05:30
case ExportFormat.Json:
setExporter(new JSONExporter(room, ExportType[exportType], exportOptions, setExportProgressText));
2021-06-27 20:55:54 +05:30
break;
2021-08-13 08:30:50 +05:30
case ExportFormat.PlainText:
setExporter(new PlainTextExporter(room, ExportType[exportType], exportOptions, setExportProgressText));
2021-06-27 20:55:54 +05:30
break;
default:
2021-10-15 16:30:53 +02:00
logger.error("Unknown export format");
2021-06-27 20:55:54 +05:30
return;
}
};
const onExportClick = async (): Promise<void> => {
2022-01-31 12:54:14 +01:00
const isValidSize =
!setSizeLimit ||
(await sizeLimitRef.current?.validate({
2021-06-26 23:07:38 +05:30
focused: false,
2022-01-31 12:54:14 +01:00
}));
2021-06-26 23:07:38 +05:30
if (!isValidSize) {
sizeLimitRef.current?.validate({ focused: true });
2021-06-26 23:07:38 +05:30
return;
}
2021-08-13 08:30:50 +05:30
if (exportType === ExportType.LastNMessages) {
const isValidNumberOfMessages = await messageCountRef.current?.validate({ focused: false });
2021-06-26 23:07:38 +05:30
if (!isValidNumberOfMessages) {
messageCountRef.current?.validate({ focused: true });
2021-06-26 23:07:38 +05:30
return;
}
}
2021-06-27 22:18:43 +05:30
setExporting(true);
2021-06-27 20:55:54 +05:30
await startExport();
};
2021-08-13 23:44:07 +05:30
const validateSize = withValidation({
rules: [
{
key: "required",
test({ value, allowEmpty }) {
return allowEmpty || !!value;
},
invalid: () => {
const min = 1;
2022-01-27 09:55:08 +01:00
const max = 2000;
return _t("export_chat|enter_number_between_min_max", {
2021-08-13 23:44:07 +05:30
min,
max,
});
},
},
{
key: "number",
test: ({ value }) => {
const parsedSize = parseInt(value!, 10);
2022-01-31 12:54:14 +01:00
return validateNumberInRange(1, 2000)(parsedSize);
2021-08-13 23:44:07 +05:30
},
invalid: () => {
const min = 1;
const max = 2000;
return _t("export_chat|size_limit_min_max", { min, max });
2021-08-13 23:44:07 +05:30
},
},
],
});
2021-06-26 23:07:38 +05:30
2021-08-13 23:44:07 +05:30
const onValidateSize = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await validateSize(fieldState);
return result;
2021-06-26 23:07:38 +05:30
};
2021-08-13 23:44:07 +05:30
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", {
2021-08-13 23:44:07 +05:30
min,
max,
});
},
},
{
key: "number",
test: ({ value }) => {
const parsedSize = parseInt(value!, 10);
2022-01-31 12:54:14 +01:00
return validateNumberInRange(1, 10 ** 8)(parsedSize);
2021-08-13 23:44:07 +05:30
},
invalid: () => {
const min = 1;
const max = 10 ** 8;
return _t("export_chat|num_messages_min_max", { min, max });
2021-08-13 23:44:07 +05:30
},
},
],
});
2021-06-26 23:07:38 +05:30
2021-08-13 23:44:07 +05:30
const onValidateNumberOfMessages = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await validateNumberOfMessages(fieldState);
return result;
2021-06-26 23:07:38 +05:30
};
const onCancel = async (): Promise<void> => {
if (isExporting) setCancelWarning(true);
else onFinished(false);
};
const confirmCancel = async (): Promise<void> => {
await exporter?.cancelExport();
setExportCancelled(true);
setExporting(false);
setExporter(null);
};
2023-02-13 11:39:16 +00:00
const exportFormatOptions = Object.values(ExportFormat).map((format) => ({
value: format,
label: textForFormat(format),
}));
2023-02-13 11:39:16 +00:00
const exportTypeOptions = Object.values(ExportType).map((type) => {
return (
2023-02-13 11:39:16 +00:00
<option key={ExportType[type]} value={type}>
{textForType(type)}
</option>
);
});
2021-07-26 00:09:59 +05:30
let messageCount: JSX.Element | undefined;
2022-01-31 12:54:14 +01:00
if (exportType === ExportType.LastNMessages && setNumberOfMessages) {
2021-06-29 10:36:24 +05:30
messageCount = (
<Field
2022-01-27 09:55:08 +01:00
id="message-count"
element="input"
2021-06-26 23:07:38 +05:30
type="number"
value={numberOfMessages.toString()}
2021-06-26 23:07:38 +05:30
ref={messageCountRef}
onValidate={onValidateNumberOfMessages}
label={_t("export_chat|num_messages")}
onChange={(e) => {
setNumberOfMessages(parseInt(e.target.value));
}}
/>
);
}
const sizePostFix = <span>{_t("export_chat|size_limit_postfix")}</span>;
2021-06-26 13:04:10 +05:30
2021-06-29 10:36:24 +05:30
if (exportCancelled) {
// Display successful cancellation message
return (
<InfoDialog
title={_t("export_chat|cancelled")}
description={_t("export_chat|cancelled_detail")}
hasCloseButton={true}
onFinished={onFinished}
/>
);
2021-06-29 10:36:24 +05:30
} else if (exportSuccessful) {
// Display successful export message
return (
<InfoDialog
title={_t("export_chat|successful")}
description={_t("export_chat|successful_detail")}
hasCloseButton={true}
onFinished={onFinished}
/>
);
2021-06-29 10:36:24 +05:30
} else if (displayCancel) {
// Display cancel warning
2021-06-29 10:57:02 +05:30
return (
<BaseDialog
title={_t("common|warning")}
2021-06-29 10:57:02 +05:30
className="mx_ExportDialog"
contentId="mx_Dialog_content"
onFinished={onFinished}
fixedWidth={true}
>
<p>{_t("export_chat|confirm_stop")}</p>
2021-06-29 10:57:02 +05:30
<DialogButtons
primaryButton={_t("action|stop")}
2021-06-29 10:57:02 +05:30
primaryButtonClass="danger"
hasCancel={true}
cancelButton={_t("action|continue")}
2021-06-29 10:57:02 +05:30
onCancel={() => setCancelWarning(false)}
onPrimaryButtonClick={confirmCancel}
2021-06-29 10:57:02 +05:30
/>
</BaseDialog>
);
2021-06-29 10:36:24 +05:30
} else {
2021-07-26 00:09:59 +05:30
// Display export settings
2021-06-29 10:57:02 +05:30
return (
<BaseDialog
title={isExporting ? _t("export_chat|exporting_your_data") : _t("export_chat|title")}
2021-07-26 00:09:59 +05:30
className={`mx_ExportDialog ${isExporting && "mx_ExportDialog_Exporting"}`}
2021-06-29 10:57:02 +05:30
contentId="mx_Dialog_content"
2021-07-26 00:09:59 +05:30
hasCancel={true}
2021-06-29 10:57:02 +05:30
onFinished={onFinished}
fixedWidth={true}
>
{!isExporting ? <p>{_t("export_chat|select_option")}</p> : null}
2021-07-26 00:09:59 +05:30
<div className="mx_ExportDialog_options">
2022-01-31 12:54:14 +01:00
{!!setExportFormat && (
<>
<span className="mx_ExportDialog_subheading">{_t("export_chat|format")}</span>
2021-07-26 00:09:59 +05:30
2022-01-31 12:54:14 +01:00
<StyledRadioGroup
name="exportFormat"
value={exportFormat}
2023-02-13 11:39:16 +00:00
onChange={(key: ExportFormatKey) => setExportFormat(ExportFormat[key])}
2022-01-31 12:54:14 +01:00
definitions={exportFormatOptions}
/>
</>
)}
2021-07-26 00:09:59 +05:30
2022-01-31 12:54:14 +01:00
{!!setExportType && (
<>
<span className="mx_ExportDialog_subheading">{_t("export_chat|messages")}</span>
2021-07-26 00:09:59 +05:30
2022-01-31 12:54:14 +01:00
<Field
id="export-type"
element="select"
value={exportType}
onChange={(e) => {
2023-02-13 11:39:16 +00:00
setExportType(ExportType[e.target.value as ExportTypeKey]);
2022-01-31 12:54:14 +01:00
}}
>
{exportTypeOptions}
</Field>
{messageCount}
</>
)}
{setSizeLimit && (
<>
<span className="mx_ExportDialog_subheading">{_t("export_chat|size_limit")}</span>
2022-01-31 12:54:14 +01:00
<Field
id="size-limit"
type="number"
autoComplete="off"
onValidate={onValidateSize}
element="input"
ref={sizeLimitRef}
value={sizeLimit.toString()}
postfixComponent={sizePostFix}
onChange={(e) => setSizeLimit(parseInt(e.target.value))}
/>
</>
)}
2021-07-26 00:09:59 +05:30
2022-01-31 12:54:14 +01:00
{setAttachments && (
2022-12-12 12:24:14 +01:00
<>
2022-01-31 12:54:14 +01:00
<StyledCheckbox
className="mx_ExportDialog_attachments-checkbox"
id="include-attachments"
checked={includeAttachments}
onChange={(e) => setAttachments((e.target as HTMLInputElement).checked)}
2022-12-12 12:24:14 +01:00
>
{_t("export_chat|include_attachments")}
2022-01-31 12:54:14 +01:00
</StyledCheckbox>
2022-12-12 12:24:14 +01:00
</>
)}
2021-07-26 00:09:59 +05:30
</div>
{isExporting ? (
<div data-testid="export-progress" className="mx_ExportDialog_progress">
<Spinner size={24} />
2021-08-14 00:03:02 +05:30
<p>{exportProgressText}</p>
2021-07-26 00:09:59 +05:30
<DialogButtons
primaryButton={_t("action|cancel")}
2021-07-26 00:09:59 +05:30
primaryButtonClass="danger"
hasCancel={false}
onPrimaryButtonClick={onCancel}
/>
</div>
) : (
<DialogButtons
primaryButton={_t("action|export")}
2021-07-26 00:09:59 +05:30
onPrimaryButtonClick={onExportClick}
onCancel={() => onFinished(false)}
/>
)}
2021-06-29 10:57:02 +05:30
</BaseDialog>
);
2021-06-29 10:36:24 +05:30
}
2021-06-26 13:04:10 +05:30
};
export default ExportDialog;