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

Commit e6835fe

Browse files
authored
Clean up editor drafts for unknown rooms (#12850)
* Clean up editor drafts for unknown rooms and add tests. * lint * Call cleanUpDraftsIfRequired when we know a live update has completed. * Fix test for new call site of draft cleaning * fix test
1 parent 6e7ddbb commit e6835fe

File tree

4 files changed

+122
-1
lines changed

4 files changed

+122
-1
lines changed

src/DraftCleaner.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2024 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 { logger } from "matrix-js-sdk/src/logger";
18+
19+
import { MatrixClientPeg } from "./MatrixClientPeg";
20+
import { EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/SendMessageComposer";
21+
22+
// The key used to persist the the timestamp we last cleaned up drafts
23+
export const DRAFT_LAST_CLEANUP_KEY = "mx_draft_cleanup";
24+
// The period of time we wait between cleaning drafts
25+
export const DRAFT_CLEANUP_PERIOD = 1000 * 60 * 60 * 24 * 30;
26+
27+
/**
28+
* Checks if `DRAFT_CLEANUP_PERIOD` has expired, if so, deletes any stord editor drafts that exist for rooms that are not in the known list.
29+
*/
30+
export function cleanUpDraftsIfRequired(): void {
31+
if (!shouldCleanupDrafts()) {
32+
return;
33+
}
34+
logger.debug(`Cleaning up editor drafts...`);
35+
cleaupDrafts();
36+
try {
37+
localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(Date.now()));
38+
} catch (error) {
39+
logger.error("Failed to persist draft cleanup key", error);
40+
}
41+
}
42+
43+
/**
44+
*
45+
* @returns {bool} True if the timestamp has not been persisted or the `DRAFT_CLEANUP_PERIOD` has expired.
46+
*/
47+
function shouldCleanupDrafts(): boolean {
48+
try {
49+
const lastCleanupTimestamp = localStorage.getItem(DRAFT_LAST_CLEANUP_KEY);
50+
if (!lastCleanupTimestamp) {
51+
return true;
52+
}
53+
const parsedTimestamp = Number.parseInt(lastCleanupTimestamp || "", 10);
54+
if (!Number.isInteger(parsedTimestamp)) {
55+
return true;
56+
}
57+
return Date.now() > parsedTimestamp + DRAFT_CLEANUP_PERIOD;
58+
} catch (error) {
59+
return true;
60+
}
61+
}
62+
63+
/**
64+
* Clear all drafts for the CIDER editor if the room does not exist in the known rooms.
65+
*/
66+
function cleaupDrafts(): void {
67+
for (let i = 0; i < localStorage.length; i++) {
68+
const keyName = localStorage.key(i);
69+
if (!keyName?.startsWith(EDITOR_STATE_STORAGE_PREFIX)) continue;
70+
// Remove the prefix and the optional event id suffix to leave the room id
71+
const roomId = keyName.slice(EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0];
72+
const room = MatrixClientPeg.safeGet().getRoom(roomId);
73+
if (!room) {
74+
logger.debug(`Removing draft for unknown room with key ${keyName}`);
75+
localStorage.removeItem(keyName);
76+
}
77+
}
78+
}

src/components/structures/MatrixChat.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock";
143143
import { SessionLockStolenView } from "./auth/SessionLockStolenView";
144144
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
145145
import { LoginSplashView } from "./auth/LoginSplashView";
146+
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
146147

147148
// legacy export
148149
export { default as Views } from "../../Views";
@@ -1528,6 +1529,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
15281529
}
15291530

15301531
if (state === SyncState.Syncing && prevState === SyncState.Syncing) {
1532+
// We know we have performabed a live update and known rooms should be in a good state.
1533+
// Now is a good time to clean up drafts.
1534+
cleanUpDraftsIfRequired();
15311535
return;
15321536
}
15331537
logger.debug(`MatrixClient sync state => ${state}`);

src/components/views/rooms/SendMessageComposer.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ import { IDiff } from "../../../editor/diff";
7171
import { getBlobSafeMimeType } from "../../../utils/blobs";
7272
import { EMOJI_REGEX } from "../../../HtmlUtils";
7373

74+
// The prefix used when persisting editor drafts to localstorage.
75+
export const EDITOR_STATE_STORAGE_PREFIX = "mx_cider_state_";
76+
7477
/**
7578
* Build the mentions information based on the editor model (and any related events):
7679
*
@@ -604,7 +607,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
604607
}
605608

606609
private get editorStateKey(): string {
607-
let key = `mx_cider_state_${this.props.room.roomId}`;
610+
let key = EDITOR_STATE_STORAGE_PREFIX + this.props.room.roomId;
608611
if (this.props.relation?.rel_type === THREAD_RELATION_TYPE.name) {
609612
key += `_${this.props.relation.event_id}`;
610613
}

test/components/structures/MatrixChat-test.tsx

+36
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
6262
import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
6363
import DMRoomMap from "../../../src/utils/DMRoomMap";
6464
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
65+
import { DRAFT_LAST_CLEANUP_KEY } from "../../../src/DraftCleaner";
6566

6667
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
6768
completeAuthorizationCodeGrant: jest.fn(),
@@ -598,6 +599,41 @@ describe("<MatrixChat />", () => {
598599
expect(screen.getByText(`Welcome ${userId}`)).toBeInTheDocument();
599600
});
600601

602+
describe("clean up drafts", () => {
603+
const roomId = "!room:server.org";
604+
const unknownRoomId = "!room2:server.org";
605+
const room = new Room(roomId, mockClient, userId);
606+
const timestamp = 2345678901234;
607+
beforeEach(() => {
608+
localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
609+
localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content");
610+
mockClient.getRoom.mockImplementation((id) => [room].find((room) => room.roomId === id) || null);
611+
});
612+
afterEach(() => {
613+
jest.restoreAllMocks();
614+
});
615+
it("should clean up drafts", async () => {
616+
Date.now = jest.fn(() => timestamp);
617+
localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content");
618+
localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
619+
await getComponentAndWaitForReady();
620+
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
621+
// let things settle
622+
await flushPromises();
623+
expect(localStorage.getItem(`mx_cider_state_${roomId}`)).not.toBeNull();
624+
expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).toBeNull();
625+
});
626+
627+
it("should not clean up drafts before expiry", async () => {
628+
// Set the last cleanup to the recent past
629+
localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
630+
localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(timestamp - 100));
631+
await getComponentAndWaitForReady();
632+
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
633+
expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).not.toBeNull();
634+
});
635+
});
636+
601637
describe("onAction()", () => {
602638
beforeEach(() => {
603639
jest.spyOn(defaultDispatcher, "dispatch").mockClear();

0 commit comments

Comments
 (0)