diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index 256bdbd9e..dbdd51f37 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -1,5 +1,5 @@ -import { Descendant, Text } from 'slate'; - +import { Descendant, Editor, Text } from 'slate'; +import { MatrixClient } from 'matrix-js-sdk'; import { sanitizeText } from '../../utils/sanitize'; import { BlockType } from './types'; import { CustomElement } from './slate'; @@ -11,6 +11,7 @@ import { } from '../../plugins/markdown'; import { findAndReplace } from '../../utils/findAndReplace'; import { sanitizeForRegex } from '../../utils/regex'; +import { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix'; export type OutputOptions = { allowTextFormatting?: boolean; @@ -195,3 +196,36 @@ export const trimCommand = (cmdName: string, str: string) => { if (!match) return str; return str.slice(match[0].length); }; + +export type MentionsData = { + room: boolean; + users: Set; +}; +export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): MentionsData => { + const mentionData: MentionsData = { + room: false, + users: new Set(), + }; + + const parseMentions = (node: Descendant): void => { + if (Text.isText(node)) return; + if (node.type === BlockType.CodeBlock) return; + + if (node.type === BlockType.Mention) { + if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) { + mentionData.room = true; + } + if (isUserId(node.id) && node.id !== mx.getUserId()) { + mentionData.users.add(node.id); + } + + return; + } + + node.children.forEach(parseMentions); + }; + + editor.children.forEach(parseMentions); + + return mentionData; +}; diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index df7310e5d..4d21f4401 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -53,6 +53,7 @@ import { isEmptyEditor, getBeginCommand, trimCommand, + getMentions, } from '../../components/editor'; import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board'; import { UseStateProvider } from '../../components/UseStateProvider'; @@ -102,12 +103,9 @@ import colorMXID from '../../../util/colorMXID'; import { getAllParents, getMemberDisplayName, - parseReplyBody, - parseReplyFormattedBody, + getMentionContent, trimReplyFromBody, - trimReplyFromFormattedBody, } from '../../utils/room'; -import { sanitizeText } from '../../utils/sanitize'; import { CommandAutocomplete } from './CommandAutocomplete'; import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '../../hooks/useCommands'; import { mobileOrTablet } from '../../utils/user-agent'; @@ -268,7 +266,6 @@ export const RoomInput = forwardRef( uploadBoardHandlers.current?.handleSend(); const commandName = getBeginCommand(editor); - let plainText = toPlainText(editor.children, isMarkdown).trim(); let customHtml = trimCustomHtml( toMatrixCustomHTML(editor.children, { @@ -309,25 +306,22 @@ export const RoomInput = forwardRef( if (plainText === '') return; - let body = plainText; - let formattedBody = customHtml; - if (replyDraft) { - body = parseReplyBody(replyDraft.userId, trimReplyFromBody(replyDraft.body)) + body; - formattedBody = - parseReplyFormattedBody( - roomId, - replyDraft.userId, - replyDraft.eventId, - replyDraft.formattedBody - ? trimReplyFromFormattedBody(replyDraft.formattedBody) - : sanitizeText(replyDraft.body) - ) + formattedBody; - } + const body = plainText; + const formattedBody = customHtml; + const mentionData = getMentions(mx, roomId, editor); const content: IContent = { msgtype: msgType, body, }; + + if (replyDraft && replyDraft.userId !== mx.getUserId()) { + mentionData.users.add(replyDraft.userId); + } + + const mMentions = getMentionContent(Array.from(mentionData.users), mentionData.room); + content['m.mentions'] = mMentions; + if (replyDraft || !customHtmlEqualsPlainText(formattedBody, body)) { content.format = 'org.matrix.custom.html'; content.formatted_body = formattedBody; diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index bdf520594..bde03eb2c 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -35,7 +35,7 @@ import { useHover, useFocusWithin } from 'react-aria'; import { MatrixEvent, Room } from 'matrix-js-sdk'; import { Relations } from 'matrix-js-sdk/lib/models/relations'; import classNames from 'classnames'; -import { EventType, RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; +import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; import { AvatarBase, BubbleLayout, diff --git a/src/app/features/room/message/MessageEditor.tsx b/src/app/features/room/message/MessageEditor.tsx index deeb82154..dc59dcdf6 100644 --- a/src/app/features/room/message/MessageEditor.tsx +++ b/src/app/features/room/message/MessageEditor.tsx @@ -21,7 +21,7 @@ import { } from 'folds'; import { Editor, Transforms } from 'slate'; import { ReactEditor } from 'slate-react'; -import { IContent, MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; +import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; import { isKeyHotkey } from 'is-hotkey'; import { AUTOCOMPLETE_PREFIXES, @@ -43,6 +43,7 @@ import { toPlainText, trimCustomHtml, useEditor, + getMentions, } from '../../../components/editor'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; @@ -50,7 +51,7 @@ import { UseStateProvider } from '../../../components/UseStateProvider'; import { EmojiBoard } from '../../../components/emoji-board'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; -import { getEditedEvent, trimReplyFromFormattedBody } from '../../../utils/room'; +import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room'; import { mobileOrTablet } from '../../../utils/user-agent'; type MessageEditorProps = { @@ -74,19 +75,23 @@ export const MessageEditor = as<'div', MessageEditorProps>( const getPrevBodyAndFormattedBody = useCallback((): [ string | undefined, - string | undefined + string | undefined, + IMentions | undefined ] => { const evtId = mEvent.getId()!; const evtTimeline = room.getTimelineForEvent(evtId); const editedEvent = evtTimeline && getEditedEvent(evtId, mEvent, evtTimeline.getTimelineSet()); - const { body, formatted_body: customHtml }: Record = - editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent(); + const content: IContent = editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent(); + const { body, formatted_body: customHtml }: Record = content; + + const mMentions: IMentions | undefined = content['m.mentions']; return [ typeof body === 'string' ? body : undefined, typeof customHtml === 'string' ? customHtml : undefined, + mMentions, ]; }, [room, mEvent]); @@ -101,7 +106,7 @@ export const MessageEditor = as<'div', MessageEditorProps>( }) ); - const [prevBody, prevCustomHtml] = getPrevBodyAndFormattedBody(); + const [prevBody, prevCustomHtml, prevMentions] = getPrevBodyAndFormattedBody(); if (plainText === '') return undefined; if (prevBody) { @@ -122,6 +127,15 @@ export const MessageEditor = as<'div', MessageEditorProps>( body: plainText, }; + const mentionData = getMentions(mx, roomId, editor); + + prevMentions?.user_ids?.forEach((prevMentionId) => { + mentionData.users.add(prevMentionId); + }); + + const mMentions = getMentionContent(Array.from(mentionData.users), mentionData.room); + newContent['m.mentions'] = mMentions; + if (!customHtmlEqualsPlainText(customHtml, plainText)) { newContent.format = 'org.matrix.custom.html'; newContent.formatted_body = customHtml; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 36de44939..3bf8cd5a4 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -4,6 +4,7 @@ import { EventTimeline, EventTimelineSet, EventType, + IMentions, IPushRule, IPushRules, JoinRule, @@ -430,3 +431,15 @@ export const getLatestEditableEvt = ( export const reactionOrEditEvent = (mEvent: MatrixEvent) => mEvent.getRelation()?.rel_type === RelationType.Annotation || mEvent.getRelation()?.rel_type === RelationType.Replace; + +export const getMentionContent = (userIds: string[], room: boolean): IMentions => { + const mMentions: IMentions = {}; + if (userIds.length > 0) { + mMentions.user_ids = userIds; + } + if (room) { + mMentions.room = true; + } + + return mMentions; +};