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
This commit is contained in:
parent
3bcea5fb0b
commit
432ce3ca31
13 changed files with 336 additions and 94 deletions
|
@ -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",
|
||||
|
|
|
@ -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<IProps, IState> {
|
|||
|
||||
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<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
|
|
|
@ -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<typeof SendComposer>) {
|
||||
return (
|
||||
<Suspense fallback={<div />}>
|
||||
|
|
|
@ -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 <br />
|
||||
// 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><br><\/div>/g, "\n") // this is pressing enter then not typing
|
||||
.replace(/<div>/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<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||
const onInput = useCallback(
|
||||
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||
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<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
|
||||
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 };
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<IContent> {
|
||||
// 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(/<br>/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<boolean>("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";
|
||||
|
|
|
@ -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<ComposerEvent>(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<ComposerEvent>({
|
||||
|
@ -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<ISendEventResponse> | undefined;
|
||||
|
||||
// If content is modified then send an updated event into the room
|
||||
if (isContentModified(newContent, editorStateTransfer)) {
|
||||
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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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" });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = "<div>hello</div";
|
||||
|
||||
await userEvent.click(textBox);
|
||||
await userEvent.type(textBox, "{enter}hello");
|
||||
|
||||
// Then it does not send a message, but inserts a newline character
|
||||
expect(onSend).toBeCalledTimes(0);
|
||||
expect(textBox).not.toContainHTML(enterThenTypeHtml);
|
||||
});
|
||||
|
||||
it("Should not insert div tags when enter is pressed then user types more when ctrlEnterToSend is true", async () => {
|
||||
//When
|
||||
mockUseSettingValue.mockReturnValue(true);
|
||||
const onSend = jest.fn();
|
||||
customRender(jest.fn(), onSend);
|
||||
const textBox = screen.getByRole("textbox");
|
||||
const defaultEnterHtml = "<div><br></div";
|
||||
|
||||
await userEvent.click(textBox);
|
||||
await userEvent.type(textBox, "{enter}");
|
||||
|
||||
// Then it does not send a message, but inserts a newline character
|
||||
expect(onSend).toBeCalledTimes(0);
|
||||
expect(textBox).not.toContainHTML(defaultEnterHtml);
|
||||
});
|
||||
|
||||
it("Should clear textbox content when clear is called", async () => {
|
||||
//When
|
||||
let composer;
|
||||
let composer: {
|
||||
clear: () => void;
|
||||
insertText: (text: string) => void;
|
||||
};
|
||||
|
||||
render(
|
||||
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()}>
|
||||
{(ref, composerFunctions) => {
|
||||
|
@ -85,9 +223,11 @@ describe("PlainTextComposer", () => {
|
|||
}}
|
||||
</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(<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} />);
|
||||
|
||||
// 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();
|
||||
|
|
|
@ -24,7 +24,7 @@ describe("createMessageContent", () => {
|
|||
return "$$permalink$$";
|
||||
},
|
||||
} as RoomPermalinkCreator;
|
||||
const message = "<i><b>hello</b> world</i>";
|
||||
const message = "<em><b>hello</b> world</em>";
|
||||
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": "> <myfakeuser> Replying to this\n\nhello world",
|
||||
"body": "> <myfakeuser> Replying to this\n\n*__hello__ world*",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body":
|
||||
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
|
||||
' <a href="https://matrix.to/#/myfakeuser">myfakeuser</a>' +
|
||||
"<br>Replying to this</blockquote></mx-reply><i><b>hello</b> world</i>",
|
||||
"<br>Replying to this</blockquote></mx-reply><em><b>hello</b> world</em>",
|
||||
"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",
|
||||
|
|
|
@ -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: "<i><b>hello</b> world</i>",
|
||||
msgtype: "m.text",
|
||||
|
@ -114,7 +187,7 @@ describe("message", () => {
|
|||
});
|
||||
|
||||
const expectedContent = {
|
||||
"body": "> <myfakeuser2> My reply\n\nhello world",
|
||||
"body": "> <myfakeuser2> My reply\n\n*__hello__ world*",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body":
|
||||
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue