Remove fallback replies & implement intentional mentions (#2138)
authornexy7574 <git@nexy7574.co.uk>
Sun, 23 Feb 2025 11:08:08 +0000 (11:08 +0000)
committerGitHub <noreply@github.com>
Sun, 23 Feb 2025 11:08:08 +0000 (22:08 +1100)
* Remove reply fallbacks & add m.mentions

(WIP) the typing on line 301 and 303 needs fixing but apart from that this is mint

* Less jank typing

* Mention the reply author in m.mentions

* Improve typing

* Fix typing in m.mentions finder

* Correctly iterate through editor children, properly handle @room, ...

..., don't mention the reply author when the reply author is ourself, don't add own user IDs when mentioning intentionally

* Formatting

* Add intentional mentions to edited messages

* refactor reusable code and fix todo

* parse mentions from all nodes

---------

Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
src/app/components/editor/output.ts
src/app/features/room/RoomInput.tsx
src/app/features/room/message/Message.tsx
src/app/features/room/message/MessageEditor.tsx
src/app/utils/room.ts

index 256bdbd9e22272bb53c2c9734cfac092d22c5e05..dbdd51f3790e9db25b50a3b162981f5b16495a9b 100644 (file)
@@ -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<string>;
+};
+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;
+};
index df7310e5d76c21f8c9ecafc40a06ab19fe668112..4d21f440138a56822310b02bdcc3a83615c2e4c9 100644 (file)
@@ -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<HTMLDivElement, RoomInputProps>(
       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<HTMLDivElement, RoomInputProps>(
 
       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;
index bdf52059406f75b2bcc98c93267205c193108d36..bde03eb2cfbd985b5373d407b413f65350a3beb5 100644 (file)
@@ -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,
index deeb821546b691e4f436acab560accd998f2ddae..dc59dcdf6dbd5e5160d11466194a79a90eaead5c 100644 (file)
@@ -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<string, unknown> =
-        editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent();
+      const content: IContent = editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent();
+      const { body, formatted_body: customHtml }: Record<string, unknown> = 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;
index 36de449397eafed2b5ef81f873a5f8b2e16c345b..3bf8cd5a4bd61596ab5ce921cf59ad7d6f02920f 100644 (file)
@@ -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;
+};