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 testdbkr/sss
parent
6e7ddbbae9
commit
e6835fe9d2
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2024 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 { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import { EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/SendMessageComposer";
|
||||
|
||||
// The key used to persist the the timestamp we last cleaned up drafts
|
||||
export const DRAFT_LAST_CLEANUP_KEY = "mx_draft_cleanup";
|
||||
// The period of time we wait between cleaning drafts
|
||||
export const DRAFT_CLEANUP_PERIOD = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function cleanUpDraftsIfRequired(): void {
|
||||
if (!shouldCleanupDrafts()) {
|
||||
return;
|
||||
}
|
||||
logger.debug(`Cleaning up editor drafts...`);
|
||||
cleaupDrafts();
|
||||
try {
|
||||
localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(Date.now()));
|
||||
} catch (error) {
|
||||
logger.error("Failed to persist draft cleanup key", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {bool} True if the timestamp has not been persisted or the `DRAFT_CLEANUP_PERIOD` has expired.
|
||||
*/
|
||||
function shouldCleanupDrafts(): boolean {
|
||||
try {
|
||||
const lastCleanupTimestamp = localStorage.getItem(DRAFT_LAST_CLEANUP_KEY);
|
||||
if (!lastCleanupTimestamp) {
|
||||
return true;
|
||||
}
|
||||
const parsedTimestamp = Number.parseInt(lastCleanupTimestamp || "", 10);
|
||||
if (!Number.isInteger(parsedTimestamp)) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() > parsedTimestamp + DRAFT_CLEANUP_PERIOD;
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all drafts for the CIDER editor if the room does not exist in the known rooms.
|
||||
*/
|
||||
function cleaupDrafts(): void {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const keyName = localStorage.key(i);
|
||||
if (!keyName?.startsWith(EDITOR_STATE_STORAGE_PREFIX)) continue;
|
||||
// Remove the prefix and the optional event id suffix to leave the room id
|
||||
const roomId = keyName.slice(EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0];
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (!room) {
|
||||
logger.debug(`Removing draft for unknown room with key ${keyName}`);
|
||||
localStorage.removeItem(keyName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -143,6 +143,7 @@ import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock";
|
|||
import { SessionLockStolenView } from "./auth/SessionLockStolenView";
|
||||
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
|
||||
import { LoginSplashView } from "./auth/LoginSplashView";
|
||||
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
|
@ -1528,6 +1529,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
if (state === SyncState.Syncing && prevState === SyncState.Syncing) {
|
||||
// We know we have performabed a live update and known rooms should be in a good state.
|
||||
// Now is a good time to clean up drafts.
|
||||
cleanUpDraftsIfRequired();
|
||||
return;
|
||||
}
|
||||
logger.debug(`MatrixClient sync state => ${state}`);
|
||||
|
|
|
@ -71,6 +71,9 @@ import { IDiff } from "../../../editor/diff";
|
|||
import { getBlobSafeMimeType } from "../../../utils/blobs";
|
||||
import { EMOJI_REGEX } from "../../../HtmlUtils";
|
||||
|
||||
// The prefix used when persisting editor drafts to localstorage.
|
||||
export const EDITOR_STATE_STORAGE_PREFIX = "mx_cider_state_";
|
||||
|
||||
/**
|
||||
* Build the mentions information based on the editor model (and any related events):
|
||||
*
|
||||
|
@ -604,7 +607,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
}
|
||||
|
||||
private get editorStateKey(): string {
|
||||
let key = `mx_cider_state_${this.props.room.roomId}`;
|
||||
let key = EDITOR_STATE_STORAGE_PREFIX + this.props.room.roomId;
|
||||
if (this.props.relation?.rel_type === THREAD_RELATION_TYPE.name) {
|
||||
key += `_${this.props.relation.event_id}`;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|||
import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
|
||||
import { DRAFT_LAST_CLEANUP_KEY } from "../../../src/DraftCleaner";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
||||
completeAuthorizationCodeGrant: jest.fn(),
|
||||
|
@ -598,6 +599,41 @@ describe("<MatrixChat />", () => {
|
|||
expect(screen.getByText(`Welcome ${userId}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("clean up drafts", () => {
|
||||
const roomId = "!room:server.org";
|
||||
const unknownRoomId = "!room2:server.org";
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
const timestamp = 2345678901234;
|
||||
beforeEach(() => {
|
||||
localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
|
||||
localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content");
|
||||
mockClient.getRoom.mockImplementation((id) => [room].find((room) => room.roomId === id) || null);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
it("should clean up drafts", async () => {
|
||||
Date.now = jest.fn(() => timestamp);
|
||||
localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content");
|
||||
localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
|
||||
await getComponentAndWaitForReady();
|
||||
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
|
||||
// let things settle
|
||||
await flushPromises();
|
||||
expect(localStorage.getItem(`mx_cider_state_${roomId}`)).not.toBeNull();
|
||||
expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("should not clean up drafts before expiry", async () => {
|
||||
// Set the last cleanup to the recent past
|
||||
localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
|
||||
localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(timestamp - 100));
|
||||
await getComponentAndWaitForReady();
|
||||
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
|
||||
expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("onAction()", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(defaultDispatcher, "dispatch").mockClear();
|
||||
|
|
Loading…
Reference in New Issue