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

Commit 085ecc7

Browse files
author
Kerry
authored
Chat export parameter customisation (#7647)
* use export settings and hide fields Signed-off-by: Kerry Archibald <[email protected]> * fix exporter tests Signed-off-by: Kerry Archibald <[email protected]> * test ExportDialog with settings Signed-off-by: Kerry Archibald <[email protected]> * tidy debugs, rename setting to Parameters Signed-off-by: Kerry Archibald <[email protected]> * use reasonable 100gb limit Signed-off-by: Kerry Archibald <[email protected]> * use normal setting instead of UIFeature Signed-off-by: Kerry Archibald <[email protected]> * use a customisation Signed-off-by: Kerry Archibald <[email protected]> * move validateNumberInRange to utils Signed-off-by: Kerry Archibald <[email protected]> * use nullish coalesce Signed-off-by: Kerry Archibald <[email protected]> * use 8gb size limit for customisation Signed-off-by: Kerry Archibald <[email protected]> * update comments Signed-off-by: Kerry Archibald <[email protected]>
1 parent ad87ee0 commit 085ecc7

File tree

13 files changed

+504
-192
lines changed

13 files changed

+504
-192
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
"stylelint": "^13.9.0",
192192
"stylelint-config-standard": "^20.0.0",
193193
"stylelint-scss": "^3.18.0",
194+
"ts-jest": "^27.1.3",
194195
"typescript": "4.5.3",
195196
"walk": "^2.3.14"
196197
},

res/css/views/dialogs/_ExportDialog.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,7 @@ limitations under the License.
8989
padding: 9px 10px;
9090
}
9191
}
92+
93+
.mx_ExportDialog_attachments-checkbox {
94+
margin-top: $spacing-16;
95+
}

src/components/views/dialogs/ExportDialog.tsx

Lines changed: 131 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

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

@@ -39,18 +39,70 @@ import { useStateCallback } from "../../../hooks/useStateCallback";
3939
import Exporter from "../../../utils/exportUtils/Exporter";
4040
import Spinner from "../elements/Spinner";
4141
import InfoDialog from "./InfoDialog";
42+
import ChatExport from "../../../customisations/ChatExport";
43+
import { validateNumberInRange } from "../../../utils/validate";
4244

4345
interface IProps extends IDialogProps {
4446
room: Room;
4547
}
4648

49+
interface ExportConfig {
50+
exportFormat: ExportFormat;
51+
exportType: ExportType;
52+
numberOfMessages: number;
53+
sizeLimit: number;
54+
includeAttachments: boolean;
55+
setExportFormat?: Dispatch<SetStateAction<ExportFormat>>;
56+
setExportType?: Dispatch<SetStateAction<ExportType>>;
57+
setAttachments?: Dispatch<SetStateAction<boolean>>;
58+
setNumberOfMessages?: Dispatch<SetStateAction<number>>;
59+
setSizeLimit?: Dispatch<SetStateAction<number>>;
60+
}
61+
62+
/**
63+
* Set up form state using "forceRoomExportParameters" or defaults
64+
* Form fields configured in ForceRoomExportParameters are not allowed to be edited
65+
* Only return change handlers for editable values
66+
*/
67+
const useExportFormState = (): ExportConfig => {
68+
const config = ChatExport.getForceChatExportParameters();
69+
70+
const [exportFormat, setExportFormat] = useState(config.format ?? ExportFormat.Html);
71+
const [exportType, setExportType] = useState(config.range ?? ExportType.Timeline);
72+
const [includeAttachments, setAttachments] =
73+
useState(config.includeAttachments ?? false);
74+
const [numberOfMessages, setNumberOfMessages] = useState<number>(config.numberOfMessages ?? 100);
75+
const [sizeLimit, setSizeLimit] = useState<number | null>(config.sizeMb ?? 8);
76+
77+
return {
78+
exportFormat,
79+
exportType,
80+
includeAttachments,
81+
numberOfMessages,
82+
sizeLimit,
83+
setExportFormat: !config.format ? setExportFormat : undefined,
84+
setExportType: !config.range ? setExportType : undefined,
85+
setNumberOfMessages: !config.numberOfMessages ? setNumberOfMessages : undefined,
86+
setSizeLimit: !config.sizeMb ? setSizeLimit : undefined,
87+
setAttachments: config.includeAttachments === undefined ? setAttachments : undefined,
88+
};
89+
};
90+
4791
const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
48-
const [exportFormat, setExportFormat] = useState(ExportFormat.Html);
49-
const [exportType, setExportType] = useState(ExportType.Timeline);
50-
const [includeAttachments, setAttachments] = useState(false);
92+
const {
93+
exportFormat,
94+
exportType,
95+
includeAttachments,
96+
numberOfMessages,
97+
sizeLimit,
98+
setExportFormat,
99+
setExportType,
100+
setNumberOfMessages,
101+
setSizeLimit,
102+
setAttachments,
103+
} = useExportFormState();
104+
51105
const [isExporting, setExporting] = useState(false);
52-
const [numberOfMessages, setNumberOfMessages] = useState<number>(100);
53-
const [sizeLimit, setSizeLimit] = useState<number | null>(8);
54106
const sizeLimitRef = useRef<Field>();
55107
const messageCountRef = useRef<Field>();
56108
const [exportProgressText, setExportProgressText] = useState(_t("Processing..."));
@@ -110,9 +162,10 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
110162
};
111163

112164
const onExportClick = async () => {
113-
const isValidSize = await sizeLimitRef.current.validate({
165+
const isValidSize = !setSizeLimit || (await sizeLimitRef.current.validate({
114166
focused: false,
115-
});
167+
}));
168+
116169
if (!isValidSize) {
117170
sizeLimitRef.current.validate({ focused: true });
118171
return;
@@ -147,10 +200,8 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
147200
}, {
148201
key: "number",
149202
test: ({ value }) => {
150-
const parsedSize = parseFloat(value);
151-
const min = 1;
152-
const max = 2000;
153-
return !(isNaN(parsedSize) || min > parsedSize || parsedSize > max);
203+
const parsedSize = parseInt(value as string, 10);
204+
return validateNumberInRange(1, 2000)(parsedSize);
154205
},
155206
invalid: () => {
156207
const min = 1;
@@ -187,11 +238,8 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
187238
}, {
188239
key: "number",
189240
test: ({ value }) => {
190-
const parsedSize = parseFloat(value);
191-
const min = 1;
192-
const max = 10 ** 8;
193-
if (isNaN(parsedSize)) return false;
194-
return !(min > parsedSize || parsedSize > max);
241+
const parsedSize = parseInt(value as string, 10);
242+
return validateNumberInRange(1, 10 ** 8)(parsedSize);
195243
},
196244
invalid: () => {
197245
const min = 1;
@@ -236,7 +284,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
236284
});
237285

238286
let messageCount = null;
239-
if (exportType === ExportType.LastNMessages) {
287+
if (exportType === ExportType.LastNMessages && setNumberOfMessages) {
240288
messageCount = (
241289
<Field
242290
id="message-count"
@@ -319,61 +367,74 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
319367
) }
320368
</p> : null }
321369

322-
<span className="mx_ExportDialog_subheading">
323-
{ _t("Format") }
324-
</span>
325-
326370
<div className="mx_ExportDialog_options">
327-
<StyledRadioGroup
328-
name="exportFormat"
329-
value={exportFormat}
330-
onChange={(key) => setExportFormat(ExportFormat[key])}
331-
definitions={exportFormatOptions}
332-
/>
371+
{ !!setExportFormat && <>
372+
<span className="mx_ExportDialog_subheading">
373+
{ _t("Format") }
374+
</span>
333375

334-
<span className="mx_ExportDialog_subheading">
335-
{ _t("Messages") }
336-
</span>
337-
338-
<Field
339-
id="export-type"
340-
element="select"
341-
value={exportType}
342-
onChange={(e) => {
343-
setExportType(ExportType[e.target.value]);
344-
}}
345-
>
346-
{ exportTypeOptions }
347-
</Field>
348-
{ messageCount }
349-
350-
<span className="mx_ExportDialog_subheading">
351-
{ _t("Size Limit") }
352-
</span>
353-
354-
<Field
355-
id="size-limit"
356-
type="number"
357-
autoComplete="off"
358-
onValidate={onValidateSize}
359-
element="input"
360-
ref={sizeLimitRef}
361-
value={sizeLimit.toString()}
362-
postfixComponent={sizePostFix}
363-
onChange={(e) => setSizeLimit(parseInt(e.target.value))}
364-
/>
376+
<StyledRadioGroup
377+
name="exportFormat"
378+
value={exportFormat}
379+
onChange={(key) => setExportFormat(ExportFormat[key])}
380+
definitions={exportFormatOptions}
381+
/>
382+
</> }
383+
384+
{
385+
!!setExportType && <>
386+
387+
<span className="mx_ExportDialog_subheading">
388+
{ _t("Messages") }
389+
</span>
390+
391+
<Field
392+
id="export-type"
393+
element="select"
394+
value={exportType}
395+
onChange={(e) => {
396+
setExportType(ExportType[e.target.value]);
397+
}}
398+
>
399+
{ exportTypeOptions }
400+
</Field>
401+
{ messageCount }
402+
</>
403+
}
404+
405+
{ setSizeLimit && <>
406+
<span className="mx_ExportDialog_subheading">
407+
{ _t("Size Limit") }
408+
</span>
409+
410+
<Field
411+
id="size-limit"
412+
type="number"
413+
autoComplete="off"
414+
onValidate={onValidateSize}
415+
element="input"
416+
ref={sizeLimitRef}
417+
value={sizeLimit.toString()}
418+
postfixComponent={sizePostFix}
419+
onChange={(e) => setSizeLimit(parseInt(e.target.value))}
420+
/>
421+
</> }
422+
423+
{ setAttachments && <>
424+
<StyledCheckbox
425+
className="mx_ExportDialog_attachments-checkbox"
426+
id="include-attachments"
427+
checked={includeAttachments}
428+
onChange={(e) =>
429+
setAttachments(
430+
(e.target as HTMLInputElement).checked,
431+
)
432+
}
433+
>
434+
{ _t("Include Attachments") }
435+
</StyledCheckbox>
436+
</> }
365437

366-
<StyledCheckbox
367-
id="include-attachments"
368-
checked={includeAttachments}
369-
onChange={(e) =>
370-
setAttachments(
371-
(e.target as HTMLInputElement).checked,
372-
)
373-
}
374-
>
375-
{ _t("Include Attachments") }
376-
</StyledCheckbox>
377438
</div>
378439
{ isExporting ? (
379440
<div data-test-id='export-progress' className="mx_ExportDialog_progress">

src/customisations/ChatExport.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { ExportFormat, ExportType } from "../utils/exportUtils/exportUtils";
18+
19+
export type ForceChatExportParameters = {
20+
format?: ExportFormat;
21+
range?: ExportType;
22+
// must be < 10**8
23+
// only used when range is 'LastNMessages'
24+
// default is 100
25+
numberOfMessages?: number;
26+
includeAttachments?: boolean;
27+
// maximum size of exported archive
28+
// must be > 0 and < 8000
29+
sizeMb?: number;
30+
};
31+
32+
/**
33+
* Force parameters in room chat export
34+
* fields returned here are forced
35+
* and not allowed to be edited in the chat export form
36+
*/
37+
const getForceChatExportParameters = (): ForceChatExportParameters => {
38+
return {};
39+
};
40+
41+
// This interface summarises all available customisation points and also marks
42+
// them all as optional. This allows customisers to only define and export the
43+
// customisations they need while still maintaining type safety.
44+
export interface IChatExportCustomisations {
45+
getForceChatExportParameters?: typeof getForceChatExportParameters;
46+
}
47+
48+
// A real customisation module will define and export one or more of the
49+
// customisation points that make up `IChatExportCustomisations`.
50+
export default {
51+
getForceChatExportParameters,
52+
} as IChatExportCustomisations;

src/settings/UIFeature.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export enum UIFeature {
3232
Communities = "UIFeature.communities",
3333
AdvancedSettings = "UIFeature.advancedSettings",
3434
RoomHistorySettings = "UIFeature.roomHistorySettings",
35-
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates"
35+
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
3636
}
3737

3838
export enum UIComponent {

src/utils/exportUtils/Exporter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default abstract class Exporter {
4848
protected setProgressText: React.Dispatch<React.SetStateAction<string>>,
4949
) {
5050
if (exportOptions.maxSize < 1 * 1024 * 1024|| // Less than 1 MB
51-
exportOptions.maxSize > 2000 * 1024 * 1024|| // More than ~ 2 GB
51+
exportOptions.maxSize > 8000 * 1024 * 1024 || // More than 8 GB
5252
exportOptions.numberOfMessages > 10**8
5353
) {
5454
throw new Error("Invalid export options");

src/utils/validate/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./numberInRange";
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
/**
3+
* Validates that a value is
4+
* - a number
5+
* - in a provided range (inclusive)
6+
*/
7+
export const validateNumberInRange = (min: number, max: number) => (value?: number) => {
8+
return typeof value === 'number' && !(isNaN(value) || min > value || value > max);
9+
};

0 commit comments

Comments
 (0)