Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

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 all 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.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",
Expand Down
26 changes: 15 additions & 11 deletions src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand All @@ -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 = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 />}>
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
3 changes: 2 additions & 1 deletion src/components/views/rooms/wysiwyg_composer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
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,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";
Expand Down
34 changes: 19 additions & 15 deletions src/components/views/rooms/wysiwyg_composer/utils/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand All @@ -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",
Expand All @@ -63,18 +71,14 @@ 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

// TODO replace emotion end of message ?

// TODO quick reaction

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

// don't bother sending an empty message
if (!content.body.trim()) {
return;
Expand All @@ -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,
Expand Down Expand Up @@ -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>({
Expand All @@ -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;
Expand All @@ -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();
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) {
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 @@ -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" });
});
});
Expand Down
Loading