-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
Copy pathcreateMessageContent.ts
148 lines (122 loc) · 5.43 KB
/
createMessageContent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { richToPlain, plainToRich } from "@vector-im/matrix-wysiwyg";
import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
import { ReplacementEvent, RoomMessageEventContent, RoomMessageTextEventContent } from "matrix-js-sdk/src/types";
import SettingsStore from "../../../../../settings/SettingsStore";
import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
import { addReplyToMessageContent } from "../../../../../utils/Reply";
import { isNotNull } from "../../../../../Typeguards";
export const EMOTE_PREFIX = "/me ";
// Merges favouring the given relation
function attachRelation(content: IContent, relation?: IEventRelation): void {
if (relation) {
content["m.relates_to"] = {
...(content["m.relates_to"] || {}),
...relation,
};
}
}
interface CreateMessageContentParams {
relation?: IEventRelation;
replyToEvent?: MatrixEvent;
editedEvent?: MatrixEvent;
}
const isMatrixEvent = (e: MatrixEvent | undefined): e is MatrixEvent => e instanceof MatrixEvent;
export async function createMessageContent(
message: string,
isHTML: boolean,
{ relation, replyToEvent, editedEvent }: CreateMessageContentParams,
): Promise<RoomMessageEventContent> {
const isEditing = isMatrixEvent(editedEvent);
const isEmote = message.startsWith(EMOTE_PREFIX);
if (isEmote) {
// if we are dealing with an emote we want to remove the prefix so that `/me` does not
// appear after the `* <userName>` text in the timeline
message = message.slice(EMOTE_PREFIX.length);
}
if (message.startsWith("//")) {
// if user wants to enter a single slash at the start of a message, this
// is how they have to do it (due to it clashing with commands), so here we
// remove the first character to make sure //word displays as /word
message = message.slice(1);
}
// if we're editing rich text, the message content is pure html
// 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, false) : convertPlainTextToBody(message);
const content = {
msgtype: isEmote ? MsgType.Emote : MsgType.Text,
body: isEditing ? `* ${body}` : body,
} as RoomMessageTextEventContent & ReplacementEvent<RoomMessageTextEventContent>;
// TODO markdown support
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message, true) : null;
if (formattedBody) {
content.format = "org.matrix.custom.html";
content.formatted_body = isEditing ? `* ${formattedBody}` : formattedBody;
}
if (isEditing) {
content["m.new_content"] = {
msgtype: content.msgtype,
body: body,
};
if (formattedBody) {
content["m.new_content"].format = "org.matrix.custom.html";
content["m.new_content"]["formatted_body"] = formattedBody;
}
}
const newRelation = isEditing ? { ...relation, rel_type: "m.replace", event_id: editedEvent.getId() } : relation;
// TODO Do we need to attach mentions here?
// TODO Handle editing?
attachRelation(content, newRelation);
if (!isEditing && replyToEvent) {
addReplyToMessageContent(content, replyToEvent);
}
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;
}