+
-
+
{ children?.(ref, wysiwyg) }
);
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts
new file mode 100644
index 00000000000..99a89589ee4
--- /dev/null
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts
@@ -0,0 +1,27 @@
+/*
+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.
+*/
+
+import { RefObject, useMemo } from "react";
+
+export function useComposerFunctions(ref: RefObject
) {
+ return useMemo(() => ({
+ clear: () => {
+ if (ref.current) {
+ ref.current.innerHTML = '';
+ }
+ },
+ }), [ref]);
+}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts
index 414b6df45c5..06839ab262a 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts
@@ -20,7 +20,7 @@ import { useCallback } from "react";
import { useSettingValue } from "../../../../../hooks/useSettings";
export function useInputEventProcessor(onSend: () => void) {
- const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend") as boolean;
+ const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend");
return useCallback((event: WysiwygInputEvent) => {
if (event instanceof ClipboardEvent) {
return event;
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts
new file mode 100644
index 00000000000..abf2a6a6d27
--- /dev/null
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts
@@ -0,0 +1,25 @@
+/*
+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.
+*/
+
+import { RefObject, useEffect } from "react";
+
+export function usePlainTextInitialization(initialContent: string, ref: RefObject) {
+ useEffect(() => {
+ if (ref.current) {
+ ref.current.innerText = initialContent;
+ }
+ }, [ref, initialContent]);
+}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts
new file mode 100644
index 00000000000..02063ddcfb0
--- /dev/null
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts
@@ -0,0 +1,50 @@
+/*
+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.
+*/
+
+import { KeyboardEvent, SyntheticEvent, useCallback, useRef } from "react";
+
+import { useSettingValue } from "../../../../../hooks/useSettings";
+
+function isDivElement(target: EventTarget): target is HTMLDivElement {
+ return target instanceof HTMLDivElement;
+}
+
+export function usePlainTextListeners(onChange: (content: string) => void, onSend: () => void) {
+ const ref = useRef();
+ const send = useCallback((() => {
+ if (ref.current) {
+ ref.current.innerHTML = '';
+ }
+ onSend();
+ }), [ref, onSend]);
+
+ const onInput = useCallback((event: SyntheticEvent) => {
+ if (isDivElement(event.target)) {
+ onChange(event.target.innerHTML);
+ }
+ }, [onChange]);
+
+ 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();
+ }
+ }, [isCtrlEnter, send]);
+
+ return { ref, onInput, onPaste: onInput, onKeyDown };
+}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSetCursorPosition.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSetCursorPosition.ts
new file mode 100644
index 00000000000..ef14d44255d
--- /dev/null
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSetCursorPosition.ts
@@ -0,0 +1,27 @@
+/*
+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.
+*/
+
+import { RefObject, useEffect } from "react";
+
+import { setCursorPositionAtTheEnd } from "./utils";
+
+export function useSetCursorPosition(disabled: boolean, ref: RefObject) {
+ useEffect(() => {
+ if (ref.current && !disabled) {
+ setCursorPositionAtTheEnd(ref.current);
+ }
+ }, [ref, disabled]);
+}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts
index b7c18f19c20..49c6302d5b3 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts
@@ -15,7 +15,6 @@ limitations under the License.
*/
import { RefObject, useCallback, useRef } from "react";
-import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
import { Action } from "../../../../../dispatcher/actions";
@@ -23,11 +22,12 @@ import { ActionPayload } from "../../../../../dispatcher/payloads";
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
import { useDispatcher } from "../../../../../hooks/useDispatcher";
import { focusComposer } from "./utils";
+import { ComposerFunctions } from "../types";
export function useWysiwygSendActionHandler(
disabled: boolean,
composerElement: RefObject,
- wysiwyg: FormattingFunctions,
+ composerFunctions: ComposerFunctions,
) {
const roomContext = useRoomContext();
const timeoutId = useRef();
@@ -45,12 +45,12 @@ export function useWysiwygSendActionHandler(
focusComposer(composerElement, context, roomContext, timeoutId);
break;
case Action.ClearAndFocusSendMessageComposer:
- wysiwyg.clear();
+ composerFunctions.clear();
focusComposer(composerElement, context, roomContext, timeoutId);
break;
// TODO: case Action.ComposerInsert: - see SendMessageComposer
}
- }, [disabled, composerElement, wysiwyg, timeoutId, roomContext]);
+ }, [disabled, composerElement, composerFunctions, timeoutId, roomContext]);
useDispatcher(defaultDispatcher, handler);
}
diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts
index eab855e0868..bfaf526f72e 100644
--- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts
+++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts
@@ -41,3 +41,14 @@ export function focusComposer(
);
}
}
+
+export function setCursorPositionAtTheEnd(element: HTMLElement) {
+ const range = document.createRange();
+ range.selectNodeContents(element);
+ range.collapse(false);
+ const selection = document.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ element.focus();
+}
diff --git a/src/components/views/rooms/wysiwyg_composer/types.ts b/src/components/views/rooms/wysiwyg_composer/types.ts
new file mode 100644
index 00000000000..96095abebfd
--- /dev/null
+++ b/src/components/views/rooms/wysiwyg_composer/types.ts
@@ -0,0 +1,19 @@
+/*
+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 type ComposerFunctions = {
+ clear: () => void;
+};
diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts
index cc0d2235bf9..6d8a9f218e4 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts
@@ -16,8 +16,11 @@ limitations under the License.
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 {
@@ -39,6 +42,18 @@ function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
return (mxReply && mxReply.outerHTML) || "";
}
+function getTextReplyFallback(mxEvent: MatrixEvent): string {
+ const body = mxEvent.getContent().body;
+ if (typeof body !== 'string') {
+ return "";
+ }
+ const lines = body.split("\n").map(l => l.trim());
+ if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
+ return `${lines[0]}\n\n`;
+ }
+ return "";
+}
+
interface CreateMessageContentParams {
relation?: IEventRelation;
replyToEvent?: MatrixEvent;
@@ -49,6 +64,7 @@ interface CreateMessageContentParams {
export function createMessageContent(
message: string,
+ isHTML: boolean,
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }:
CreateMessageContentParams,
): IContent {
@@ -56,6 +72,7 @@ export function createMessageContent(
const isEditing = Boolean(editedEvent);
const isReply = isEditing ? Boolean(editedEvent?.replyEventId) : Boolean(replyToEvent);
+ const isReplyAndEditing = isEditing && isReply;
/*const isEmote = containsEmote(model);
if (isEmote) {
@@ -67,37 +84,44 @@ export function createMessageContent(
model = unescapeMessage(model);*/
// const body = textSerialize(model);
- const body = message;
+
+ // TODO remove this ugly hack for replace br tag
+ const body = isHTML && htmlToPlainText(message) || message.replace(/
/g, '\n');
+ const bodyPrefix = isReplyAndEditing && getTextReplyFallback(editedEvent) || '';
+ const formattedBodyPrefix = isReplyAndEditing && getHtmlReplyFallback(editedEvent) || '';
const content: IContent = {
// TODO emote
- // msgtype: isEmote ? "m.emote" : "m.text",
msgtype: MsgType.Text,
- body: body,
+ // TODO when available, use HTML --> Plain text conversion from wysiwyg rust model
+ body: isEditing ? `${bodyPrefix} * ${body}` : body,
};
// TODO markdown support
- /*const formattedBody = htmlSerializeIfNeeded(model, {
- forceHTML: !!replyToEvent,
- useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
- });*/
- const formattedBody = message;
+ const isMarkdownEnabled = SettingsStore.getValue("MessageComposerInput.useMarkdown");
+ const formattedBody =
+ isHTML ?
+ message :
+ isMarkdownEnabled ?
+ htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply }) :
+ null;
if (formattedBody) {
content.format = "org.matrix.custom.html";
-
- const htmlPrefix = isReply && isEditing ? getHtmlReplyFallback(editedEvent) : '';
- content.formatted_body = isEditing ? `${htmlPrefix} * ${formattedBody}` : formattedBody;
+ content.formatted_body = isEditing ? `${formattedBodyPrefix} * ${formattedBody}` : formattedBody;
}
if (isEditing) {
content['m.new_content'] = {
"msgtype": content.msgtype,
"body": body,
- "format": "org.matrix.custom.html",
- 'formatted_body': formattedBody,
};
+
+ if (formattedBody) {
+ content['m.new_content'].format = "org.matrix.custom.html";
+ content['m.new_content']['formatted_body'] = formattedBody;
+ }
}
const newRelation = isEditing ?
diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
index dbea29c848c..d84392c18e7 100644
--- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts
+++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts
@@ -44,7 +44,8 @@ interface SendMessageParams {
}
export function sendMessage(
- html: string,
+ message: string,
+ isHTML: boolean,
{ roomContext, mxClient, ...params }: SendMessageParams,
) {
const { relation, replyToEvent } = params;
@@ -76,7 +77,8 @@ export function sendMessage(
if (!content) {
content = createMessageContent(
- html,
+ message,
+ isHTML,
params,
);
}
@@ -167,7 +169,7 @@ export function editMessage(
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
}*/
- const editContent = createMessageContent(html, { editedEvent });
+ const editContent = createMessageContent(html, true, { editedEvent });
const newContent = editContent["m.new_content"];
const shouldSend = true;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8af41255fce..b892c67bf92 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1881,6 +1881,8 @@
"Voice Message": "Voice Message",
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
"Poll": "Poll",
+ "Show plain text": "Show plain text",
+ "Show formatting": "Show formatting",
"Bold": "Bold",
"Italics": "Italics",
"Strikethrough": "Strikethrough",
diff --git a/src/utils/room/htmlToPlaintext.ts b/src/utils/room/htmlToPlaintext.ts
new file mode 100644
index 00000000000..883db8d360d
--- /dev/null
+++ b/src/utils/room/htmlToPlaintext.ts
@@ -0,0 +1,19 @@
+/*
+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 72fd52be574..00d6a43f977 100644
--- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx
@@ -163,7 +163,7 @@ describe('EditWysiwygComposer', () => {
// Then
const expectedContent = {
- "body": mockContent,
+ "body": ` * ${mockContent}`,
"format": "org.matrix.custom.html",
"formatted_body": ` * ${mockContent}`,
"m.new_content": {
@@ -186,6 +186,7 @@ describe('EditWysiwygComposer', () => {
it('Should focus when receiving an Action.FocusEditMessageComposer action', async () => {
// Given we don't have focus
customRender();
+ screen.getByLabelText('Bold').focus();
expect(screen.getByRole('textbox')).not.toHaveFocus();
// When we send the right action
@@ -201,6 +202,7 @@ describe('EditWysiwygComposer', () => {
it('Should not focus when disabled', async () => {
// Given we don't have focus and we are disabled
customRender(true);
+ screen.getByLabelText('Bold').focus();
expect(screen.getByRole('textbox')).not.toHaveFocus();
// When we send an action that would cause us to get focus
diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
index 20148b802a7..c85692d221a 100644
--- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx
@@ -24,8 +24,10 @@ import RoomContext from "../../../../../src/contexts/RoomContext";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
-import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
+import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
+import * as useComposerFunctions
+ from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions";
const mockClear = jest.fn();
@@ -68,83 +70,112 @@ describe('SendWysiwygComposer', () => {
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
- const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => {
+ const customRender = (
+ onChange = (_content: string) => void 0,
+ onSend = () => void 0,
+ disabled = false,
+ isRichTextEnabled = true) => {
return render(
-
+
,
);
};
- it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
- // Given we don't have focus
- customRender(jest.fn(), jest.fn());
- expect(screen.getByRole('textbox')).not.toHaveFocus();
+ it('Should render WysiwygComposer when isRichTextEnabled is at true', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), false, true);
- // When we send the right action
- defaultDispatcher.dispatch({
- action: Action.FocusSendMessageComposer,
- context: null,
- });
-
- // Then the component gets the focus
- await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
- });
-
- it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => {
- // Given we don't have focus
- customRender(jest.fn(), jest.fn());
- expect(screen.getByRole('textbox')).not.toHaveFocus();
-
- // When we send the right action
- defaultDispatcher.dispatch({
- action: Action.ClearAndFocusSendMessageComposer,
- context: null,
- });
-
- // Then the component gets the focus
- await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
- expect(mockClear).toBeCalledTimes(1);
+ // Then
+ expect(screen.getByTestId('WysiwygComposer')).toBeTruthy();
});
- it('Should focus when receiving a reply_to_event action', async () => {
- // Given we don't have focus
- customRender(jest.fn(), jest.fn());
- expect(screen.getByRole('textbox')).not.toHaveFocus();
+ it('Should render PlainTextComposer when isRichTextEnabled is at false', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), false, false);
- // When we send the right action
- defaultDispatcher.dispatch({
- action: "reply_to_event",
- context: null,
- });
-
- // Then the component gets the focus
- await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
+ // Then
+ expect(screen.getByTestId('PlainTextComposer')).toBeTruthy();
});
- it('Should not focus when disabled', async () => {
- // Given we don't have focus and we are disabled
- customRender(jest.fn(), jest.fn(), true);
- expect(screen.getByRole('textbox')).not.toHaveFocus();
-
- // When we send an action that would cause us to get focus
- defaultDispatcher.dispatch({
- action: Action.FocusSendMessageComposer,
- context: null,
- });
- // (Send a second event to exercise the clearTimeout logic)
- defaultDispatcher.dispatch({
- action: Action.FocusSendMessageComposer,
- context: null,
+ describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])(
+ 'Should focus when receiving an Action.FocusSendMessageComposer action',
+ ({ isRichTextEnabled }) => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
+ // Given we don't have focus
+ customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
+
+ // When we send the right action
+ defaultDispatcher.dispatch({
+ action: Action.FocusSendMessageComposer,
+ context: null,
+ });
+
+ // Then the component gets the focus
+ await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
+ });
+
+ it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => {
+ // Given we don't have focus
+ const mock = jest.spyOn(useComposerFunctions, 'useComposerFunctions');
+ mock.mockReturnValue({ clear: mockClear });
+ customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
+
+ // When we send the right action
+ defaultDispatcher.dispatch({
+ action: Action.ClearAndFocusSendMessageComposer,
+ context: null,
+ });
+
+ // Then the component gets the focus
+ await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
+ expect(mockClear).toBeCalledTimes(1);
+
+ mock.mockRestore();
+ });
+
+ it('Should focus when receiving a reply_to_event action', async () => {
+ // Given we don't have focus
+ customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
+
+ // When we send the right action
+ defaultDispatcher.dispatch({
+ action: "reply_to_event",
+ context: null,
+ });
+
+ // Then the component gets the focus
+ await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
+ });
+
+ it('Should not focus when disabled', async () => {
+ // Given we don't have focus and we are disabled
+ customRender(jest.fn(), jest.fn(), true, isRichTextEnabled);
+ expect(screen.getByRole('textbox')).not.toHaveFocus();
+
+ // When we send an action that would cause us to get focus
+ defaultDispatcher.dispatch({
+ action: Action.FocusSendMessageComposer,
+ context: null,
+ });
+ // (Send a second event to exercise the clearTimeout logic)
+ defaultDispatcher.dispatch({
+ action: Action.FocusSendMessageComposer,
+ context: null,
+ });
+
+ // Wait for event dispatch to happen
+ await flushPromises();
+
+ // Then we don't get it because we are disabled
+ expect(screen.getByRole('textbox')).not.toHaveFocus();
+ });
});
-
- // Wait for event dispatch to happen
- await new Promise((r) => setTimeout(r, 200));
-
- // Then we don't get it because we are disabled
- expect(screen.getByRole('textbox')).not.toHaveFocus();
- });
});
diff --git a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx
new file mode 100644
index 00000000000..5d1b03020cf
--- /dev/null
+++ b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx
@@ -0,0 +1,94 @@
+/*
+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.
+*/
+
+import React from 'react';
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import { PlainTextComposer }
+ from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
+
+// Work around missing ClipboardEvent type
+class MyClipboardEvent {}
+window.ClipboardEvent = MyClipboardEvent as any;
+
+describe('PlainTextComposer', () => {
+ const customRender = (
+ onChange = (_content: string) => void 0,
+ onSend = () => void 0,
+ disabled = false,
+ initialContent?: string) => {
+ return render(
+ ,
+ );
+ };
+
+ it('Should have contentEditable at false when disabled', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), true);
+
+ // Then
+ expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false");
+ });
+
+ it('Should have focus', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), false);
+
+ // Then
+ expect(screen.getByRole('textbox')).toHaveFocus();
+ });
+
+ it('Should call onChange handler', async () => {
+ // When
+ const content = 'content';
+ const onChange = jest.fn();
+ customRender(onChange, jest.fn());
+ await userEvent.type(screen.getByRole('textbox'), content);
+
+ // Then
+ expect(onChange).toBeCalledWith(content);
+ });
+
+ it('Should call onSend when Enter is pressed', async () => {
+ //When
+ const onSend = jest.fn();
+ customRender(jest.fn(), onSend);
+ await userEvent.type(screen.getByRole('textbox'), '{enter}');
+
+ // Then it sends a message
+ expect(onSend).toBeCalledTimes(1);
+ });
+
+ it('Should clear textbox content when clear is called', async () => {
+ //When
+ let composer;
+ render(
+
+ { (ref, composerFunctions) => {
+ composer = composerFunctions;
+ return null;
+ } }
+ ,
+ );
+ await userEvent.type(screen.getByRole('textbox'), 'content');
+ expect(screen.getByRole('textbox').innerHTML).toBe('content');
+ composer.clear();
+
+ // Then
+ expect(screen.getByRole('textbox').innerHTML).toBeFalsy();
+ });
+});
diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx
index e7e21ca839c..7e3db04abcf 100644
--- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx
+++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx
@@ -19,10 +19,6 @@ import React from "react";
import { render, screen } from "@testing-library/react";
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
-import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
-import { IRoomState } from "../../../../../../src/components/structures/RoomView";
-import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../../test-utils";
-import RoomContext from "../../../../../../src/contexts/RoomContext";
import { WysiwygComposer }
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
@@ -54,32 +50,14 @@ jest.mock("@matrix-org/matrix-wysiwyg", () => ({
}));
describe('WysiwygComposer', () => {
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- const mockClient = createTestClient();
- const mockEvent = mkEvent({
- type: "m.room.message",
- room: 'myfakeroom',
- user: 'myfakeuser',
- content: { "msgtype": "m.text", "body": "Replying to this" },
- event: true,
- });
- const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any;
- mockRoom.findEventById = jest.fn(eventId => {
- return eventId === mockEvent.getId() ? mockEvent : null;
- });
-
- const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
-
- const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => {
+ const customRender = (
+ onChange = (_content: string) => void 0,
+ onSend = () => void 0,
+ disabled = false,
+ initialContent?: string) => {
return render(
-
-
-
-
- ,
+ ,
+
);
};
@@ -91,6 +69,14 @@ describe('WysiwygComposer', () => {
expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false");
});
+ it('Should have focus', () => {
+ // When
+ customRender(jest.fn(), jest.fn(), false);
+
+ // Then
+ expect(screen.getByRole('textbox')).toHaveFocus();
+ });
+
it('Should call onChange handler', (done) => {
const html = 'html';
customRender((content) => {
@@ -104,7 +90,7 @@ describe('WysiwygComposer', () => {
const onSend = jest.fn();
customRender(jest.fn(), onSend);
- // When we tell its inputEventProcesser that the user pressed Enter
+ // When we tell its inputEventProcessor that the user pressed Enter
const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" });
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
inputEventProcessor(event, wysiwyg);
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 a4335b2bf10..4c7028749c4 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts
@@ -40,11 +40,11 @@ describe('createMessageContent', () => {
it("Should create html message", () => {
// When
- const content = createMessageContent(message, { permalinkCreator });
+ const content = createMessageContent(message, true, { permalinkCreator });
// Then
expect(content).toEqual({
- "body": message,
+ "body": "hello world",
"format": "org.matrix.custom.html",
"formatted_body": message,
"msgtype": "m.text",
@@ -53,11 +53,11 @@ describe('createMessageContent', () => {
it('Should add reply to message content', () => {
// When
- const content = createMessageContent(message, { permalinkCreator, replyToEvent: mockEvent });
+ const content = createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
// Then
expect(content).toEqual({
- "body": "> Replying to this\n\nhello world",
+ "body": "> Replying to this\n\nhello world",
"format": "org.matrix.custom.html",
"formatted_body": "In reply to" +
" myfakeuser"+
@@ -77,11 +77,11 @@ describe('createMessageContent', () => {
rel_type: "m.thread",
event_id: "myFakeThreadId",
};
- const content = createMessageContent(message, { permalinkCreator, relation });
+ const content = createMessageContent(message, true, { permalinkCreator, relation });
// Then
expect(content).toEqual({
- "body": message,
+ "body": "hello world",
"format": "org.matrix.custom.html",
"formatted_body": message,
"msgtype": "m.text",
@@ -110,16 +110,16 @@ describe('createMessageContent', () => {
event: true,
});
const content =
- createMessageContent(message, { permalinkCreator, editedEvent });
+ createMessageContent(message, true, { permalinkCreator, editedEvent });
// Then
expect(content).toEqual({
- "body": message,
+ "body": " * hello world",
"format": "org.matrix.custom.html",
"formatted_body": ` * ${message}`,
"msgtype": "m.text",
"m.new_content": {
- "body": message,
+ "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 9d13f281760..0829b19adb2 100644
--- a/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
+++ b/test/components/views/rooms/wysiwyg_composer/utils/message-test.ts
@@ -65,7 +65,7 @@ describe('message', () => {
describe('sendMessage', () => {
it('Should not send empty html message', async () => {
// When
- await sendMessage('', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
+ await sendMessage('', true, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
// Then
expect(mockClient.sendMessage).toBeCalledTimes(0);
@@ -74,11 +74,15 @@ describe('message', () => {
it('Should send html message', async () => {
// When
- await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
+ await sendMessage(
+ message,
+ true,
+ { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator },
+ );
// Then
const expectedContent = {
- "body": "hello world",
+ "body": "hello world",
"format": "org.matrix.custom.html",
"formatted_body": "hello world",
"msgtype": "m.text",
@@ -97,7 +101,7 @@ describe('message', () => {
});
// When
- await sendMessage(message, {
+ await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
@@ -112,7 +116,7 @@ describe('message', () => {
});
const expectedContent = {
- "body": "> My reply\n\nhello world",
+ "body": "> My reply\n\nhello world",
"format": "org.matrix.custom.html",
"formatted_body": "In reply to" +
" myfakeuser2" +
@@ -130,7 +134,11 @@ describe('message', () => {
it('Should scroll to bottom after sending a html message', async () => {
// When
SettingsStore.setValue("scrollToBottomOnMessageSent", null, SettingLevel.DEVICE, true);
- await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
+ await sendMessage(
+ message,
+ true,
+ { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator },
+ );
// Then
expect(spyDispatcher).toBeCalledWith(
@@ -140,7 +148,11 @@ describe('message', () => {
it('Should handle emojis', async () => {
// When
- await sendMessage('🎉', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
+ await sendMessage(
+ '🎉',
+ false,
+ { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator },
+ );
// Then
expect(spyDispatcher).toBeCalledWith(
@@ -203,7 +215,7 @@ describe('message', () => {
// Then
const { msgtype, format } = mockEvent.getContent();
const expectedContent = {
- "body": newMessage,
+ "body": ` * ${newMessage}`,
"formatted_body": ` * ${newMessage}`,
"m.new_content": {
"body": "Replying to this new content",