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

Commit d64018c

Browse files
authored
Improve message body output from plain text editor (#11124)
* add failing test * WIP - pause work until we can implement with new patch release of RTE * focus tests purely on the body output * remove unused import
1 parent ad8543e commit d64018c

File tree

2 files changed

+173
-95
lines changed

2 files changed

+173
-95
lines changed

src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg";
1818
import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
1919

2020
import SettingsStore from "../../../../../settings/SettingsStore";
21-
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
21+
import { parsePermalink, RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
2222
import { addReplyToMessageContent } from "../../../../../utils/Reply";
23+
import { isNotNull } from "../../../../../Typeguards";
2324

2425
export const EMOTE_PREFIX = "/me ";
2526

@@ -94,8 +95,8 @@ export async function createMessageContent(
9495
}
9596

9697
// if we're editing rich text, the message content is pure html
97-
// BUT if we're not, the message content will be plain text
98-
const body = isHTML ? await richToPlain(message) : message;
98+
// BUT if we're not, the message content will be plain text where we need to convert the mentions
99+
const body = isHTML ? await richToPlain(message) : convertPlainTextToBody(message);
99100
const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || "";
100101
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
101102

@@ -141,3 +142,51 @@ export async function createMessageContent(
141142

142143
return content;
143144
}
145+
146+
/**
147+
* Without a model, we need to manually amend mentions in uncontrolled message content
148+
* to make sure that mentions meet the matrix specification.
149+
*
150+
* @param content - the output from the `MessageComposer` state when in plain text mode
151+
* @returns - a string formatted with the mentions replaced as required
152+
*/
153+
function convertPlainTextToBody(content: string): string {
154+
const document = new DOMParser().parseFromString(content, "text/html");
155+
const mentions = Array.from(document.querySelectorAll("a[data-mention-type]"));
156+
157+
mentions.forEach((mention) => {
158+
const mentionType = mention.getAttribute("data-mention-type");
159+
switch (mentionType) {
160+
case "at-room": {
161+
mention.replaceWith("@room");
162+
break;
163+
}
164+
case "user": {
165+
const innerText = mention.innerHTML;
166+
mention.replaceWith(innerText);
167+
break;
168+
}
169+
case "room": {
170+
// for this case we use parsePermalink to try and get the mx id
171+
const href = mention.getAttribute("href");
172+
173+
// if the mention has no href attribute, leave it alone
174+
if (href === null) break;
175+
176+
// otherwise, attempt to parse the room alias or id from the href
177+
const permalinkParts = parsePermalink(href);
178+
179+
// then if we have permalink parts with a valid roomIdOrAlias, replace the
180+
// room mention with that text
181+
if (isNotNull(permalinkParts) && isNotNull(permalinkParts.roomIdOrAlias)) {
182+
mention.replaceWith(permalinkParts.roomIdOrAlias);
183+
}
184+
break;
185+
}
186+
default:
187+
break;
188+
}
189+
});
190+
191+
return document.body.innerHTML;
192+
}

test/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts

Lines changed: 121 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -41,117 +41,146 @@ describe("createMessageContent", () => {
4141
jest.resetAllMocks();
4242
});
4343

44-
it("Should create html message", async () => {
45-
// When
46-
const content = await createMessageContent(message, true, { permalinkCreator });
47-
48-
// Then
49-
expect(content).toEqual({
50-
body: "*__hello__ world*",
51-
format: "org.matrix.custom.html",
52-
formatted_body: message,
53-
msgtype: "m.text",
44+
describe("Richtext composer input", () => {
45+
it("Should create html message", async () => {
46+
// When
47+
const content = await createMessageContent(message, true, { permalinkCreator });
48+
49+
// Then
50+
expect(content).toEqual({
51+
body: "*__hello__ world*",
52+
format: "org.matrix.custom.html",
53+
formatted_body: message,
54+
msgtype: "m.text",
55+
});
5456
});
55-
});
5657

57-
it("Should add reply to message content", async () => {
58-
// When
59-
const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
60-
61-
// Then
62-
expect(content).toEqual({
63-
"body": "> <myfakeuser> Replying to this\n\n*__hello__ world*",
64-
"format": "org.matrix.custom.html",
65-
"formatted_body":
66-
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
67-
' <a href="https://matrix.to/#/myfakeuser">myfakeuser</a>' +
68-
"<br>Replying to this</blockquote></mx-reply><em><b>hello</b> world</em>",
69-
"msgtype": "m.text",
70-
"m.relates_to": {
71-
"m.in_reply_to": {
72-
event_id: mockEvent.getId(),
58+
it("Should add reply to message content", async () => {
59+
// When
60+
const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
61+
62+
// Then
63+
expect(content).toEqual({
64+
"body": "> <myfakeuser> Replying to this\n\n*__hello__ world*",
65+
"format": "org.matrix.custom.html",
66+
"formatted_body":
67+
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
68+
' <a href="https://matrix.to/#/myfakeuser">myfakeuser</a>' +
69+
"<br>Replying to this</blockquote></mx-reply><em><b>hello</b> world</em>",
70+
"msgtype": "m.text",
71+
"m.relates_to": {
72+
"m.in_reply_to": {
73+
event_id: mockEvent.getId(),
74+
},
7375
},
74-
},
76+
});
7577
});
76-
});
7778

78-
it("Should add relation to message", async () => {
79-
// When
80-
const relation = {
81-
rel_type: "m.thread",
82-
event_id: "myFakeThreadId",
83-
};
84-
const content = await createMessageContent(message, true, { permalinkCreator, relation });
85-
86-
// Then
87-
expect(content).toEqual({
88-
"body": "*__hello__ world*",
89-
"format": "org.matrix.custom.html",
90-
"formatted_body": message,
91-
"msgtype": "m.text",
92-
"m.relates_to": {
93-
event_id: "myFakeThreadId",
79+
it("Should add relation to message", async () => {
80+
// When
81+
const relation = {
9482
rel_type: "m.thread",
95-
},
83+
event_id: "myFakeThreadId",
84+
};
85+
const content = await createMessageContent(message, true, { permalinkCreator, relation });
86+
87+
// Then
88+
expect(content).toEqual({
89+
"body": "*__hello__ world*",
90+
"format": "org.matrix.custom.html",
91+
"formatted_body": message,
92+
"msgtype": "m.text",
93+
"m.relates_to": {
94+
event_id: "myFakeThreadId",
95+
rel_type: "m.thread",
96+
},
97+
});
9698
});
97-
});
9899

99-
it("Should add fields related to edition", async () => {
100-
// When
101-
const editedEvent = mkEvent({
102-
type: "m.room.message",
103-
room: "myfakeroom",
104-
user: "myfakeuser2",
105-
content: {
100+
it("Should add fields related to edition", async () => {
101+
// When
102+
const editedEvent = mkEvent({
103+
type: "m.room.message",
104+
room: "myfakeroom",
105+
user: "myfakeuser2",
106+
content: {
107+
"msgtype": "m.text",
108+
"body": "First message",
109+
"formatted_body": "<b>First Message</b>",
110+
"m.relates_to": {
111+
"m.in_reply_to": {
112+
event_id: "eventId",
113+
},
114+
},
115+
},
116+
event: true,
117+
});
118+
const content = await createMessageContent(message, true, { permalinkCreator, editedEvent });
119+
120+
// Then
121+
expect(content).toEqual({
122+
"body": " * *__hello__ world*",
123+
"format": "org.matrix.custom.html",
124+
"formatted_body": ` * ${message}`,
106125
"msgtype": "m.text",
107-
"body": "First message",
108-
"formatted_body": "<b>First Message</b>",
126+
"m.new_content": {
127+
body: "*__hello__ world*",
128+
format: "org.matrix.custom.html",
129+
formatted_body: message,
130+
msgtype: "m.text",
131+
},
109132
"m.relates_to": {
110-
"m.in_reply_to": {
111-
event_id: "eventId",
112-
},
133+
event_id: editedEvent.getId(),
134+
rel_type: "m.replace",
113135
},
114-
},
115-
event: true,
136+
});
116137
});
117-
const content = await createMessageContent(message, true, { permalinkCreator, editedEvent });
118-
119-
// Then
120-
expect(content).toEqual({
121-
"body": " * *__hello__ world*",
122-
"format": "org.matrix.custom.html",
123-
"formatted_body": ` * ${message}`,
124-
"msgtype": "m.text",
125-
"m.new_content": {
126-
body: "*__hello__ world*",
127-
format: "org.matrix.custom.html",
128-
formatted_body: message,
129-
msgtype: "m.text",
130-
},
131-
"m.relates_to": {
132-
event_id: editedEvent.getId(),
133-
rel_type: "m.replace",
134-
},
138+
139+
it("Should strip the /me prefix from a message", async () => {
140+
const textBody = "some body text";
141+
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
142+
143+
expect(content).toMatchObject({ body: textBody, formatted_body: textBody });
135144
});
136-
});
137145

138-
it("Should strip the /me prefix from a message", async () => {
139-
const textBody = "some body text";
140-
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
146+
it("Should strip single / from message prefixed with //", async () => {
147+
const content = await createMessageContent("//twoSlashes", true, { permalinkCreator });
141148

142-
expect(content).toMatchObject({ body: textBody, formatted_body: textBody });
143-
});
149+
expect(content).toMatchObject({ body: "/twoSlashes", formatted_body: "/twoSlashes" });
150+
});
144151

145-
it("Should strip single / from message prefixed with //", async () => {
146-
const content = await createMessageContent("//twoSlashes", true, { permalinkCreator });
152+
it("Should set the content type to MsgType.Emote when /me prefix is used", async () => {
153+
const textBody = "some body text";
154+
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
147155

148-
expect(content).toMatchObject({ body: "/twoSlashes", formatted_body: "/twoSlashes" });
156+
expect(content).toMatchObject({ msgtype: MsgType.Emote });
157+
});
149158
});
150159

151-
it("Should set the content type to MsgType.Emote when /me prefix is used", async () => {
152-
const textBody = "some body text";
153-
const content = await createMessageContent(EMOTE_PREFIX + textBody, true, { permalinkCreator });
160+
describe("Plaintext composer input", () => {
161+
it("Should replace at-room mentions with `@room` in body", async () => {
162+
const messageComposerState = `<a href="#" contenteditable="false" data-mention-type="at-room" style="some styling">@room</a> `;
163+
164+
const content = await createMessageContent(messageComposerState, false, { permalinkCreator });
165+
expect(content).toMatchObject({ body: "@room " });
166+
});
167+
168+
it("Should replace user mentions with user name in body", async () => {
169+
const messageComposerState = `<a href="https://matrix.to/#/@test_user:element.io" contenteditable="false" data-mention-type="user" style="some styling">a test user</a> `;
170+
171+
const content = await createMessageContent(messageComposerState, false, { permalinkCreator });
154172

155-
expect(content).toMatchObject({ msgtype: MsgType.Emote });
173+
expect(content).toMatchObject({ body: "a test user " });
174+
});
175+
176+
it("Should replace room mentions with room mxid in body", async () => {
177+
const messageComposerState = `<a href="https://matrix.to/#/#test_room:element.io" contenteditable="false" data-mention-type="room" style="some styling">a test room</a> `;
178+
179+
const content = await createMessageContent(messageComposerState, false, { permalinkCreator });
180+
181+
expect(content).toMatchObject({
182+
body: "#test_room:element.io ",
183+
});
184+
});
156185
});
157186
});

0 commit comments

Comments
 (0)