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 all 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;
}
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