-
-
Notifications
You must be signed in to change notification settings - Fork 828
Chat export parameter customisation #7647
Changes from 5 commits
77078df
def7439
3146317
63f3559
74ccf3d
6998302
63d9785
02dbc60
4118e3e
00f0495
506900c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
||
|
@@ -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); | ||
}; | ||
|
||
// 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the benefit? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
|
||
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...")); | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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" | ||
|
@@ -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"> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -956,4 +956,8 @@ export const SETTINGS: {[setting: string]: ISetting} = { | |
supportedLevels: LEVELS_UI_FEATURE, | ||
default: true, | ||
}, | ||
[UIFeature.ForceRoomExportParameters]: { | ||
supportedLevels: LEVELS_UI_FEATURE, | ||
default: {}, | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
UIFs must be booleans |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
|
There was a problem hiding this comment.
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/