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

Commit

Permalink
Chat export parameter customisation (#7647)
Browse files Browse the repository at this point in the history
* use export settings and hide fields

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix exporter tests

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test ExportDialog with settings

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy debugs, rename setting to Parameters

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use reasonable 100gb limit

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use normal setting instead of UIFeature

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use a customisation

Signed-off-by: Kerry Archibald <kerrya@element.io>

* move validateNumberInRange to utils

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use nullish coalesce

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use 8gb size limit for customisation

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update comments

Signed-off-by: Kerry Archibald <kerrya@element.io>
  • Loading branch information
Kerry authored Jan 31, 2022
1 parent ad87ee0 commit 085ecc7
Show file tree
Hide file tree
Showing 13 changed files with 504 additions and 192 deletions.
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;
}
201 changes: 131 additions & 70 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,70 @@ 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";
import { validateNumberInRange } from "../../../utils/validate";

interface IProps extends IDialogProps {
room: Room;
}

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 | null>(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<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 +162,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 @@ -147,10 +200,8 @@ 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);
const parsedSize = parseInt(value as string, 10);
return validateNumberInRange(1, 2000)(parsedSize);
},
invalid: () => {
const min = 1;
Expand Down Expand Up @@ -187,11 +238,8 @@ 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);
const parsedSize = parseInt(value as string, 10);
return validateNumberInRange(1, 10 ** 8)(parsedSize);
},
invalid: () => {
const min = 1;
Expand Down Expand Up @@ -236,7 +284,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 +367,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
52 changes: 52 additions & 0 deletions src/customisations/ChatExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
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
// only used when range is 'LastNMessages'
// default is 100
numberOfMessages?: number;
includeAttachments?: boolean;
// maximum size of exported archive
// must be > 0 and < 8000
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 > 8000 * 1024 * 1024 || // More than 8 GB
exportOptions.numberOfMessages > 10**8
) {
throw new Error("Invalid export options");
Expand Down
1 change: 1 addition & 0 deletions src/utils/validate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./numberInRange";
9 changes: 9 additions & 0 deletions src/utils/validate/numberInRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

/**
* Validates that a value is
* - a number
* - in a provided range (inclusive)
*/
export const validateNumberInRange = (min: number, max: number) => (value?: number) => {
return typeof value === 'number' && !(isNaN(value) || min > value || value > max);
};
Loading

0 comments on commit 085ecc7

Please sign in to comment.