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

Commit 10a429c

Browse files
turt2liveSinharitik589t3chguyyaya-usman
authored
Include a file-safe room name and ISO date in chat exports (#9440)
* conversation export named after room * sanitization added for exported file name * sanitization added for exported file name * sanitization added for exported file name * sanitization added for exported file name=>lint error fixed * sanitization added for exported file name=>lint error fixed * sanitization added for exported file name=>redundancy removed * sanitization added for exported file name=>redundancy removed * reverted to previous commit * sanitization added for exported file name=>redundancy removed * exported chat date iso formatted * conversation export named after room * conversation export named after room Co-authored-by: Michael Telatynski <[email protected]> * code refacto filename date format * Add docs to fn * Bring in a util library for sanitizing * Extract file naming function and make consistent for all 3 types Also use the library we dragged in * Write tests & associated fixes * Apply linters locally * Include new date util in index Co-authored-by: Sinharitik589 <[email protected]> Co-authored-by: Michael Telatynski <[email protected]> Co-authored-by: yaya-usman <[email protected]> Co-authored-by: Sinharitik589 <[email protected]>
1 parent ca8b1b0 commit 10a429c

16 files changed

+233
-90
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"react-focus-lock": "^2.5.1",
112112
"react-transition-group": "^4.4.1",
113113
"rfc4648": "^1.4.0",
114+
"sanitize-filename": "^1.6.3",
114115
"sanitize-html": "^2.3.2",
115116
"tar-js": "^0.3.0",
116117
"ua-parser-js": "^1.0.2",

src/DateUtils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
Copyright 2015, 2016 OpenMarket Ltd
33
Copyright 2017 Vector Creations Ltd
4+
Copyright 2022 The Matrix.org Foundation C.I.C.
45
56
Licensed under the Apache License, Version 2.0 (the "License");
67
you may not use this file except in compliance with the License.
@@ -177,6 +178,16 @@ export function formatFullDateNoDay(date: Date) {
177178
});
178179
}
179180

181+
/**
182+
* Returns an ISO date string without textual description of the date (ie: no "Wednesday" or
183+
* similar)
184+
* @param date The date to format.
185+
* @returns The date string in ISO format.
186+
*/
187+
export function formatFullDateNoDayISO(date: Date): string {
188+
return date.toISOString();
189+
}
190+
180191
export function formatFullDateNoDayNoTime(date: Date) {
181192
return (
182193
date.getFullYear() +

src/i18n/strings/en_EN.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,7 @@
756756
"Zoom in": "Zoom in",
757757
"Zoom out": "Zoom out",
758758
"Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?",
759+
"Unnamed Room": "Unnamed Room",
759760
"Generating a ZIP": "Generating a ZIP",
760761
"Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s",
761762
"Fetched %(count)s events out of %(total)s|one": "Fetched %(count)s event out of %(total)s",
@@ -2768,7 +2769,6 @@
27682769
"Or send invite link": "Or send invite link",
27692770
"Unnamed Space": "Unnamed Space",
27702771
"Invite to %(roomName)s": "Invite to %(roomName)s",
2771-
"Unnamed Room": "Unnamed Room",
27722772
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
27732773
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
27742774
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",

src/utils/exportUtils/Exporter.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,12 +20,13 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
2020
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
2121
import { saveAs } from "file-saver";
2222
import { logger } from "matrix-js-sdk/src/logger";
23+
import sanitizeFilename from "sanitize-filename";
2324

2425
import { MatrixClientPeg } from "../../MatrixClientPeg";
2526
import { ExportType, IExportOptions } from "./exportUtils";
2627
import { decryptFile } from "../DecryptFile";
2728
import { mediaFromContent } from "../../customisations/Media";
28-
import { formatFullDateNoDay } from "../../DateUtils";
29+
import { formatFullDateNoDay, formatFullDateNoDayISO } from "../../DateUtils";
2930
import { isVoiceMessage } from "../EventUtils";
3031
import { IMediaEventContent } from "../../customisations/models/IMediaEventContent";
3132
import { _t } from "../../languageHandler";
@@ -57,6 +58,10 @@ export default abstract class Exporter {
5758
window.addEventListener("beforeunload", this.onBeforeUnload);
5859
}
5960

61+
public get destinationFileName(): string {
62+
return this.makeFileNameNoExtension(SdkConfig.get().brand) + ".zip";
63+
}
64+
6065
protected onBeforeUnload(e: BeforeUnloadEvent): string {
6166
e.preventDefault();
6267
return e.returnValue = _t("Are you sure you want to exit during this export?");
@@ -75,10 +80,19 @@ export default abstract class Exporter {
7580
this.files.push(file);
7681
}
7782

83+
protected makeFileNameNoExtension(brand = "matrix"): string {
84+
// First try to use the real name of the room, then a translated copy of a generic name,
85+
// then finally hardcoded default to guarantee we'll have a name.
86+
const safeRoomName = sanitizeFilename(this.room.name ?? _t("Unnamed Room")).trim() || "Unnamed Room";
87+
const safeDate = formatFullDateNoDayISO(new Date())
88+
.replace(/:/g, '-'); // ISO format automatically removes a lot of stuff for us
89+
const safeBrand = sanitizeFilename(brand);
90+
return `${safeBrand} - ${safeRoomName} - Chat Export - ${safeDate}`;
91+
}
92+
7893
protected async downloadZIP(): Promise<string | void> {
79-
const brand = SdkConfig.get().brand;
80-
const filenameWithoutExt = `${brand} - Chat Export - ${formatFullDateNoDay(new Date())}`;
81-
const filename = `${filenameWithoutExt}.zip`;
94+
const filename = this.destinationFileName;
95+
const filenameWithoutExt = filename.substring(0, filename.length - 4); // take off the .zip
8296
const { default: JSZip } = await import('jszip');
8397

8498
const zip = new JSZip();

src/utils/exportUtils/JSONExport.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
2020
import { logger } from "matrix-js-sdk/src/logger";
2121

2222
import Exporter from "./Exporter";
23-
import { formatFullDateNoDay, formatFullDateNoDayNoTime } from "../../DateUtils";
23+
import { formatFullDateNoDayNoTime } from "../../DateUtils";
2424
import { ExportType, IExportOptions } from "./exportUtils";
2525
import { _t } from "../../languageHandler";
2626
import { haveRendererForEvent } from "../../events/EventTileFactory";
@@ -38,6 +38,10 @@ export default class JSONExporter extends Exporter {
3838
super(room, exportType, exportOptions, setProgressText);
3939
}
4040

41+
public get destinationFileName(): string {
42+
return this.makeFileNameNoExtension() + ".json";
43+
}
44+
4145
protected createJSONString(): string {
4246
const exportDate = formatFullDateNoDayNoTime(new Date());
4347
const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
@@ -108,7 +112,7 @@ export default class JSONExporter extends Exporter {
108112
this.addFile("export.json", new Blob([text]));
109113
await this.downloadZIP();
110114
} else {
111-
const fileName = `matrix-export-${formatFullDateNoDay(new Date())}.json`;
115+
const fileName = this.destinationFileName;
112116
this.downloadPlainText(fileName, text);
113117
}
114118

src/utils/exportUtils/PlainTextExport.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import { logger } from "matrix-js-sdk/src/logger";
2020
import React from "react";
2121

2222
import Exporter from "./Exporter";
23-
import { formatFullDateNoDay } from "../../DateUtils";
2423
import { _t } from "../../languageHandler";
2524
import { ExportType, IExportOptions } from "./exportUtils";
2625
import { textForEvent } from "../../TextForEvent";
@@ -43,6 +42,10 @@ export default class PlainTextExporter extends Exporter {
4342
: _t("Media omitted - file size limit exceeded");
4443
}
4544

45+
public get destinationFileName(): string {
46+
return this.makeFileNameNoExtension() + ".txt";
47+
}
48+
4649
public textForReplyEvent = (content: IContent) => {
4750
const REPLY_REGEX = /> <(.*?)>(.*?)\n\n(.*)/s;
4851
const REPLY_SOURCE_MAX_LENGTH = 32;
@@ -137,7 +140,7 @@ export default class PlainTextExporter extends Exporter {
137140
this.addFile("export.txt", new Blob([text]));
138141
await this.downloadZIP();
139142
} else {
140-
const fileName = `matrix-export-${formatFullDateNoDay(new Date())}.txt`;
143+
const fileName = this.destinationFileName;
141144
this.downloadPlainText(fileName, text);
142145
}
143146

test/test-utils/date.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
export const REPEATABLE_DATE = new Date(2022, 10, 17, 16, 58, 32, 517);

test/test-utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export * from './test-utils';
2424
export * from './call';
2525
export * from './wrappers';
2626
export * from './utilities';
27+
export * from './date';

test/utils/DateUtils-test.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { formatSeconds, formatRelativeTime, formatDuration } from "../../src/DateUtils";
17+
import { formatSeconds, formatRelativeTime, formatDuration, formatFullDateNoDayISO } from "../../src/DateUtils";
18+
import { REPEATABLE_DATE } from "../test-utils";
1819

1920
describe("formatSeconds", () => {
2021
it("correctly formats time with hours", () => {
@@ -92,3 +93,9 @@ describe('formatDuration()', () => {
9293
expect(formatDuration(input)).toEqual(expectedResult);
9394
});
9495
});
96+
97+
describe("formatFullDateNoDayISO", () => {
98+
it("should return ISO format", () => {
99+
expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z");
100+
});
101+
});
+52
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 { mocked } from "jest-mock";
18+
19+
import { createTestClient, mkStubRoom, REPEATABLE_DATE } from "../../test-utils";
20+
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
21+
import SdkConfig from "../../../src/SdkConfig";
22+
import HTMLExporter from "../../../src/utils/exportUtils/HtmlExport";
23+
24+
describe("HTMLExport", () => {
25+
beforeEach(() => {
26+
jest.useFakeTimers('modern');
27+
jest.setSystemTime(REPEATABLE_DATE);
28+
});
29+
30+
afterEach(() => {
31+
mocked(SdkConfig.get).mockRestore();
32+
});
33+
34+
it("should have an SDK-branded destination file name", () => {
35+
const roomName = "My / Test / Room: Welcome";
36+
const client = createTestClient();
37+
const stubOptions: IExportOptions = {
38+
attachmentsIncluded: false,
39+
maxSize: 50000000,
40+
};
41+
const stubRoom = mkStubRoom("!myroom:example.org", roomName, client);
42+
const exporter = new HTMLExporter(stubRoom, ExportType.Timeline, stubOptions, () => {});
43+
44+
expect(exporter.destinationFileName).toMatchSnapshot();
45+
46+
jest.spyOn(SdkConfig, "get").mockImplementation(() => {
47+
return { brand: "BrandedChat/WithSlashes/ForFun" };
48+
});
49+
50+
expect(exporter.destinationFileName).toMatchSnapshot();
51+
});
52+
});
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 JSONExporter from "../../../src/utils/exportUtils/JSONExport";
18+
import { createTestClient, mkStubRoom, REPEATABLE_DATE } from "../../test-utils";
19+
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
20+
21+
describe("JSONExport", () => {
22+
beforeEach(() => {
23+
jest.useFakeTimers('modern');
24+
jest.setSystemTime(REPEATABLE_DATE);
25+
});
26+
27+
it("should have a Matrix-branded destination file name", () => {
28+
const roomName = "My / Test / Room: Welcome";
29+
const client = createTestClient();
30+
const stubOptions: IExportOptions = {
31+
attachmentsIncluded: false,
32+
maxSize: 50000000,
33+
};
34+
const stubRoom = mkStubRoom("!myroom:example.org", roomName, client);
35+
const exporter = new JSONExporter(stubRoom, ExportType.Timeline, stubOptions, () => {});
36+
37+
expect(exporter.destinationFileName).toMatchSnapshot();
38+
});
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 { createTestClient, mkStubRoom, REPEATABLE_DATE } from "../../test-utils";
18+
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
19+
import PlainTextExporter from "../../../src/utils/exportUtils/PlainTextExport";
20+
21+
describe("PlainTextExport", () => {
22+
beforeEach(() => {
23+
jest.useFakeTimers('modern');
24+
jest.setSystemTime(REPEATABLE_DATE);
25+
});
26+
27+
it("should have a Matrix-branded destination file name", () => {
28+
const roomName = "My / Test / Room: Welcome";
29+
const client = createTestClient();
30+
const stubOptions: IExportOptions = {
31+
attachmentsIncluded: false,
32+
maxSize: 50000000,
33+
};
34+
const stubRoom = mkStubRoom("!myroom:example.org", roomName, client);
35+
const exporter = new PlainTextExporter(stubRoom, ExportType.Timeline, stubOptions, () => {});
36+
37+
expect(exporter.destinationFileName).toMatchSnapshot();
38+
});
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`HTMLExport should have an SDK-branded destination file name 1`] = `"Element - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.zip"`;
4+
5+
exports[`HTMLExport should have an SDK-branded destination file name 2`] = `"BrandedChatWithSlashesForFun - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.zip"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`JSONExport should have a Matrix-branded destination file name 1`] = `"matrix - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.json"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`PlainTextExport should have a Matrix-branded destination file name 1`] = `"matrix - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.txt"`;

0 commit comments

Comments
 (0)