Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Chat export parameter customisation #7647

Merged
merged 11 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"ts-jest": "^27.1.3",
"typescript": "4.5.3",
"walk": "^2.3.14"
},
Expand Down
4 changes: 4 additions & 0 deletions res/css/views/dialogs/_ExportDialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@ limitations under the License.
padding: 9px 10px;
}
}

.mx_ExportDialog_attachments-checkbox {
margin-top: $spacing-16;
}
207 changes: 133 additions & 74 deletions src/components/views/dialogs/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { useRef, useState } from "react";
import React, { useRef, useState, Dispatch, SetStateAction } from "react";
import { Room } from "matrix-js-sdk/src";
import { logger } from "matrix-js-sdk/src/logger";

Expand All @@ -39,18 +39,74 @@ import { useStateCallback } from "../../../hooks/useStateCallback";
import Exporter from "../../../utils/exportUtils/Exporter";
import Spinner from "../elements/Spinner";
import InfoDialog from "./InfoDialog";
import ChatExport from "../../../customisations/ChatExport";

interface IProps extends IDialogProps {
room: Room;
}

const validateNumberInRange = (min: number, max: number) => (value?: string | number) => {
const parsedSize = parseInt(value as string, 10);
return !(isNaN(parsedSize) || min > parsedSize || parsedSize > max);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be a nice shared util in /utils/


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 !== undefined && config.includeAttachments);
const [numberOfMessages, setNumberOfMessages] = useState<number>(config.numberOfMessages || 100);
const [sizeLimit, setSizeLimit] = useState<number | null>(config.sizeMb || 8);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? instead of || here please

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If sizeMb was set to 0 it'd consistently be respected, vs with the current approach -1 stays as -1, 0 goes to 0


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<IProps> = ({ room, onFinished }) => {
const [exportFormat, setExportFormat] = useState(ExportFormat.Html);
const [exportType, setExportType] = useState(ExportType.Timeline);
const [includeAttachments, setAttachments] = useState(false);
const {
exportFormat,
exportType,
includeAttachments,
numberOfMessages,
sizeLimit,
setExportFormat,
setExportType,
setNumberOfMessages,
setSizeLimit,
setAttachments,
} = useExportFormState();

const [isExporting, setExporting] = useState(false);
const [numberOfMessages, setNumberOfMessages] = useState<number>(100);
const [sizeLimit, setSizeLimit] = useState<number | null>(8);
const sizeLimitRef = useRef<Field>();
const messageCountRef = useRef<Field>();
const [exportProgressText, setExportProgressText] = useState(_t("Processing..."));
Expand Down Expand Up @@ -110,9 +166,10 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
};

const onExportClick = async () => {
const isValidSize = await sizeLimitRef.current.validate({
const isValidSize = !setSizeLimit || (await sizeLimitRef.current.validate({
focused: false,
});
}));

if (!isValidSize) {
sizeLimitRef.current.validate({ focused: true });
return;
Expand Down Expand Up @@ -146,12 +203,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
},
}, {
key: "number",
test: ({ value }) => {
const parsedSize = parseFloat(value);
const min = 1;
const max = 2000;
return !(isNaN(parsedSize) || min > parsedSize || parsedSize > max);
},
test: ({ value }) => validateNumberInRange(1, 2000)(value),
invalid: () => {
const min = 1;
const max = 2000;
Expand Down Expand Up @@ -186,13 +238,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
},
}, {
key: "number",
test: ({ value }) => {
const parsedSize = parseFloat(value);
const min = 1;
const max = 10 ** 8;
if (isNaN(parsedSize)) return false;
return !(min > parsedSize || parsedSize > max);
},
test: ({ value }) => validateNumberInRange(1, 10 ** 8)(value),
invalid: () => {
const min = 1;
const max = 10 ** 8;
Expand Down Expand Up @@ -236,7 +282,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
});

let messageCount = null;
if (exportType === ExportType.LastNMessages) {
if (exportType === ExportType.LastNMessages && setNumberOfMessages) {
messageCount = (
<Field
id="message-count"
Expand Down Expand Up @@ -319,61 +365,74 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
) }
</p> : null }

<span className="mx_ExportDialog_subheading">
{ _t("Format") }
</span>

<div className="mx_ExportDialog_options">
<StyledRadioGroup
name="exportFormat"
value={exportFormat}
onChange={(key) => setExportFormat(ExportFormat[key])}
definitions={exportFormatOptions}
/>
{ !!setExportFormat && <>
<span className="mx_ExportDialog_subheading">
{ _t("Format") }
</span>

<span className="mx_ExportDialog_subheading">
{ _t("Messages") }
</span>

<Field
id="export-type"
element="select"
value={exportType}
onChange={(e) => {
setExportType(ExportType[e.target.value]);
}}
>
{ exportTypeOptions }
</Field>
{ messageCount }

<span className="mx_ExportDialog_subheading">
{ _t("Size Limit") }
</span>

<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))}
/>
<StyledRadioGroup
name="exportFormat"
value={exportFormat}
onChange={(key) => setExportFormat(ExportFormat[key])}
definitions={exportFormatOptions}
/>
</> }

{
!!setExportType && <>

<span className="mx_ExportDialog_subheading">
{ _t("Messages") }
</span>

<Field
id="export-type"
element="select"
value={exportType}
onChange={(e) => {
setExportType(ExportType[e.target.value]);
}}
>
{ exportTypeOptions }
</Field>
{ messageCount }
</>
}

{ setSizeLimit && <>
<span className="mx_ExportDialog_subheading">
{ _t("Size Limit") }
</span>

<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))}
/>
</> }

{ setAttachments && <>
<StyledCheckbox
className="mx_ExportDialog_attachments-checkbox"
id="include-attachments"
checked={includeAttachments}
onChange={(e) =>
setAttachments(
(e.target as HTMLInputElement).checked,
)
}
>
{ _t("Include Attachments") }
</StyledCheckbox>
</> }

<StyledCheckbox
id="include-attachments"
checked={includeAttachments}
onChange={(e) =>
setAttachments(
(e.target as HTMLInputElement).checked,
)
}
>
{ _t("Include Attachments") }
</StyledCheckbox>
</div>
{ isExporting ? (
<div data-test-id='export-progress' className="mx_ExportDialog_progress">
Expand Down
49 changes: 49 additions & 0 deletions src/customisations/ChatExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
Copyright 2022 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 { ExportFormat, ExportType } from "../utils/exportUtils/exportUtils";

export type ForceChatExportParameters = {
format?: ExportFormat;
range?: ExportType;
// must be < 10**8
numberOfMessages?: number;
includeAttachments?: boolean;
// must be > 0 and < 100000
sizeMb?: number;
};

/**
* Force parameters in room chat export
* fields returned here are forced
* and not allowed to be edited in the chat export form
*/
const getForceChatExportParameters = (): ForceChatExportParameters => {
return {};
};

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IChatExportCustomisations {
getForceChatExportParameters?: typeof getForceChatExportParameters;
}

// A real customisation module will define and export one or more of the
// customisation points that make up `IChatExportCustomisations`.
export default {
getForceChatExportParameters,
} as IChatExportCustomisations;
2 changes: 1 addition & 1 deletion src/settings/UIFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export enum UIFeature {
Communities = "UIFeature.communities",
AdvancedSettings = "UIFeature.advancedSettings",
RoomHistorySettings = "UIFeature.roomHistorySettings",
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates"
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
}

export enum UIComponent {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/exportUtils/Exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default abstract class Exporter {
protected setProgressText: React.Dispatch<React.SetStateAction<string>>,
) {
if (exportOptions.maxSize < 1 * 1024 * 1024|| // Less than 1 MB
exportOptions.maxSize > 2000 * 1024 * 1024|| // More than ~ 2 GB
exportOptions.maxSize > 100000 * 1024 * 1024 || // More than 100 GB
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that it has to be loaded into RAM before downloading, I doubt this is a sane change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my thinking was that it's basically a 'use at your own risk' config, but we should probably still stop people from crashing their browser. Not sure what's the highest possible reasonable limit - 16gb?

exportOptions.numberOfMessages > 10**8
) {
throw new Error("Invalid export options");
Expand Down
Loading