mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	Remove fallback replies & implement intentional mentions (#2138)
* 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>
This commit is contained in:
		
							parent
							
								
									dd4c1a94e6
								
							
						
					
					
						commit
						8d95758ed7
					
				
					 5 changed files with 83 additions and 28 deletions
				
			
		| 
						 | 
				
			
			@ -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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue