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 { SessionLockStolenView } from "./auth/SessionLockStolenView";
|
||||||
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
|
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
|
||||||
import { LoginSplashView } from "./auth/LoginSplashView";
|
import { LoginSplashView } from "./auth/LoginSplashView";
|
||||||
|
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
logger.debug(`MatrixClient sync state => ${state}`);
|
logger.debug(`MatrixClient sync state => ${state}`);
|
||||||
|
|
|
@ -71,6 +71,9 @@ import { IDiff } from "../../../editor/diff";
|
||||||
import { getBlobSafeMimeType } from "../../../utils/blobs";
|
import { getBlobSafeMimeType } from "../../../utils/blobs";
|
||||||
import { EMOJI_REGEX } from "../../../HtmlUtils";
|
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):
|
* 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 {
|
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) {
|
if (this.props.relation?.rel_type === THREAD_RELATION_TYPE.name) {
|
||||||
key += `_${this.props.relation.event_id}`;
|
key += `_${this.props.relation.event_id}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
|
import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
|
||||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||||
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
|
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
|
||||||
|
import { DRAFT_LAST_CLEANUP_KEY } from "../../../src/DraftCleaner";
|
||||||
|
|
||||||
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
||||||
completeAuthorizationCodeGrant: jest.fn(),
|
completeAuthorizationCodeGrant: jest.fn(),
|
||||||
|
@ -598,6 +599,41 @@ describe("<MatrixChat />", () => {
|
||||||
expect(screen.getByText(`Welcome ${userId}`)).toBeInTheDocument();
|
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()", () => {
|
describe("onAction()", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(defaultDispatcher, "dispatch").mockClear();
|
jest.spyOn(defaultDispatcher, "dispatch").mockClear();
|
||||||
|
|
Loading…
Reference in New Issue