Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve switching between rich and plain editing modes #9776

Merged
merged 64 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
4169059
use new conversion function when switching editor
Dec 14, 2022
3133541
use textContent not innerHTML
Dec 14, 2022
e7a6210
use new conversion functions both ways
Dec 14, 2022
34619b7
use innerText to maintain linebreaks
Dec 14, 2022
6739232
revert to use innerHTML
Dec 14, 2022
80e3d93
tidy up handler function, use renamed functions
Dec 14, 2022
382a5a0
update method due to signature change in imported function
Dec 15, 2022
452e788
make message conversion work
Dec 15, 2022
211ddc8
make sendMessage function await the async call
Dec 15, 2022
816768f
make tests use async/await
Dec 15, 2022
92fa9c5
add comment
Dec 15, 2022
ff376e0
use new functions in createMessageContent, remove old functions
Dec 15, 2022
c7b205c
remove unused util file
Dec 15, 2022
252eb79
make cmd and enter command work to send plain message
Dec 16, 2022
5c81f2e
make enter work as expected with cmd enter setting true
Dec 16, 2022
4c1d774
remove console log
Dec 16, 2022
5da0ad2
add some tests
Dec 16, 2022
8186890
add extra tests, comments
Dec 16, 2022
aa12d4e
remove uses of fireEvent
Dec 16, 2022
04a604d
tidy up code, make newlines work properly
Dec 19, 2022
410efef
add comment
Dec 20, 2022
614b9d2
add test, tidy up test names
Dec 20, 2022
b6f8437
update RTE dependency to 0.12.0
Dec 20, 2022
841b120
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 20, 2022
8ba7e16
fix lint errors in test
Dec 20, 2022
5032fb5
fix type for jest spy
Dec 20, 2022
8609139
fix lint errors
Dec 20, 2022
66ed4be
fix strict errors in test file
Dec 20, 2022
88ae5ee
change to use dynamic import for conversion functions
Dec 20, 2022
d30a916
Merge remote-tracking branch 'origin/develop' into alunturner/psu-780…
Dec 20, 2022
79b2ff2
use dataset to remove type coercion
Dec 20, 2022
45126c0
dynamically import sendMessage due to it using wysiwyg lib functions
Dec 20, 2022
f5e5f99
fix three strict mode errors
Dec 21, 2022
7f6d514
fix all TS errors message.ts
Dec 21, 2022
e090162
Merge remote-tracking branch 'origin/develop' into alunturner/psu-780…
Dec 21, 2022
60bf211
edit 'hello world' strings to account for md in plain body
Dec 21, 2022
76bd5fd
fix failing test
Dec 21, 2022
6248abc
fix failing test
Dec 21, 2022
0dd6a79
add message test
Dec 21, 2022
a4253bf
add logic tests
Dec 22, 2022
fe5fe09
update wysiwyg to 0.13.0
Dec 22, 2022
56e363c
Merge remote-tracking branch 'origin/develop' into alunturner/psu-780…
Dec 22, 2022
401c9de
fix type error
Dec 22, 2022
f53e691
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 22, 2022
04abc08
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 22, 2022
d9eea32
export interface to allow typing in dynamic import
Dec 22, 2022
96f5226
add new dynamic import functions
Dec 22, 2022
c0a98c7
export new dynamically imported functions
Dec 22, 2022
140cde4
use dynamically imported functions
Dec 22, 2022
fc4a644
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 22, 2022
eda2b3d
Merge remote-tracking branch 'origin/develop' into alunturner/psu-780…
Dec 22, 2022
36ed59d
Merge remote-tracking branch 'origin/develop' into alunturner/psu-780…
Dec 23, 2022
3be6180
Merge remote-tracking branch 'origin/develop' into alunturner/psu-780…
Dec 23, 2022
42a5709
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 23, 2022
609a861
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 23, 2022
9aa7a09
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 23, 2022
fd8de61
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 23, 2022
ae12817
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 23, 2022
6fffe26
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 28, 2022
8e8925e
Merge branch 'develop' into alunturner/psu-780-message-content
Dec 28, 2022
626a20a
Merge branch 'develop' into alunturner/psu-780-message-content
alunturner Jan 3, 2023
06394d2
Merge branch 'develop' into alunturner/psu-780-message-content
alunturner Jan 3, 2023
f0e3541
Merge branch 'develop' into alunturner/psu-780-message-content
alunturner Jan 4, 2023
c9d05d0
Merge branch 'develop' into alunturner/psu-780-message-content
alunturner Jan 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.3.0",
"@matrix-org/matrix-wysiwyg": "^0.9.0",
"@matrix-org/matrix-wysiwyg": "^0.12.0",
"@matrix-org/react-sdk-module-api": "^0.0.3",
"@sentry/browser": "^7.0.0",
"@sentry/tracing": "^7.0.0",
Expand Down
23 changes: 13 additions & 10 deletions src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import React, { createRef, ReactNode } from "react";
import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg";
Copy link
Member

@t3chguy t3chguy Dec 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@alunturner alunturner Dec 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you're right this is going to cause the same issue that was looked at this morning. I'm sorting out lint/TS errors then will sort this. They will become dynamic imports.

import classNames from "classnames";
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
Expand Down Expand Up @@ -57,7 +58,6 @@ import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
import { VoiceBroadcastRecordingsStore } from "../../../voice-broadcast";
import { SendWysiwygComposer, sendMessage } 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";

Expand Down Expand Up @@ -334,7 +334,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,
Expand All @@ -359,14 +359,17 @@ 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 { isRichTextEnabled, composerContent } = this.state;
const convertedContent = isRichTextEnabled
? await richToPlain(composerContent)
: await plainToRich(composerContent);

this.setState({
isRichTextEnabled: !isRichTextEnabled,
composerContent: convertedContent,
initialComposerContent: convertedContent,
});
};

private onVoiceStoreUpdate = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))) {
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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -62,7 +61,7 @@ interface CreateMessageContentParams {
editedEvent?: MatrixEvent;
}

export function createMessageContent(
export async function createMessageContent(
message: string,
isHTML: boolean,
{
Expand All @@ -72,7 +71,7 @@ export function createMessageContent(
includeReplyLegacyFallback = true,
editedEvent,
}: CreateMessageContentParams,
): IContent {
): Promise<IContent> {
// TODO emote ?

const isEditing = Boolean(editedEvent);
Expand All @@ -90,15 +89,15 @@ 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,
};

Expand All @@ -108,8 +107,8 @@ export function createMessageContent(
const formattedBody = isHTML
? message
: isMarkdownEnabled
? htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply })
: null;
? await plainToRich(message)
: null;

if (formattedBody) {
content.format = "org.matrix.custom.html";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ 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;
Expand Down Expand Up @@ -72,7 +76,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
// TODO quick reaction

if (!content) {
content = createMessageContent(message, isHTML, params);
content = await createMessageContent(message, isHTML, params);
}

// don't bother sending an empty message
Expand Down
19 changes: 0 additions & 19 deletions src/utils/room/htmlToPlaintext.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ 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";
import * as mockUseSettingsHook from "../../../../../../src/hooks/useSettings";
import * as mockKeyboard from "../../../../../../src/Keyboard";

jest.mock("../../../../../../src/hooks/useSettings");

jest.mock("../../../../../../src/hooks/useSettings");

beforeEach(() => {
// defaults for these tests are:
// ctrlEnterToSend is false
mockUseSettingsHook.useSettingValue = jest.fn().mockReturnValue(false);
// platform is not mac
mockKeyboard.IS_MAC = false;
});



describe("PlainTextComposer", () => {
const customRender = (
Expand Down Expand Up @@ -64,7 +80,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);
Expand All @@ -74,6 +90,127 @@ describe("PlainTextComposer", () => {
expect(onSend).toBeCalledTimes(1);
});

it("Should not call onSend when Enter is pressed when ctrlEnterToSend is true", async () => {
//When
mockUseSettingsHook.useSettingValue.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
mockUseSettingsHook.useSettingValue.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
mockUseSettingsHook.useSettingValue.mockReturnValue(true);
mockKeyboard.IS_MAC = 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
mockUseSettingsHook.useSettingValue.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
mockUseSettingsHook.useSettingValue.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
mockUseSettingsHook.useSettingValue.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;
Expand Down
Loading