From 4e0ad167503600e040b1d752701e3880a730861b Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 24 Jun 2024 09:45:40 +0100 Subject: [PATCH 1/9] Add drafts to the RTE and tests --- playwright/e2e/composer/composer.spec.ts | 70 +++++++++++++++ .../views/rooms/MessageComposer.tsx | 88 +++++++++++++++++-- 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/playwright/e2e/composer/composer.spec.ts b/playwright/e2e/composer/composer.spec.ts index e7be457f83e..0e369b600b0 100644 --- a/playwright/e2e/composer/composer.spec.ts +++ b/playwright/e2e/composer/composer.spec.ts @@ -319,5 +319,75 @@ test.describe("Composer", () => { ); }); }); + + test.describe("Drafts", () => { + test("drafts with rich and plain text", async ({ page, app }) => { + // Set up a second room to swtich to, to test drafts + const firstRoomname = "Composing Room"; + const secondRoomname = "Second Composing Room"; + await app.client.createRoom({ name: secondRoomname }); + + // Composer is visible + const composer = page.locator("div[contenteditable=true]"); + await expect(composer).toBeVisible(); + + // Type some formatted text + await composer.pressSequentially("my "); + await composer.press(`${CtrlOrMeta}+KeyB`); + await composer.pressSequentially("bold"); + + // Change to plain text mode + await page.getByRole("button", { name: "Hide formatting" }).click(); + + // Change to another room and back again + await app.viewRoomByName(secondRoomname); + await app.viewRoomByName(firstRoomname); + + // assert the markdown + await expect(page.locator("div[contenteditable=true]", { hasText: "my __bold__" })).toBeVisible(); + + // Change to plain text mode and assert the markdown + await page.getByRole("button", { name: "Show formatting" }).click(); + + // Change to another room and back again + await app.viewRoomByName(secondRoomname); + await app.viewRoomByName(firstRoomname); + + // Send the message and assert the message + await page.getByRole("button", { name: "Send message" }).click(); + await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my bold")).toBeVisible(); + }); + + test("draft with replies", async ({ page, app }) => { + // Set up a second room to swtich to, to test drafts + const firstRoomname = "Composing Room"; + const secondRoomname = "Second Composing Room"; + await app.client.createRoom({ name: secondRoomname }); + + // Composer is visible + const composer = page.locator("div[contenteditable=true]"); + await expect(composer).toBeVisible(); + + // Send a message + await composer.pressSequentially("my first message"); + await page.getByRole("button", { name: "Send message" }).click(); + + // Click reply + const tile = page.locator(".mx_EventTile_last"); + await tile.hover(); + await tile.getByRole("button", { name: "Reply", exact: true }).click(); + + // Type reply text + await composer.pressSequentially("my reply"); + + // Change to another room and back again + await app.viewRoomByName(secondRoomname); + await app.viewRoomByName(firstRoomname); + + // Assert reply mode and reply text + await expect(page.getByText("Replying")).toBeVisible(); + await expect(page.locator("div[contenteditable=true]", { hasText: "my reply" })).toBeVisible(); + }); + }); }); }); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index bb4b4c72453..947e24143f7 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -64,6 +64,7 @@ import { VoiceBroadcastInfoState } from "../../../voice-broadcast"; import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog"; import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; +import { logger } from "matrix-js-sdk/src/logger"; let instanceCount = 0; @@ -109,6 +110,12 @@ interface IState { initialComposerContent: string; } +type WysiwygComposerState = { + content: string; + isRichText: boolean; + replyEventId?: string; +}; + export class MessageComposer extends React.Component { private dispatcherRef?: string; private messageComposerInput = createRef(); @@ -127,13 +134,34 @@ export class MessageComposer extends React.Component { isRichTextEnabled: true, }; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); + this.context = context; // otherwise React will only set it prior to render due to type def above + VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); + window.addEventListener("beforeunload", this.saveWysiwygEditorState); + const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); + let isRichTextEnabled = true; + let initialComposerContent = ""; + if (isWysiwygLabEnabled) { + const wysiwygState = this.restoreWysiwygEditorState(); + if (wysiwygState) { + isRichTextEnabled = wysiwygState.isRichText; + initialComposerContent = wysiwygState.content; + if (wysiwygState.replyEventId) { + dis.dispatch({ + action: "reply_to_event", + event: this.props.room.findEventById(wysiwygState.replyEventId), + context: this.context.timelineRenderingType, + }); + } + } + } + this.state = { isComposerEmpty: true, - composerContent: "", + composerContent: initialComposerContent, haveRecording: false, recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast isMenuOpen: false, @@ -141,9 +169,9 @@ export class MessageComposer extends React.Component { showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"), showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"), showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast), - isWysiwygLabEnabled: SettingsStore.getValue("feature_wysiwyg_composer"), - isRichTextEnabled: true, - initialComposerContent: "", + isWysiwygLabEnabled: isWysiwygLabEnabled, + isRichTextEnabled: isRichTextEnabled, + initialComposerContent: initialComposerContent, }; this.instanceId = instanceCount++; @@ -154,6 +182,52 @@ export class MessageComposer extends React.Component { SettingsStore.monitorSetting("feature_wysiwyg_composer", null); } + private get editorStateKey(): string { + let key = `mx_wysiwyg_state_${this.props.room.roomId}`; + if (this.props.relation?.rel_type === THREAD_RELATION_TYPE.name) { + key += `_${this.props.relation.event_id}`; + } + return key; + } + + private restoreWysiwygEditorState(): WysiwygComposerState | undefined { + const json = localStorage.getItem(this.editorStateKey); + if (json) { + try { + const state: WysiwygComposerState = JSON.parse(json); + return state; + } catch (e) { + logger.error(e); + } + } + return undefined; + } + + private saveWysiwygEditorState = (): void => { + if (this.shouldSaveWysiwygEditorState()) { + const { isRichTextEnabled, composerContent } = this.state; + const replyEventId = this.props.replyToEvent ? this.props.replyToEvent.getId() : undefined; + const item: WysiwygComposerState = { + content: composerContent, + isRichText: isRichTextEnabled, + replyEventId: replyEventId, + }; + localStorage.setItem(this.editorStateKey, JSON.stringify(item)); + } else { + this.clearStoredEditorState(); + } + }; + + // should save state when wysiwyg is enabled and has contents or reply is open + private shouldSaveWysiwygEditorState = (): boolean => { + const { isWysiwygLabEnabled, isComposerEmpty } = this.state; + return isWysiwygLabEnabled && (!isComposerEmpty || !!this.props.replyToEvent); + }; + + private clearStoredEditorState(): void { + localStorage.removeItem(this.editorStateKey); + } + private get voiceRecording(): Optional { return this._voiceRecording; } @@ -265,6 +339,8 @@ export class MessageComposer extends React.Component { UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`); UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize); + window.removeEventListener("beforeunload", this.saveWysiwygEditorState); + this.saveWysiwygEditorState(); // clean up our listeners by setting our cached recording to falsy (see internal setter) this.voiceRecording = null; } From 2c92308e83616282dbe4546ee0ff134428c32b24 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 24 Jun 2024 10:28:56 +0100 Subject: [PATCH 2/9] test drafts in threads --- playwright/e2e/composer/composer.spec.ts | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/playwright/e2e/composer/composer.spec.ts b/playwright/e2e/composer/composer.spec.ts index 0e369b600b0..25a5440bf35 100644 --- a/playwright/e2e/composer/composer.spec.ts +++ b/playwright/e2e/composer/composer.spec.ts @@ -388,6 +388,41 @@ test.describe("Composer", () => { await expect(page.getByText("Replying")).toBeVisible(); await expect(page.locator("div[contenteditable=true]", { hasText: "my reply" })).toBeVisible(); }); + + test("draft in threads", async ({ page, app }) => { + // Set up a second room to swtich to, to test drafts + const firstRoomname = "Composing Room"; + const secondRoomname = "Second Composing Room"; + await app.client.createRoom({ name: secondRoomname }); + + // Composer is visible + const composer = page.locator("div[contenteditable=true]"); + await expect(composer).toBeVisible(); + + // Send a message + await composer.pressSequentially("my first message"); + await page.getByRole("button", { name: "Send message" }).click(); + + // Click reply + const tile = page.locator(".mx_EventTile_last"); + await tile.hover(); + await tile.getByRole("button", { name: "Reply in thread" }).click(); + + const thread = page.locator(".mx_ThreadView"); + const threadComposer = thread.locator("div[contenteditable=true]"); + + // Type threaded text + await threadComposer.pressSequentially("my threaded message"); + + // Change to another room and back again + await app.viewRoomByName(secondRoomname); + await app.viewRoomByName(firstRoomname); + + // Assert threaded draft + await expect( + thread.locator("div[contenteditable=true]", { hasText: "my threaded message" }), + ).toBeVisible(); + }); }); }); }); From ba9523d5f8659a4f640d65ae15e69496ffbe34a4 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 24 Jun 2024 10:45:49 +0100 Subject: [PATCH 3/9] lint --- src/components/views/rooms/MessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 947e24143f7..d1038722d04 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -26,6 +26,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { Optional } from "matrix-events-sdk"; import { Tooltip } from "@vector-im/compound-web"; +import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -64,7 +65,6 @@ import { VoiceBroadcastInfoState } from "../../../voice-broadcast"; import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog"; import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; -import { logger } from "matrix-js-sdk/src/logger"; let instanceCount = 0; From 57b51a0cb0d9724ff07eb4cea9fe5275a1512309 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 24 Jun 2024 16:46:34 +0100 Subject: [PATCH 4/9] Add unit test. --- .../views/rooms/MessageComposer.tsx | 2 +- .../views/rooms/MessageComposer-test.tsx | 67 ++++++++++++++----- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index d1038722d04..c95fe387e23 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -160,7 +160,7 @@ export class MessageComposer extends React.Component { } this.state = { - isComposerEmpty: true, + isComposerEmpty: initialComposerContent?.length === 0, composerContent: initialComposerContent, haveRecording: false, recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 1aea150a8cd..d435ee7067a 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -16,7 +16,7 @@ limitations under the License. import * as React from "react"; import { EventType, MatrixEvent, Room, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix"; -import { act, render, screen } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { @@ -42,17 +42,13 @@ import SettingsStore from "../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; import dis from "../../../../src/dispatcher/dispatcher"; import { E2EStatus } from "../../../../src/utils/ShieldUtils"; -import { addTextToComposerRTL } from "../../../test-utils/composer"; +import { addTextToComposer, addTextToComposerRTL } from "../../../test-utils/composer"; import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore"; import { Action } from "../../../../src/dispatcher/actions"; import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../src/voice-broadcast"; import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; -jest.mock("../../../../src/components/views/rooms/wysiwyg_composer", () => ({ - SendWysiwygComposer: jest.fn().mockImplementation(() =>
), -})); - const openStickerPicker = async (): Promise => { await act(async () => { await userEvent.click(screen.getByLabelText("More options")); @@ -467,12 +463,49 @@ describe("MessageComposer", () => { }); }); - it("should render SendWysiwygComposer when enabled", () => { + it("wysiwyg correctly persists state to and from localStorage", async () => { const room = mkStubRoom("!roomId:server", "Room 1", cli); + const messateText = "Test Text"; SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); + const { renderResult, rawComponent } = wrapAndRender({ room }); + const { unmount, rerender } = renderResult; + + await act(async () => { + await flushPromises(); + }); + + const key = `mx_wysiwyg_state_${room.roomId}`; + + await act(async () => { + await userEvent.click(renderResult.getByRole("textbox")); + }); + fireEvent.input(screen.getByRole("textbox"), { + data: messateText, + inputType: "insertText", + }); + + await waitFor(() => expect(renderResult.getByRole("textbox")).toHaveTextContent(messateText)); - wrapAndRender({ room }); - expect(screen.getByTestId("wysiwyg-composer")).toBeInTheDocument(); + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // assert there is state persisted + expect(localStorage.getItem(key)).toBeNull(); + + // ensure the right state was persisted to localStorage + unmount(); + + // assert the persisted state + expect(JSON.parse(localStorage.getItem(key)!)).toStrictEqual({ + content: messateText, + isRichText: true, + }); + + // ensure the correct state is re-loaded + rerender(rawComponent); + await waitFor(() => expect(renderResult.getByRole("textbox")).toHaveTextContent(messateText)); }); }); @@ -506,14 +539,16 @@ function wrapAndRender( permalinkCreator: new RoomPermalinkCreator(room), }; + const getRawComponent = (props = {}, context = roomContext, client = mockClient) => ( + + + + + + ); return { - renderResult: render( - - - - - , - ), + rawComponent: getRawComponent(props, roomContext, mockClient), + renderResult: render(getRawComponent(props, roomContext, mockClient)), roomContext, }; } From cd973b2062647428d9caef199e644f232b6f577b Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 25 Jun 2024 14:22:31 +0200 Subject: [PATCH 5/9] Fix test failure --- .../views/rooms/MessageComposer-test.tsx | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index d435ee7067a..cdbb08c5e05 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -72,12 +72,6 @@ const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording); }; -const shouldClearModal = async (): Promise => { - afterEach(async () => { - await clearAllModals(); - }); -}; - const expectVoiceMessageRecordingTriggered = (): void => { // Checking for the voice message dialog text, if no mic can be found. // By this we know at least that starting a voice message was triggered. @@ -92,7 +86,8 @@ describe("MessageComposer", () => { mockPlatformPeg(); }); - afterEach(() => { + afterEach(async () => { + await clearAllModals(); jest.useRealTimers(); SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); @@ -411,8 +406,6 @@ describe("MessageComposer", () => { await flushPromises(); }); - shouldClearModal(); - it("should try to start a voice message", () => { expectVoiceMessageRecordingTriggered(); }); @@ -426,8 +419,6 @@ describe("MessageComposer", () => { await waitEnoughCyclesForModal(); }); - shouldClearModal(); - it("should not start a voice message and display the info dialog", async () => { expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument(); expect(screen.getByText("Can't start voice message")).toBeInTheDocument(); @@ -442,8 +433,6 @@ describe("MessageComposer", () => { await waitEnoughCyclesForModal(); }); - shouldClearModal(); - it("should try to start a voice message and should not display the info dialog", async () => { expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument(); expectVoiceMessageRecordingTriggered(); @@ -466,7 +455,7 @@ describe("MessageComposer", () => { it("wysiwyg correctly persists state to and from localStorage", async () => { const room = mkStubRoom("!roomId:server", "Room 1", cli); const messateText = "Test Text"; - SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); + await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); const { renderResult, rawComponent } = wrapAndRender({ room }); const { unmount, rerender } = renderResult; @@ -477,14 +466,14 @@ describe("MessageComposer", () => { const key = `mx_wysiwyg_state_${room.roomId}`; await act(async () => { - await userEvent.click(renderResult.getByRole("textbox")); + await userEvent.click(screen.getByRole("textbox")); }); fireEvent.input(screen.getByRole("textbox"), { data: messateText, inputType: "insertText", }); - await waitFor(() => expect(renderResult.getByRole("textbox")).toHaveTextContent(messateText)); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messateText)); // Wait for event dispatch to happen await act(async () => { @@ -505,7 +494,7 @@ describe("MessageComposer", () => { // ensure the correct state is re-loaded rerender(rawComponent); - await waitFor(() => expect(renderResult.getByRole("textbox")).toHaveTextContent(messateText)); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messateText)); }); }); From 2741659ccba0eb73d4d197474b16cab4a5569ff1 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 25 Jun 2024 13:40:52 +0100 Subject: [PATCH 6/9] Remove unused import --- test/components/views/rooms/MessageComposer-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index cdbb08c5e05..144c23f9d1b 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -42,7 +42,7 @@ import SettingsStore from "../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; import dis from "../../../../src/dispatcher/dispatcher"; import { E2EStatus } from "../../../../src/utils/ShieldUtils"; -import { addTextToComposer, addTextToComposerRTL } from "../../../test-utils/composer"; +import { addTextToComposerRTL } from "../../../test-utils/composer"; import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore"; import { Action } from "../../../../src/dispatcher/actions"; import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../src/voice-broadcast"; From 482268e7704c37685476e4f7ec1deab64d55f193 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 7 Aug 2024 23:38:07 +0100 Subject: [PATCH 7/9] Clean up wysiwyg drafts and add test. --- src/DraftCleaner.ts | 14 +++++++++++--- src/components/views/rooms/MessageComposer.tsx | 5 ++++- test/components/structures/MatrixChat-test.tsx | 12 ++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/DraftCleaner.ts b/src/DraftCleaner.ts index 5e6c1cbae7f..cede027f223 100644 --- a/src/DraftCleaner.ts +++ b/src/DraftCleaner.ts @@ -18,6 +18,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/SendMessageComposer"; +import { WYSIWYG_EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/MessageComposer"; // The key used to persist the the timestamp we last cleaned up drafts export const DRAFT_LAST_CLEANUP_KEY = "mx_draft_cleanup"; @@ -61,14 +62,21 @@ function shouldCleanupDrafts(): boolean { } /** - * Clear all drafts for the CIDER editor if the room does not exist in the known rooms. + * Clear all drafts for the CIDER and WYSIWYG editors 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; + if (!keyName) continue; + let roomId: string | undefined = undefined; + if (keyName.startsWith(EDITOR_STATE_STORAGE_PREFIX)) { + roomId = keyName.slice(EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0]; + } + if (keyName.startsWith(WYSIWYG_EDITOR_STATE_STORAGE_PREFIX)) { + roomId = keyName.slice(WYSIWYG_EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0]; + } + if (!roomId) 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}`); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 5d82156bc14..b99c1c401ef 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -66,6 +66,9 @@ import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStart import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; +// The prefix used when persisting editor drafts to localstorage. +export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_"; + let instanceCount = 0; interface ISendButtonProps { @@ -183,7 +186,7 @@ export class MessageComposer extends React.Component { } private get editorStateKey(): string { - let key = `mx_wysiwyg_state_${this.props.room.roomId}`; + let key = WYSIWYG_EDITOR_STATE_STORAGE_PREFIX + this.props.room.roomId; if (this.props.relation?.rel_type === THREAD_RELATION_TYPE.name) { key += `_${this.props.relation.event_id}`; } diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 3b49ec21394..8d7f07e7dfe 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -624,6 +624,18 @@ describe("", () => { expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).toBeNull(); }); + it("should clean up wysiwyg drafts", async () => { + Date.now = jest.fn(() => timestamp); + localStorage.setItem(`mx_wysiwyg_state_${roomId}`, "fake_content"); + localStorage.setItem(`mx_wysiwyg_state_${unknownRoomId}`, "fake_content"); + await getComponentAndWaitForReady(); + mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing); + // let things settle + await flushPromises(); + expect(localStorage.getItem(`mx_wysiwyg_state_${roomId}`)).not.toBeNull(); + expect(localStorage.getItem(`mx_wysiwyg_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"); From 53e0523c6b3073267c5038ea4f77cdfd9187c64e Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 19 Aug 2024 18:34:22 +0200 Subject: [PATCH 8/9] Fix typo --- test/components/views/rooms/MessageComposer-test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 144c23f9d1b..10db976887e 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -454,7 +454,7 @@ describe("MessageComposer", () => { it("wysiwyg correctly persists state to and from localStorage", async () => { const room = mkStubRoom("!roomId:server", "Room 1", cli); - const messateText = "Test Text"; + const messageText = "Test Text"; await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); const { renderResult, rawComponent } = wrapAndRender({ room }); const { unmount, rerender } = renderResult; @@ -469,11 +469,11 @@ describe("MessageComposer", () => { await userEvent.click(screen.getByRole("textbox")); }); fireEvent.input(screen.getByRole("textbox"), { - data: messateText, + data: messageText, inputType: "insertText", }); - await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messateText)); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText)); // Wait for event dispatch to happen await act(async () => { @@ -488,13 +488,13 @@ describe("MessageComposer", () => { // assert the persisted state expect(JSON.parse(localStorage.getItem(key)!)).toStrictEqual({ - content: messateText, + content: messageText, isRichText: true, }); // ensure the correct state is re-loaded rerender(rawComponent); - await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messateText)); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText)); }); }); From 48c6a99277932fa91474bffc60fb8898819fc482 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 22 Aug 2024 13:36:30 +0100 Subject: [PATCH 9/9] Add timeout to allow for wasm loading. --- test/components/views/rooms/MessageComposer-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 10db976887e..6480a84007f 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -495,7 +495,7 @@ describe("MessageComposer", () => { // ensure the correct state is re-loaded rerender(rawComponent); await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(messageText)); - }); + }, 10000); }); function wrapAndRender(