From 432ce3ca31a830faf3887c6fb3859dc47caaf55d Mon Sep 17 00:00:00 2001 From: alunturner <56027671+alunturner@users.noreply.github.com> Date: Wed, 4 Jan 2023 12:57:09 +0000 Subject: [PATCH] Improve switching between rich and plain editing modes (#9776) * allows switching between modes that retains formatting * updates rich text composer dependency to 0.13.0 (@matrix-org/matrix-wysiwyg) * improves handling of enter keypresses when ctrlEnterTosend setting is true in plain text editor * changes the message event content when using the new editor * adds tests for the changes to the plain text editor --- package.json | 2 +- .../views/rooms/MessageComposer.tsx | 26 +-- .../DynamicImportWysiwygComposer.tsx | 16 ++ .../hooks/usePlainTextListeners.ts | 41 ++++- .../views/rooms/wysiwyg_composer/index.ts | 3 +- .../utils/createMessageContent.ts | 19 +-- .../rooms/wysiwyg_composer/utils/message.ts | 34 ++-- src/utils/room/htmlToPlaintext.ts | 19 --- .../EditWysiwygComposer-test.tsx | 5 +- .../components/PlainTextComposer-test.tsx | 150 +++++++++++++++++- .../utils/createMessageContent-test.ts | 30 ++-- .../wysiwyg_composer/utils/message-test.ts | 77 ++++++++- yarn.lock | 8 +- 13 files changed, 336 insertions(+), 94 deletions(-) delete mode 100644 src/utils/room/htmlToPlaintext.ts diff --git a/package.json b/package.json index 4d5a725671..37f8a2d895 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.3.0", - "@matrix-org/matrix-wysiwyg": "^0.11.0", + "@matrix-org/matrix-wysiwyg": "^0.13.0", "@matrix-org/react-sdk-module-api": "^0.0.3", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index b50b285f8f..d7e9a5f71a 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -54,9 +54,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import { Features } from "../../../settings/Settings"; import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; -import { SendWysiwygComposer, sendMessage } from "./wysiwyg_composer/"; +import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/"; import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -import { htmlToPlainText } from "../../../utils/room/htmlToPlaintext"; import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; import { SdkContextClass } from "../../../contexts/SDKContext"; @@ -333,7 +332,7 @@ export class MessageComposer extends React.Component { if (this.state.isWysiwygLabEnabled) { const { permalinkCreator, relation, replyToEvent } = this.props; - sendMessage(this.state.composerContent, this.state.isRichTextEnabled, { + await sendMessage(this.state.composerContent, this.state.isRichTextEnabled, { mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, @@ -358,14 +357,19 @@ export class MessageComposer extends React.Component { }); }; - private onRichTextToggle = () => { - this.setState((state) => ({ - isRichTextEnabled: !state.isRichTextEnabled, - initialComposerContent: !state.isRichTextEnabled - ? state.composerContent - : // TODO when available use rust model plain text - htmlToPlainText(state.composerContent), - })); + private onRichTextToggle = async () => { + const { richToPlain, plainToRich } = await getConversionFunctions(); + + const { isRichTextEnabled, composerContent } = this.state; + const convertedContent = isRichTextEnabled + ? await richToPlain(composerContent) + : await plainToRich(composerContent); + + this.setState({ + isRichTextEnabled: !isRichTextEnabled, + composerContent: convertedContent, + initialComposerContent: convertedContent, + }); }; private onVoiceStoreUpdate = () => { diff --git a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx index 65a365b06d..2799f494de 100644 --- a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx @@ -16,9 +16,25 @@ limitations under the License. import React, { ComponentProps, lazy, Suspense } from "react"; +// we need to import the types for TS, but do not import the sendMessage +// function to avoid importing from "@matrix-org/matrix-wysiwyg" +import { SendMessageParams } from "./utils/message"; + const SendComposer = lazy(() => import("./SendWysiwygComposer")); const EditComposer = lazy(() => import("./EditWysiwygComposer")); +export const dynamicImportSendMessage = async (message: string, isHTML: boolean, params: SendMessageParams) => { + const { sendMessage } = await import("./utils/message"); + + return sendMessage(message, isHTML, params); +}; + +export const dynamicImportConversionFunctions = async () => { + const { richToPlain, plainToRich } = await import("@matrix-org/matrix-wysiwyg"); + + return { richToPlain, plainToRich }; +}; + export function DynamicImportSendWysiwygComposer(props: ComponentProps) { return ( }> diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index fdab7647b9..958396c419 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -17,11 +17,22 @@ limitations under the License. import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react"; import { useSettingValue } from "../../../../../hooks/useSettings"; +import { IS_MAC, Key } from "../../../../../Keyboard"; function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; } +// Hitting enter inside the editor inserts an editable div, initially containing a
+// For correct display, first replace this pattern with a newline character and then remove divs +// noting that they are used to delimit paragraphs +function amendInnerHtml(text: string) { + return text + .replace(/

<\/div>/g, "\n") // this is pressing enter then not typing + .replace(/
/g, "\n") // this is from pressing enter, then typing inside the div + .replace(/<\/div>/g, ""); +} + export function usePlainTextListeners( initialContent?: string, onChange?: (content: string) => void, @@ -44,25 +55,39 @@ export function usePlainTextListeners( [onChange], ); + const enterShouldSend = !useSettingValue("MessageComposerInput.ctrlEnterToSend"); const onInput = useCallback( (event: SyntheticEvent) => { if (isDivElement(event.target)) { - setText(event.target.innerHTML); + // if enterShouldSend, we do not need to amend the html before setting text + const newInnerHTML = enterShouldSend ? event.target.innerHTML : amendInnerHtml(event.target.innerHTML); + setText(newInnerHTML); } }, - [setText], + [setText, enterShouldSend], ); - const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend"); const onKeyDown = useCallback( (event: KeyboardEvent) => { - if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) { - event.preventDefault(); - event.stopPropagation(); - send(); + if (event.key === Key.ENTER) { + const sendModifierIsPressed = IS_MAC ? event.metaKey : event.ctrlKey; + + // if enter should send, send if the user is not pushing shift + if (enterShouldSend && !event.shiftKey) { + event.preventDefault(); + event.stopPropagation(); + send(); + } + + // if enter should not send, send only if the user is pushing ctrl/cmd + if (!enterShouldSend && sendModifierIsPressed) { + event.preventDefault(); + event.stopPropagation(); + send(); + } } }, - [isCtrlEnter, send], + [enterShouldSend, send], ); return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText }; diff --git a/src/components/views/rooms/wysiwyg_composer/index.ts b/src/components/views/rooms/wysiwyg_composer/index.ts index c82f59ca89..92cf97032b 100644 --- a/src/components/views/rooms/wysiwyg_composer/index.ts +++ b/src/components/views/rooms/wysiwyg_composer/index.ts @@ -17,5 +17,6 @@ limitations under the License. export { DynamicImportSendWysiwygComposer as SendWysiwygComposer, DynamicImportEditWysiwygComposer as EditWysiwygComposer, + dynamicImportSendMessage as sendMessage, + dynamicImportConversionFunctions as getConversionFunctions, } from "./DynamicImportWysiwygComposer"; -export { sendMessage } from "./utils/message"; diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index a6c2146e67..0819b758d8 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg"; import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; -import { htmlSerializeFromMdIfNeeded } from "../../../../../editor/serialize"; import SettingsStore from "../../../../../settings/SettingsStore"; import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks"; import { addReplyToMessageContent } from "../../../../../utils/Reply"; -import { htmlToPlainText } from "../../../../../utils/room/htmlToPlaintext"; // Merges favouring the given relation function attachRelation(content: IContent, relation?: IEventRelation): void { @@ -62,7 +61,7 @@ interface CreateMessageContentParams { editedEvent?: MatrixEvent; } -export function createMessageContent( +export async function createMessageContent( message: string, isHTML: boolean, { @@ -72,7 +71,7 @@ export function createMessageContent( includeReplyLegacyFallback = true, editedEvent, }: CreateMessageContentParams, -): IContent { +): Promise { // TODO emote ? const isEditing = Boolean(editedEvent); @@ -90,26 +89,22 @@ export function createMessageContent( // const body = textSerialize(model); - // TODO remove this ugly hack for replace br tag - const body = (isHTML && htmlToPlainText(message)) || message.replace(/
/g, "\n"); + // if we're editing rich text, the message content is pure html + // BUT if we're not, the message content will be plain text + const body = isHTML ? await richToPlain(message) : message; const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || ""; const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || ""; const content: IContent = { // TODO emote msgtype: MsgType.Text, - // TODO when available, use HTML --> Plain text conversion from wysiwyg rust model body: isEditing ? `${bodyPrefix} * ${body}` : body, }; // TODO markdown support const isMarkdownEnabled = SettingsStore.getValue("MessageComposerInput.useMarkdown"); - const formattedBody = isHTML - ? message - : isMarkdownEnabled - ? htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply }) - : null; + const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message) : null; if (formattedBody) { content.format = "org.matrix.custom.html"; diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts index 8039bbe194..18878a97d1 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer"; -import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; @@ -34,7 +34,7 @@ import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import { createMessageContent } from "./createMessageContent"; import { isContentModified } from "./isContentModified"; -interface SendMessageParams { +export interface SendMessageParams { mxClient: MatrixClient; relation?: IEventRelation; replyToEvent?: MatrixEvent; @@ -43,10 +43,18 @@ interface SendMessageParams { includeReplyLegacyFallback?: boolean; } -export function sendMessage(message: string, isHTML: boolean, { roomContext, mxClient, ...params }: SendMessageParams) { +export async function sendMessage( + message: string, + isHTML: boolean, + { roomContext, mxClient, ...params }: SendMessageParams, +) { const { relation, replyToEvent } = params; const { room } = roomContext; - const { roomId } = room; + const roomId = room?.roomId; + + if (!roomId) { + return; + } const posthogEvent: ComposerEvent = { eventName: "Composer", @@ -63,7 +71,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC }*/ PosthogAnalytics.instance.trackEvent(posthogEvent); - let content: IContent; + const content = await createMessageContent(message, isHTML, params); // TODO slash comment @@ -71,10 +79,6 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC // TODO quick reaction - if (!content) { - content = createMessageContent(message, isHTML, params); - } - // don't bother sending an empty message if (!content.body.trim()) { return; @@ -84,7 +88,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC decorateStartSendingTime(content); } - const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; + const threadId = relation?.event_id && relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; const prom = doMaybeLocalRoomAction( roomId, @@ -139,7 +143,7 @@ interface EditMessageParams { editorStateTransfer: EditorStateTransfer; } -export function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) { +export async function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) { const editedEvent = editorStateTransfer.getEvent(); PosthogAnalytics.instance.trackEvent({ @@ -156,7 +160,7 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd); this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON); }*/ - const editContent = createMessageContent(html, true, { editedEvent }); + const editContent = await createMessageContent(html, true, { editedEvent }); const newContent = editContent["m.new_content"]; const shouldSend = true; @@ -174,10 +178,10 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr let response: Promise | undefined; - // If content is modified then send an updated event into the room - if (isContentModified(newContent, editorStateTransfer)) { - const roomId = editedEvent.getRoomId(); + const roomId = editedEvent.getRoomId(); + // If content is modified then send an updated event into the room + if (isContentModified(newContent, editorStateTransfer) && roomId) { // TODO Slash Commands if (shouldSend) { diff --git a/src/utils/room/htmlToPlaintext.ts b/src/utils/room/htmlToPlaintext.ts deleted file mode 100644 index 4b0272b4e1..0000000000 --- a/src/utils/room/htmlToPlaintext.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2022 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. -*/ - -export function htmlToPlainText(html: string) { - return new DOMParser().parseFromString(html, "text/html").documentElement.textContent; -} diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 7f64be2437..d9343208c4 100644 --- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -229,7 +229,10 @@ describe("EditWysiwygComposer", () => { }, "msgtype": "m.text", }; - expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent); + await waitFor(() => + expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent), + ); + expect(spyDispatcher).toBeCalledWith({ action: "message_sent" }); }); }); diff --git a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx index ed421f50af..6d16a3b152 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx @@ -19,6 +19,8 @@ import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { PlainTextComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer"; +import * as mockUseSettingsHook from "../../../../../../src/hooks/useSettings"; +import * as mockKeyboard from "../../../../../../src/Keyboard"; describe("PlainTextComposer", () => { const customRender = ( @@ -37,6 +39,17 @@ describe("PlainTextComposer", () => { ); }; + let mockUseSettingValue: jest.SpyInstance; + beforeEach(() => { + // defaults for these tests are: + // ctrlEnterToSend is false + mockUseSettingValue = jest.spyOn(mockUseSettingsHook, "useSettingValue").mockReturnValue(false); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it("Should have contentEditable at false when disabled", () => { // When customRender(jest.fn(), jest.fn(), true); @@ -64,7 +77,7 @@ describe("PlainTextComposer", () => { expect(onChange).toBeCalledWith(content); }); - it("Should call onSend when Enter is pressed", async () => { + it("Should call onSend when Enter is pressed when ctrlEnterToSend is false", async () => { //When const onSend = jest.fn(); customRender(jest.fn(), onSend); @@ -74,9 +87,134 @@ describe("PlainTextComposer", () => { expect(onSend).toBeCalledTimes(1); }); + it("Should not call onSend when Enter is pressed when ctrlEnterToSend is true", async () => { + //When + mockUseSettingValue.mockReturnValue(true); + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + await userEvent.type(screen.getByRole("textbox"), "{enter}"); + + // Then it does not send a message + expect(onSend).toBeCalledTimes(0); + }); + + it("Should only call onSend when ctrl+enter is pressed when ctrlEnterToSend is true on windows", async () => { + //When + mockUseSettingValue.mockReturnValue(true); + + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + const textBox = screen.getByRole("textbox"); + await userEvent.type(textBox, "hello"); + + // Then it does NOT send a message on enter + await userEvent.type(textBox, "{enter}"); + expect(onSend).toBeCalledTimes(0); + + // Then it does NOT send a message on windows+enter + await userEvent.type(textBox, "{meta>}{enter}{meta/}"); + expect(onSend).toBeCalledTimes(0); + + // Then it does send a message on ctrl+enter + await userEvent.type(textBox, "{control>}{enter}{control/}"); + expect(onSend).toBeCalledTimes(1); + }); + + it("Should only call onSend when cmd+enter is pressed when ctrlEnterToSend is true on mac", async () => { + //When + mockUseSettingValue.mockReturnValue(true); + Object.defineProperty(mockKeyboard, "IS_MAC", { value: true }); + + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + const textBox = screen.getByRole("textbox"); + await userEvent.type(textBox, "hello"); + + // Then it does NOT send a message on enter + await userEvent.type(textBox, "{enter}"); + expect(onSend).toBeCalledTimes(0); + + // Then it does NOT send a message on ctrl+enter + await userEvent.type(textBox, "{control>}{enter}{control/}"); + expect(onSend).toBeCalledTimes(0); + + // Then it does send a message on cmd+enter + await userEvent.type(textBox, "{meta>}{enter}{meta/}"); + expect(onSend).toBeCalledTimes(1); + }); + + it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is false", async () => { + //When + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + const textBox = screen.getByRole("textbox"); + const inputWithShiftEnter = "new{Shift>}{enter}{/Shift}line"; + const expectedInnerHtml = "new\nline"; + + await userEvent.click(textBox); + await userEvent.type(textBox, inputWithShiftEnter); + + // Then it does not send a message, but inserts a newline character + expect(onSend).toBeCalledTimes(0); + expect(textBox.innerHTML).toBe(expectedInnerHtml); + }); + + it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is true", async () => { + //When + mockUseSettingValue.mockReturnValue(true); + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + const textBox = screen.getByRole("textbox"); + const keyboardInput = "new{Shift>}{enter}{/Shift}line"; + const expectedInnerHtml = "new\nline"; + + await userEvent.click(textBox); + await userEvent.type(textBox, keyboardInput); + + // Then it does not send a message, but inserts a newline character + expect(onSend).toBeCalledTimes(0); + expect(textBox.innerHTML).toBe(expectedInnerHtml); + }); + + it("Should not insert div and br tags when enter is pressed when ctrlEnterToSend is true", async () => { + //When + mockUseSettingValue.mockReturnValue(true); + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + const textBox = screen.getByRole("textbox"); + const enterThenTypeHtml = "
hello { + //When + mockUseSettingValue.mockReturnValue(true); + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + const textBox = screen.getByRole("textbox"); + const defaultEnterHtml = "

{ //When - let composer; + let composer: { + clear: () => void; + insertText: (text: string) => void; + }; + render( {(ref, composerFunctions) => { @@ -85,9 +223,11 @@ describe("PlainTextComposer", () => { }} , ); + await userEvent.type(screen.getByRole("textbox"), "content"); expect(screen.getByRole("textbox").innerHTML).toBe("content"); - composer.clear(); + + composer!.clear(); // Then expect(screen.getByRole("textbox").innerHTML).toBeFalsy(); @@ -112,7 +252,7 @@ describe("PlainTextComposer", () => { render(); // Then - expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("false"); + expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("false"); expect(editor).toBe(screen.getByRole("textbox")); // When @@ -126,7 +266,7 @@ describe("PlainTextComposer", () => { }); // Then - expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("true"); + expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("true"); jest.useRealTimers(); (global.ResizeObserver as jest.Mock).mockRestore(); diff --git a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts index e654186617..340a4c1af2 100644 --- a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts @@ -24,7 +24,7 @@ describe("createMessageContent", () => { return "$$permalink$$"; }, } as RoomPermalinkCreator; - const message = "hello world"; + const message = "hello world"; const mockEvent = mkEvent({ type: "m.room.message", room: "myfakeroom", @@ -37,31 +37,31 @@ describe("createMessageContent", () => { jest.resetAllMocks(); }); - it("Should create html message", () => { + it("Should create html message", async () => { // When - const content = createMessageContent(message, true, { permalinkCreator }); + const content = await createMessageContent(message, true, { permalinkCreator }); // Then expect(content).toEqual({ - body: "hello world", + body: "*__hello__ world*", format: "org.matrix.custom.html", formatted_body: message, msgtype: "m.text", }); }); - it("Should add reply to message content", () => { + it("Should add reply to message content", async () => { // When - const content = createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent }); + const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent }); // Then expect(content).toEqual({ - "body": "> Replying to this\n\nhello world", + "body": "> Replying to this\n\n*__hello__ world*", "format": "org.matrix.custom.html", "formatted_body": '
In reply to' + ' myfakeuser' + - "
Replying to this
hello world", + "
Replying to thishello world", "msgtype": "m.text", "m.relates_to": { "m.in_reply_to": { @@ -71,17 +71,17 @@ describe("createMessageContent", () => { }); }); - it("Should add relation to message", () => { + it("Should add relation to message", async () => { // When const relation = { rel_type: "m.thread", event_id: "myFakeThreadId", }; - const content = createMessageContent(message, true, { permalinkCreator, relation }); + const content = await createMessageContent(message, true, { permalinkCreator, relation }); // Then expect(content).toEqual({ - "body": "hello world", + "body": "*__hello__ world*", "format": "org.matrix.custom.html", "formatted_body": message, "msgtype": "m.text", @@ -92,7 +92,7 @@ describe("createMessageContent", () => { }); }); - it("Should add fields related to edition", () => { + it("Should add fields related to edition", async () => { // When const editedEvent = mkEvent({ type: "m.room.message", @@ -110,16 +110,16 @@ describe("createMessageContent", () => { }, event: true, }); - const content = createMessageContent(message, true, { permalinkCreator, editedEvent }); + const content = await createMessageContent(message, true, { permalinkCreator, editedEvent }); // Then expect(content).toEqual({ - "body": " * hello world", + "body": " * *__hello__ world*", "format": "org.matrix.custom.html", "formatted_body": ` * ${message}`, "msgtype": "m.text", "m.new_content": { - body: "hello world", + body: "*__hello__ world*", format: "org.matrix.custom.html", formatted_body: message, msgtype: "m.text", diff --git a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts index ceb00ade79..733d5c117e 100644 --- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts @@ -70,6 +70,79 @@ describe("message", () => { expect(spyDispatcher).toBeCalledTimes(0); }); + it("Should not send message when there is no roomId", async () => { + // When + const mockRoomWithoutId = mkStubRoom("", "room without id", mockClient) as any; + const mockRoomContextWithoutId: IRoomState = getRoomContext(mockRoomWithoutId, {}); + + await sendMessage(message, true, { + roomContext: mockRoomContextWithoutId, + mxClient: mockClient, + permalinkCreator, + }); + + // Then + expect(mockClient.sendMessage).toBeCalledTimes(0); + expect(spyDispatcher).toBeCalledTimes(0); + }); + + describe("calls client.sendMessage with", () => { + it("a null argument if SendMessageParams is missing relation", async () => { + // When + await sendMessage(message, true, { + roomContext: defaultRoomContext, + mxClient: mockClient, + permalinkCreator, + }); + + // Then + expect(mockClient.sendMessage).toHaveBeenCalledWith(expect.anything(), null, expect.anything()); + }); + it("a null argument if SendMessageParams has relation but relation is missing event_id", async () => { + // When + await sendMessage(message, true, { + roomContext: defaultRoomContext, + mxClient: mockClient, + permalinkCreator, + relation: {}, + }); + + // Then + expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything()); + }); + it("a null argument if SendMessageParams has relation but rel_type does not match THREAD_RELATION_TYPE.name", async () => { + // When + await sendMessage(message, true, { + roomContext: defaultRoomContext, + mxClient: mockClient, + permalinkCreator, + relation: { + event_id: "valid_id", + rel_type: "m.does_not_match", + }, + }); + + // Then + expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything()); + }); + + it("the event_id if SendMessageParams has relation and rel_type matches THREAD_RELATION_TYPE.name", async () => { + // When + await sendMessage(message, true, { + roomContext: defaultRoomContext, + mxClient: mockClient, + permalinkCreator, + relation: { + event_id: "valid_id", + rel_type: "m.thread", + }, + }); + + // Then + expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), "valid_id", expect.anything()); + }); + }); + it("Should send html message", async () => { // When await sendMessage(message, true, { @@ -80,7 +153,7 @@ describe("message", () => { // Then const expectedContent = { - body: "hello world", + body: "*__hello__ world*", format: "org.matrix.custom.html", formatted_body: "hello world", msgtype: "m.text", @@ -114,7 +187,7 @@ describe("message", () => { }); const expectedContent = { - "body": "> My reply\n\nhello world", + "body": "> My reply\n\n*__hello__ world*", "format": "org.matrix.custom.html", "formatted_body": '
In reply to' + diff --git a/yarn.lock b/yarn.lock index 0a0353d772..e7e33c7db3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1525,10 +1525,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== -"@matrix-org/matrix-wysiwyg@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.11.0.tgz#3000ee809a3e38242c5da47bef17c572582f2f6b" - integrity sha512-B16iLfNnW4PKG4fpDuwJVc0QUrUUqTkhwJ/kxzawcxwVNmWbsPCWJ3hkextYrN2gqRL1d4CNASkNbWLCNNiXhA== +"@matrix-org/matrix-wysiwyg@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.13.0.tgz#e643df4e13cdc5dbf9285740bc0ce2aef9873c16" + integrity sha512-MCeTj4hkl0snjlygd1v+mEEOgaN6agyjAVjJEbvEvP/BaYaDiPEXMTDaRQrcUt3OIY53UNhm1DDEn4yPTn83Jg== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14"