Skip to content

Commit c5eb302

Browse files
committed
fix stripping nickname from replies and plain mentions
1 parent 2865df0 commit c5eb302

2 files changed

Lines changed: 130 additions & 5 deletions

File tree

src/app/components/editor/output.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export type OutputOptions = {
1717
allowTextFormatting?: boolean;
1818
allowInlineMarkdown?: boolean;
1919
allowBlockMarkdown?: boolean;
20+
/**
21+
* if true it will remove the nickname of the person from the message
22+
*/
23+
stripNickname?: boolean;
24+
/**
25+
* a map of regex patterns to replace nicknames with, used when stripNickname is true
26+
*/
27+
nickNameReplacement?: Map<RegExp, string>;
2028
};
2129

2230
const textToCustomHtml = (node: Text, opts: OutputOptions): string => {
@@ -99,21 +107,34 @@ const ignoreHTMLParseInlineMD = (text: string): string =>
99107
(txt) => parseInlineMD(txt)
100108
).join('');
101109

110+
/**
111+
* convert slate internal representation to a custom HTML string that can be sent to the server
112+
* @param node slate node
113+
* @param opts options for output
114+
* @returns custom HTML string
115+
*/
102116
export const toMatrixCustomHTML = (
103117
node: Descendant | Descendant[],
104118
opts: OutputOptions
105119
): string => {
106120
let markdownLines = '';
107121
const parseNode = (n: Descendant, index: number, targetNodes: Descendant[]) => {
108122
if (opts.allowBlockMarkdown && 'type' in n && n.type === BlockType.Paragraph) {
109-
const line = toMatrixCustomHTML(n, {
123+
let line = toMatrixCustomHTML(n, {
110124
...opts,
111125
allowInlineMarkdown: false,
112126
allowBlockMarkdown: false,
113127
})
114128
.replace(/<br\/>$/, '\n')
115129
.replace(/^(\\*)&gt;/, '$1>');
116130

131+
// strip nicknames if needed
132+
if (opts.stripNickname && opts.nickNameReplacement) {
133+
opts.nickNameReplacement?.keys().forEach((key) => {
134+
const replacement = opts.nickNameReplacement!.get(key) ?? '';
135+
line = line.replaceAll(key, replacement);
136+
});
137+
}
117138
markdownLines += line;
118139
if (index === targetNodes.length - 1) {
119140
return parseBlockMD(markdownLines, ignoreHTMLParseInlineMD);
@@ -175,12 +196,37 @@ const elementToPlainText = (node: CustomElement, children: string): string => {
175196
}
176197
};
177198

178-
export const toPlainText = (node: Descendant | Descendant[], isMarkdown: boolean): string => {
179-
if (Array.isArray(node)) return node.map((n) => toPlainText(n, isMarkdown)).join('');
180-
if (Text.isText(node))
199+
/**
200+
* convert slate internal representation to a plain text string that can be sent to the server
201+
* @param node the slate node
202+
* @param isMarkdown set true if it's a markdown formatted text
203+
* @param stripNickname whether to strip nicknames
204+
* @param nickNameReplacement the nickname replacement
205+
* @returns the plain text we want to send
206+
*/
207+
export const toPlainText = (
208+
node: Descendant | Descendant[],
209+
isMarkdown: boolean,
210+
stripNickname = false,
211+
nickNameReplacement?: Map<RegExp, string>
212+
): string => {
213+
if (Array.isArray(node))
214+
return node.map((n) => toPlainText(n, isMarkdown, stripNickname, nickNameReplacement)).join('');
215+
if (Text.isText(node)) {
216+
if (stripNickname && nickNameReplacement) {
217+
let { text } = node;
218+
nickNameReplacement?.keys().forEach((key) => {
219+
const replacement = nickNameReplacement.get(key) ?? '';
220+
text = text.replaceAll(key, replacement);
221+
});
222+
return isMarkdown
223+
? unescapeMarkdownBlockSequences(text, unescapeMarkdownInlineSequences)
224+
: text;
225+
}
181226
return isMarkdown
182227
? unescapeMarkdownBlockSequences(node.text, unescapeMarkdownInlineSequences)
183228
: node.text;
229+
}
184230

185231
const children = node.children.map((n) => toPlainText(n, isMarkdown)).join('');
186232
return elementToPlainText(node, children);
@@ -208,10 +254,27 @@ export const trimCommand = (cmdName: string, str: string) => {
208254
return str.slice(match[0].length);
209255
};
210256

257+
/**
258+
* Type representing Mentions
259+
*/
211260
export type MentionsData = {
261+
/**
262+
* a boolean to denote if it's a room mention
263+
*/
212264
room: boolean;
265+
/**
266+
* a set of user ids that are mentioned in the message
267+
*/
213268
users: Set<string>;
214269
};
270+
271+
/**
272+
* get the mentions in a message
273+
* @param mx the matrix client
274+
* @param roomId the room id we will send the message in
275+
* @param editor the slate editor
276+
* @returns the mentions in a message {@link MentionsData}
277+
*/
215278
export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): MentionsData => {
216279
const mentionData: MentionsData = {
217280
room: false,

src/app/features/room/RoomInput.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
254254
const emojiBtnRef = useRef<HTMLButtonElement>(null);
255255
const micBtnRef = useRef<HTMLButtonElement>(null);
256256
const roomToParents = useAtomValue(roomToParentsAtom);
257+
/**
258+
* Nickname someone set for another user
259+
* this nickname should be treated as private
260+
*/
257261
const nicknames = useAtomValue(nicknamesAtom);
258262

259263
const powerLevels = usePowerLevelsContext();
@@ -372,6 +376,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
372376
let replyBodyJSX: ReactNode = replyDraft ? trimReplyFromBody(replyDraft.body) : null;
373377

374378
if (htmlBody) {
379+
/**
380+
* message with linebreaks, etc stripped
381+
*/
375382
const strippedHtml = trimReplyFromFormattedBody(htmlBody)
376383
.replaceAll(/<br\s*\/?>/gi, ' ')
377384
.replaceAll(/<\/p>\s*<p[^>]*>/gi, ' ')
@@ -574,12 +581,65 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
574581
uploadBoardHandlers.current?.handleSend();
575582

576583
const commandName = getBeginCommand(editor);
577-
let plainText = toPlainText(editor.children, isMarkdown).trim();
584+
/**
585+
* a map of regex patterns to replace nicknames with,
586+
* used when stripNickname is true in toMatrixCustomHTML
587+
* during HTML generation for the message content.
588+
* This is necessary because the HTML generation needs to know
589+
* which nicknames to strip in order to generate the correct formatted_body,
590+
* and the plain text generation needs to replace those same nicknames with
591+
* the original user IDs so that the message content remains consistent and
592+
* mentions are correctly processed by the server and clients.
593+
*/
594+
const nicknameReplacement = new Map<RegExp, string>();
595+
if (replyEvent) {
596+
/**
597+
* the id of the user being replied to,
598+
* whose nickname (if any) should be stripped
599+
* from the message content and replaced with their
600+
* user ID for correct mention processing
601+
*/
602+
const senderId = replyEvent.getSender();
603+
if (senderId) {
604+
const nick = nicknames[senderId];
605+
if (typeof nick === 'string' && nick.length > 0) {
606+
nicknameReplacement.set(
607+
new RegExp(`@?${nick}`, 'g'),
608+
room.getMember(senderId)?.rawDisplayName ?? senderId
609+
);
610+
}
611+
}
612+
}
613+
/**
614+
* any other users mentioned in the message being replied to,
615+
* whose nicknames should also be stripped and replaced with user IDs
616+
*/
617+
const mentions = getMentions(mx, roomId, editor);
618+
if (mentions?.users) {
619+
mentions.users.forEach((id) => {
620+
const nick = nicknames[id];
621+
if (typeof nick === 'string' && nick.length > 0) {
622+
nicknameReplacement.set(
623+
new RegExp(`@?${nick}`, 'g'),
624+
room.getMember(id)?.rawDisplayName ?? id
625+
);
626+
}
627+
});
628+
}
629+
/**
630+
* the plain text we will send
631+
*/
632+
let plainText = toPlainText(editor.children, isMarkdown, true, nicknameReplacement).trim();
633+
/**
634+
* the html we will send
635+
*/
578636
let customHtml = trimCustomHtml(
579637
toMatrixCustomHTML(editor.children, {
580638
allowTextFormatting: true,
581639
allowBlockMarkdown: isMarkdown,
582640
allowInlineMarkdown: isMarkdown,
641+
stripNickname: true,
642+
nickNameReplacement: nicknameReplacement,
583643
})
584644
);
585645
let msgType = MsgType.Text;
@@ -720,6 +780,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
720780
}
721781
}, [
722782
editor,
783+
replyEvent,
723784
isMarkdown,
724785
canSendReaction,
725786
mx,
@@ -729,6 +790,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
729790
silentReply,
730791
scheduledTime,
731792
editingScheduledDelayId,
793+
nicknames,
732794
handleQuickReact,
733795
commands,
734796
sendTypingStatus,

0 commit comments

Comments
 (0)