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 5 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;
}
233 changes: 159 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,100 @@ import { useStateCallback } from "../../../hooks/useStateCallback";
import Exporter from "../../../utils/exportUtils/Exporter";
import Spinner from "../elements/Spinner";
import InfoDialog from "./InfoDialog";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";

interface IProps extends IDialogProps {
room: Room;
}
const isExportFormat = (config?: string): config is ExportFormat =>
config && Object.values(ExportFormat).includes(config as ExportFormat);

const isExportType = (config?: string): config is ExportType =>
config && Object.values(ExportType).includes(config as ExportType);

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/


// Sanitize setting values, exclude invalid or missing values
export type ForceRoomExportParameters = {
format?: ExportFormat; range?: ExportType; numberOfMessages?: number; includeAttachments?: boolean; sizeMb?: number;
};
export const getSafeForceRoomExportParameters = (): ForceRoomExportParameters => {
const config = SettingsStore.getValue<ForceRoomExportParameters>(UIFeature.ForceRoomExportParameters);
if (!config || typeof config !== "object") return {};

const { format, range, numberOfMessages, includeAttachments, sizeMb } = config;

return {
...(isExportFormat(format) && { format }),
...(isExportType(range) && { range }),
...(validateNumberInRange(1, 10 ** 8)(numberOfMessages) && { numberOfMessages }),
// ~100GB limit
...(validateNumberInRange(1, 100000)(sizeMb) && { sizeMb }),
...(typeof includeAttachments === 'boolean' && { includeAttachments }),
};
};

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 UIFeature.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 = getSafeForceRoomExportParameters();

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 +192,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 +229,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 +264,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 +308,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 +391,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
4 changes: 4 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -956,4 +956,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.ForceRoomExportParameters]: {
supportedLevels: LEVELS_UI_FEATURE,
default: {},
},
Copy link
Member

Choose a reason for hiding this comment

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

https://github.com/vector-im/element-web/blob/develop/docs/config.md#ui-features

Parts of the UI can be disabled using UI features. These are settings which appear under settingDefaults and can only be true (default) or false. When false, parts of the UI relating to that feature will be disabled regardless of the user's preferences.

UIFs must be booleans

};
3 changes: 2 additions & 1 deletion src/settings/UIFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export enum UIFeature {
Communities = "UIFeature.communities",
AdvancedSettings = "UIFeature.advancedSettings",
RoomHistorySettings = "UIFeature.roomHistorySettings",
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates"
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
ForceRoomExportParameters = "UIFeature.ForceRoomExportParameters"
}

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