diff --git a/package.json b/package.json index 654d7930548..df29cd7e509 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.5.0", - "@matrix-org/matrix-wysiwyg": "^2.3.0", + "@matrix-org/matrix-wysiwyg": "^2.3.1", "@matrix-org/react-sdk-module-api": "^1.0.0", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 1c3167214f2..2cac7f0a8ca 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -372,7 +372,7 @@ export class MessageComposer extends React.Component { const { isRichTextEnabled, composerContent } = this.state; const convertedContent = isRichTextEnabled ? await richToPlain(composerContent) - : await plainToRich(composerContent); + : await plainToRich(composerContent, false); this.setState({ isRichTextEnabled: !isRichTextEnabled, diff --git a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx index a7d79b617b4..ba41bd4055d 100644 --- a/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/DynamicImportWysiwygComposer.tsx @@ -39,8 +39,27 @@ export const dynamicImportSendMessage = async ( }; export const dynamicImportConversionFunctions = async (): Promise<{ + /** + * Creates a rust model from rich text input (html) and uses it to generate the plain text equivalent (which may + * contain markdown). The return value must be used to set `.innerHTML` (rather than `.innerText`) to + * ensure that HTML entities are correctly interpreted, and to prevent newline characters being turned into `
`. + * + * @param rich - html to convert + * @returns a string of plain text that may contain markdown + */ richToPlain(rich: string): Promise; - plainToRich(plain: string): Promise; + + /** + * Creates a rust model from plain text input (interpreted as markdown) and uses it to generate the rich text + * equivalent. Output can be formatted for display in the composer or for sending in a Matrix message. + * + * @param plain - plain text to convert. Note: when reading the plain text from the editor element, be sure to + * use `.innerHTML` (rather than `.innerText`) to ensure that punctuation characters are correctly HTML-encoded. + * @param inMessageFormat - `true` to format the return value for use as a message `formatted_body`. + * `false` to format it for writing to an editor element. + * @returns a string of html + */ + plainToRich(plain: string, inMessageFormat: boolean): Promise; }> => { const { richToPlain, plainToRich } = await retry(() => import("@matrix-org/matrix-wysiwyg"), RETRY_COUNT); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts index 45d0ba0d864..3528fda0105 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts @@ -18,8 +18,9 @@ import { RefObject, useEffect } from "react"; export function usePlainTextInitialization(initialContent = "", ref: RefObject): void { useEffect(() => { + // always read and write the ref.current using .innerHTML for consistency in linebreak and HTML entity handling if (ref.current) { - ref.current.innerText = initialContent; + ref.current.innerHTML = 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 index 39d13288111..6121f0c877f 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -31,16 +31,6 @@ function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; } -// Hitting enter inside the editor inserts an editable div, initially containing a
-// For correct display, first replace this pattern with a newline character and then remove divs -// noting that they are used to delimit paragraphs -function amendInnerHtml(text: string): string { - return text - .replace(/

<\/div>/g, "\n") // this is pressing enter then not typing - .replace(/
/g, "\n") // this is from pressing enter, then typing inside the div - .replace(/<\/div>/g, ""); -} - /** * React hook which generates all of the listeners and the ref to be attached to the editor. * @@ -100,9 +90,8 @@ export function usePlainTextListeners( } else if (isNotNull(ref) && isNotNull(ref.current)) { // if called with no argument, read the current innerHTML from the ref and amend it as per `onInput` const currentRefContent = ref.current.innerHTML; - const amendedContent = amendInnerHtml(currentRefContent); - setContent(amendedContent); - onChange?.(amendedContent); + setContent(currentRefContent); + onChange?.(currentRefContent); } }, [onChange, ref], @@ -113,16 +102,13 @@ export function usePlainTextListeners( // when a user selects a suggestion from the autocomplete menu const { suggestion, onSelect, handleCommand, handleMention, handleAtRoomMention } = useSuggestion(ref, setText); - const enterShouldSend = !useSettingValue("MessageComposerInput.ctrlEnterToSend"); const onInput = useCallback( (event: SyntheticEvent) => { if (isDivElement(event.target)) { - // 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(event.target.innerHTML); } }, - [setText, enterShouldSend], + [setText], ); const onPaste = useCallback( @@ -146,6 +132,7 @@ export function usePlainTextListeners( [eventRelation, mxClient, onInput, roomContext], ); + const enterShouldSend = !useSettingValue("MessageComposerInput.ctrlEnterToSend"); const onKeyDown = useCallback( (event: KeyboardEvent) => { // we need autocomplete to take priority when it is open for using enter to select diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index 440b489a583..aa469a5fd29 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -108,7 +108,7 @@ export async function createMessageContent( // TODO markdown support const isMarkdownEnabled = SettingsStore.getValue("MessageComposerInput.useMarkdown"); - const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message) : null; + const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message, true) : null; if (formattedBody) { content.format = "org.matrix.custom.html"; diff --git a/yarn.lock b/yarn.lock index 1ce7b0aec2a..a4e13553994 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1809,10 +1809,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0.tgz#766580036d4df12120ded223e13b5640e77db136" integrity sha512-ra/bcFdleC1iRNms2I96UXA0NvQYWpMsHrV5EfJRS7qV1PtnQNvgsvMfjMbkx8QT2ErEmIhsvB5fPCpfp8BSuw== -"@matrix-org/matrix-wysiwyg@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.3.0.tgz#7a815fb90600342cc74c03a3cc7c9908a1d15dd1" - integrity sha512-VtA+Bti2IdqpnpCNaTFHMjbpKXe4xHR+OWWJl/gjuYgn4NJO9lfeeEIv34ftC6dBh7R280JEiMxQ9mDcH0J54g== +"@matrix-org/matrix-wysiwyg@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.3.1.tgz#4b607323f3ffd8c332abeba7226010ecc031ed12" + integrity sha512-OxJvA+pSGdP2f55foZGEDmU2qvILFLLjV53MOgPw1F6zDAp8nDL1rPPIzFv1qgDj5W7d4Rzq7FnN25vINnAu+A== "@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"