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
Changes from 1 commit
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
Prev Previous commit
Next Next commit
use a customisation
Signed-off-by: Kerry Archibald <[email protected]>
Kerry Archibald committed Jan 27, 2022
commit 63d97853f6f4b9ec9fb1c4cec593ccd8d664297d
29 changes: 2 additions & 27 deletions src/components/views/dialogs/ExportDialog.tsx
Original file line number Diff line number Diff line change
@@ -39,42 +39,17 @@ 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 ChatExport from "../../../customisations/ChatExport";

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>("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;
@@ -94,7 +69,7 @@ interface ExportConfig {
* Only return change handlers for editable values
*/
const useExportFormState = (): ExportConfig => {
const config = getSafeForceRoomExportParameters();
const config = ChatExport.getForceChatExportParameters();

const [exportFormat, setExportFormat] = useState(config.format || ExportFormat.Html);
const [exportType, setExportType] = useState(config.range || ExportType.Timeline);
49 changes: 49 additions & 0 deletions src/customisations/ChatExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
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
numberOfMessages?: number;
includeAttachments?: boolean;
// must be > 0 and < 100000
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;
4 changes: 0 additions & 4 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -883,10 +883,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false,
controller: new ReloadOnChangeController(),
},
"forceRoomExportParameters": {
supportedLevels: LEVELS_UI_FEATURE,
default: {},
},
[UIFeature.RoomHistorySettings]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
60 changes: 14 additions & 46 deletions test/components/views/dialogs/ExportDialog-test.tsx
Original file line number Diff line number Diff line change
@@ -21,14 +21,12 @@ import '../../../skinned-sdk';
import { act } from "react-dom/test-utils";
import { Room } from 'matrix-js-sdk';

import ExportDialog,
{ getSafeForceRoomExportParameters, ForceRoomExportParameters }
from '../../../../src/components/views/dialogs/ExportDialog';
import ExportDialog from '../../../../src/components/views/dialogs/ExportDialog';
import { ExportType, ExportFormat } from '../../../../src/utils/exportUtils/exportUtils';
import { createTestClient, mkStubRoom } from '../../../test-utils';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
import HTMLExporter from "../../../../src/utils/exportUtils/HtmlExport";
import SettingsStore from '../../../../src/settings/SettingsStore';
import ChatExport from '../../../../src/customisations/ChatExport';
import PlainTextExporter from '../../../../src/utils/exportUtils/PlainTextExport';

jest.useFakeTimers();
@@ -42,12 +40,11 @@ const plainTextExporterInstance = ({
jest.mock("../../../../src/utils/exportUtils/HtmlExport", () => jest.fn());
jest.mock("../../../../src/utils/exportUtils/PlainTextExport", () => jest.fn());

jest.mock('../../../../src/settings/SettingsStore', () => ({
monitorSetting: jest.fn(),
getValue: jest.fn(),
jest.mock('../../../../src/customisations/ChatExport', () => ({
getForceChatExportParameters: jest.fn().mockReturnValue({}),
}));

const SettingsStoreMock = mocked(SettingsStore);
const ChatExportMock = mocked(ChatExport);
const HTMLExporterMock = mocked(HTMLExporter);
const PlainTextExporterMock = mocked(PlainTextExporter);

@@ -105,7 +102,7 @@ describe('<ExportDialog />', () => {
plainTextExporterInstance.export.mockClear();

// default setting value
SettingsStoreMock.getValue.mockClear().mockReturnValue({});
ChatExportMock.getForceChatExportParameters.mockClear().mockReturnValue({});
});

it('renders export dialog', () => {
@@ -141,12 +138,12 @@ describe('<ExportDialog />', () => {
});

it('exports room using values set from ForceRoomExportParameters', async () => {
SettingsStoreMock.getValue.mockReturnValue({
ChatExportMock.getForceChatExportParameters.mockReturnValue({
format: ExportFormat.PlainText,
range: ExportType.Beginning,
sizeMb: 15000,
numberOfMessages: 30,
attachmentsIncluded: true,
includeAttachments: true,
});
const component = getComponent();
await submitForm(component);
@@ -157,7 +154,7 @@ describe('<ExportDialog />', () => {
defaultProps.room,
ExportType.Beginning,
{
attachmentsIncluded: false,
attachmentsIncluded: true,
maxSize: 15000 * 1024 * 1024,
numberOfMessages: 30,
},
@@ -194,7 +191,7 @@ describe('<ExportDialog />', () => {
});

it('does not render export format when set in ForceRoomExportParameters', () => {
SettingsStoreMock.getValue.mockReturnValue({
ChatExportMock.getForceChatExportParameters.mockReturnValue({
format: ExportFormat.PlainText,
});
const component = getComponent();
@@ -215,7 +212,7 @@ describe('<ExportDialog />', () => {
});

it('does not render export type when set in ForceRoomExportParameters', () => {
SettingsStoreMock.getValue.mockReturnValue({
ChatExportMock.getForceChatExportParameters.mockReturnValue({
range: ExportType.Beginning,
});
const component = getComponent();
@@ -306,7 +303,7 @@ describe('<ExportDialog />', () => {
});

it('does not render size limit input when set in ForceRoomExportParameters', () => {
SettingsStoreMock.getValue.mockReturnValue({
ChatExportMock.getForceChatExportParameters.mockReturnValue({
sizeMb: 10000,
});
const component = getComponent();
@@ -317,7 +314,7 @@ describe('<ExportDialog />', () => {
* 2000mb size limit does not apply when higher limit is configured in config
*/
it('exports when size limit set in ForceRoomExportParameters is larger than 2000', async () => {
SettingsStoreMock.getValue.mockReturnValue({
ChatExportMock.getForceChatExportParameters.mockReturnValue({
sizeMb: 10000,
});
const component = getComponent();
@@ -340,41 +337,12 @@ describe('<ExportDialog />', () => {
});

it('does not render input when set in ForceRoomExportParameters', () => {
SettingsStoreMock.getValue.mockReturnValue({
ChatExportMock.getForceChatExportParameters.mockReturnValue({
includeAttachments: false,
});
const component = getComponent();
expect(getAttachmentsCheckbox(component).length).toBeFalsy();
});
});

describe('getSafeForceRoomExportParameters()', () => {
const testCases: [string, ForceRoomExportParameters, ForceRoomExportParameters][] = [
['setting is falsy', undefined, {}],
['setting is configured to string', 'test' as unknown, {}],
['setting is empty', {}, {}],
['format is not a valid ExportFormat', { format: 'mp3' }, {}],
['format is a valid ExportFormat', { format: ExportFormat.Html }, { format: ExportFormat.Html }],
['range is not a valid ExportType', { range: 'yesterday' }, {}],
['range is a valid ExportType', { range: ExportType.LastNMessages }, { range: ExportType.LastNMessages }],
['numberOfMessages is not a number', { numberOfMessages: 'test' }, {}],
['numberOfMessages is less than 1', { numberOfMessages: -1 }, {}],
['numberOfMessages is more than 100000000', { numberOfMessages: 9999999999 }, {}],
['numberOfMessages is valid', { numberOfMessages: 2000 }, { numberOfMessages: 2000 }],
['sizeMb is not a number', { sizeMb: 'test' }, {}],
['sizeMb is less than 1', { sizeMb: -1 }, {}],
['sizeMb is more than 100000', { sizeMb: Number.MAX_SAFE_INTEGER }, {}],
['sizeMb is valid', { sizeMb: 50000 }, { sizeMb: 50000 }],
['includeAttachments is not a boolean', { includeAttachments: 'yes' }, {}],
['includeAttachments is true', { includeAttachments: true }, { includeAttachments: true }],
['includeAttachments is false', { includeAttachments: false }, { includeAttachments: false }],
];

it.each(testCases)('sanitizes correctly when %s', (_d, setting, expected) => {
SettingsStoreMock.getValue.mockReturnValue(setting);

expect(getSafeForceRoomExportParameters()).toEqual(expected);
});
});
});