diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index 1bde32ca2b..440b489a58 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -18,8 +18,9 @@ import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg"; import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; import SettingsStore from "../../../../../settings/SettingsStore"; -import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks"; +import { parsePermalink, RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks"; import { addReplyToMessageContent } from "../../../../../utils/Reply"; +import { isNotNull } from "../../../../../Typeguards"; export const EMOTE_PREFIX = "/me "; @@ -94,8 +95,8 @@ export async function createMessageContent( } // 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; + // BUT if we're not, the message content will be plain text where we need to convert the mentions + const body = isHTML ? await richToPlain(message) : convertPlainTextToBody(message); const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || ""; const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || ""; @@ -141,3 +142,51 @@ export async function createMessageContent( return content; } + +/** + * Without a model, we need to manually amend mentions in uncontrolled message content + * to make sure that mentions meet the matrix specification. + * + * @param content - the output from the `MessageComposer` state when in plain text mode + * @returns - a string formatted with the mentions replaced as required + */ +function convertPlainTextToBody(content: string): string { + const document = new DOMParser().parseFromString(content, "text/html"); + const mentions = Array.from(document.querySelectorAll("a[data-mention-type]")); + + mentions.forEach((mention) => { + const mentionType = mention.getAttribute("data-mention-type"); + switch (mentionType) { + case "at-room": { + mention.replaceWith("@room"); + break; + } + case "user": { + const innerText = mention.innerHTML; + mention.replaceWith(innerText); + break; + } + case "room": { + // for this case we use parsePermalink to try and get the mx id + const href = mention.getAttribute("href"); + + // if the mention has no href attribute, leave it alone + if (href === null) break; + + // otherwise, attempt to parse the room alias or id from the href + const permalinkParts = parsePermalink(href); + + // then if we have permalink parts with a valid roomIdOrAlias, replace the + // room mention with that text + if (isNotNull(permalinkParts) && isNotNull(permalinkParts.roomIdOrAlias)) { + mention.replaceWith(permalinkParts.roomIdOrAlias); + } + break; + } + default: + break; + } + }); + + return document.body.innerHTML; +} 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 a77c7dc604..06194d86d3 100644 --- a/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts @@ -41,117 +41,146 @@ describe("createMessageContent", () => { jest.resetAllMocks(); }); - it("Should create html message", async () => { - // When - const content = await createMessageContent(message, true, { permalinkCreator }); + describe("Richtext composer input", () => { + it("Should create html message", async () => { + // When + const content = await createMessageContent(message, true, { permalinkCreator }); - // Then - expect(content).toEqual({ - body: "*__hello__ world*", - format: "org.matrix.custom.html", - formatted_body: message, - msgtype: "m.text", - }); - }); - - it("Should add reply to message content", async () => { - // When - const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent }); - - // Then - expect(content).toEqual({ - "body": "> Replying to this\n\n*__hello__ world*", - "format": "org.matrix.custom.html", - "formatted_body": - '
In reply to' + - ' myfakeuser' + - "
Replying to this
hello world", - "msgtype": "m.text", - "m.relates_to": { - "m.in_reply_to": { - event_id: mockEvent.getId(), - }, - }, - }); - }); - - it("Should add relation to message", async () => { - // When - const relation = { - rel_type: "m.thread", - event_id: "myFakeThreadId", - }; - const content = await createMessageContent(message, true, { permalinkCreator, relation }); - - // Then - expect(content).toEqual({ - "body": "*__hello__ world*", - "format": "org.matrix.custom.html", - "formatted_body": message, - "msgtype": "m.text", - "m.relates_to": { - event_id: "myFakeThreadId", - rel_type: "m.thread", - }, - }); - }); - - it("Should add fields related to edition", async () => { - // When - const editedEvent = mkEvent({ - type: "m.room.message", - room: "myfakeroom", - user: "myfakeuser2", - content: { - "msgtype": "m.text", - "body": "First message", - "formatted_body": "First Message", - "m.relates_to": { - "m.in_reply_to": { - event_id: "eventId", - }, - }, - }, - event: true, - }); - const content = await createMessageContent(message, true, { permalinkCreator, editedEvent }); - - // Then - expect(content).toEqual({ - "body": " * *__hello__ world*", - "format": "org.matrix.custom.html", - "formatted_body": ` * ${message}`, - "msgtype": "m.text", - "m.new_content": { + // Then + expect(content).toEqual({ body: "*__hello__ world*", format: "org.matrix.custom.html", formatted_body: message, msgtype: "m.text", - }, - "m.relates_to": { - event_id: editedEvent.getId(), - rel_type: "m.replace", - }, + }); + }); + + it("Should add reply to message content", async () => { + // When + const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent }); + + // Then + expect(content).toEqual({ + "body": "> Replying to this\n\n*__hello__ world*", + "format": "org.matrix.custom.html", + "formatted_body": + '
In reply to' + + ' myfakeuser' + + "
Replying to this
hello world", + "msgtype": "m.text", + "m.relates_to": { + "m.in_reply_to": { + event_id: mockEvent.getId(), + }, + }, + }); + }); + + it("Should add relation to message", async () => { + // When + const relation = { + rel_type: "m.thread", + event_id: "myFakeThreadId", + }; + const content = await createMessageContent(message, true, { permalinkCreator, relation }); + + // Then + expect(content).toEqual({ + "body": "*__hello__ world*", + "format": "org.matrix.custom.html", + "formatted_body": message, + "msgtype": "m.text", + "m.relates_to": { + event_id: "myFakeThreadId", + rel_type: "m.thread", + }, + }); + }); + + it("Should add fields related to edition", async () => { + // When + const editedEvent = mkEvent({ + type: "m.room.message", + room: "myfakeroom", + user: "myfakeuser2", + content: { + "msgtype": "m.text", + "body": "First message", + "formatted_body": "First Message", + "m.relates_to": { + "m.in_reply_to": { + event_id: "eventId", + }, + }, + }, + event: true, + }); + const content = await createMessageContent(message, true, { permalinkCreator, editedEvent }); + + // Then + expect(content).toEqual({ + "body": " * *__hello__ world*", + "format": "org.matrix.custom.html", + "formatted_body": ` * ${message}`, + "msgtype": "m.text", + "m.new_content": { + body: "*__hello__ world*", + format: "org.matrix.custom.html", + formatted_body: message, + msgtype: "m.text", + }, + "m.relates_to": { + event_id: editedEvent.getId(), + rel_type: "m.replace", + }, + }); + }); + + it("Should strip the /me prefix from a message", async () => { + const textBody = "some body text"; + const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator }); + + expect(content).toMatchObject({ body: textBody, formatted_body: textBody }); + }); + + it("Should strip single / from message prefixed with //", async () => { + const content = await createMessageContent("//twoSlashes", true, { permalinkCreator }); + + expect(content).toMatchObject({ body: "/twoSlashes", formatted_body: "/twoSlashes" }); + }); + + it("Should set the content type to MsgType.Emote when /me prefix is used", async () => { + const textBody = "some body text"; + const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator }); + + expect(content).toMatchObject({ msgtype: MsgType.Emote }); }); }); - it("Should strip the /me prefix from a message", async () => { - const textBody = "some body text"; - const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator }); + describe("Plaintext composer input", () => { + it("Should replace at-room mentions with `@room` in body", async () => { + const messageComposerState = `@room `; - expect(content).toMatchObject({ body: textBody, formatted_body: textBody }); - }); + const content = await createMessageContent(messageComposerState, false, { permalinkCreator }); + expect(content).toMatchObject({ body: "@room " }); + }); - it("Should strip single / from message prefixed with //", async () => { - const content = await createMessageContent("//twoSlashes", true, { permalinkCreator }); + it("Should replace user mentions with user name in body", async () => { + const messageComposerState = `a test user `; - expect(content).toMatchObject({ body: "/twoSlashes", formatted_body: "/twoSlashes" }); - }); + const content = await createMessageContent(messageComposerState, false, { permalinkCreator }); - it("Should set the content type to MsgType.Emote when /me prefix is used", async () => { - const textBody = "some body text"; - const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator }); + expect(content).toMatchObject({ body: "a test user " }); + }); - expect(content).toMatchObject({ msgtype: MsgType.Emote }); + it("Should replace room mentions with room mxid in body", async () => { + const messageComposerState = `a test room `; + + const content = await createMessageContent(messageComposerState, false, { permalinkCreator }); + + expect(content).toMatchObject({ + body: "#test_room:element.io ", + }); + }); }); });