mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-26 15:13:05 +01: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 { sanitizeText } from '../../utils/sanitize';
|
||||||
import { BlockType } from './types';
|
import { BlockType } from './types';
|
||||||
import { CustomElement } from './slate';
|
import { CustomElement } from './slate';
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
} from '../../plugins/markdown';
|
} from '../../plugins/markdown';
|
||||||
import { findAndReplace } from '../../utils/findAndReplace';
|
import { findAndReplace } from '../../utils/findAndReplace';
|
||||||
import { sanitizeForRegex } from '../../utils/regex';
|
import { sanitizeForRegex } from '../../utils/regex';
|
||||||
|
import { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix';
|
||||||
|
|
||||||
export type OutputOptions = {
|
export type OutputOptions = {
|
||||||
allowTextFormatting?: boolean;
|
allowTextFormatting?: boolean;
|
||||||
|
@ -195,3 +196,36 @@ export const trimCommand = (cmdName: string, str: string) => {
|
||||||
if (!match) return str;
|
if (!match) return str;
|
||||||
return str.slice(match[0].length);
|
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,
|
isEmptyEditor,
|
||||||
getBeginCommand,
|
getBeginCommand,
|
||||||
trimCommand,
|
trimCommand,
|
||||||
|
getMentions,
|
||||||
} from '../../components/editor';
|
} from '../../components/editor';
|
||||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
|
@ -102,12 +103,9 @@ import colorMXID from '../../../util/colorMXID';
|
||||||
import {
|
import {
|
||||||
getAllParents,
|
getAllParents,
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
parseReplyBody,
|
getMentionContent,
|
||||||
parseReplyFormattedBody,
|
|
||||||
trimReplyFromBody,
|
trimReplyFromBody,
|
||||||
trimReplyFromFormattedBody,
|
|
||||||
} from '../../utils/room';
|
} from '../../utils/room';
|
||||||
import { sanitizeText } from '../../utils/sanitize';
|
|
||||||
import { CommandAutocomplete } from './CommandAutocomplete';
|
import { CommandAutocomplete } from './CommandAutocomplete';
|
||||||
import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '../../hooks/useCommands';
|
import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '../../hooks/useCommands';
|
||||||
import { mobileOrTablet } from '../../utils/user-agent';
|
import { mobileOrTablet } from '../../utils/user-agent';
|
||||||
|
@ -268,7 +266,6 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
uploadBoardHandlers.current?.handleSend();
|
uploadBoardHandlers.current?.handleSend();
|
||||||
|
|
||||||
const commandName = getBeginCommand(editor);
|
const commandName = getBeginCommand(editor);
|
||||||
|
|
||||||
let plainText = toPlainText(editor.children, isMarkdown).trim();
|
let plainText = toPlainText(editor.children, isMarkdown).trim();
|
||||||
let customHtml = trimCustomHtml(
|
let customHtml = trimCustomHtml(
|
||||||
toMatrixCustomHTML(editor.children, {
|
toMatrixCustomHTML(editor.children, {
|
||||||
|
@ -309,25 +306,22 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
|
|
||||||
if (plainText === '') return;
|
if (plainText === '') return;
|
||||||
|
|
||||||
let body = plainText;
|
const body = plainText;
|
||||||
let formattedBody = customHtml;
|
const formattedBody = customHtml;
|
||||||
if (replyDraft) {
|
const mentionData = getMentions(mx, roomId, editor);
|
||||||
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 content: IContent = {
|
const content: IContent = {
|
||||||
msgtype: msgType,
|
msgtype: msgType,
|
||||||
body,
|
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)) {
|
if (replyDraft || !customHtmlEqualsPlainText(formattedBody, body)) {
|
||||||
content.format = 'org.matrix.custom.html';
|
content.format = 'org.matrix.custom.html';
|
||||||
content.formatted_body = formattedBody;
|
content.formatted_body = formattedBody;
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { useHover, useFocusWithin } from 'react-aria';
|
||||||
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
import { Relations } from 'matrix-js-sdk/lib/models/relations';
|
import { Relations } from 'matrix-js-sdk/lib/models/relations';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { EventType, RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import {
|
import {
|
||||||
AvatarBase,
|
AvatarBase,
|
||||||
BubbleLayout,
|
BubbleLayout,
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { Editor, Transforms } from 'slate';
|
import { Editor, Transforms } from 'slate';
|
||||||
import { ReactEditor } from 'slate-react';
|
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 { isKeyHotkey } from 'is-hotkey';
|
||||||
import {
|
import {
|
||||||
AUTOCOMPLETE_PREFIXES,
|
AUTOCOMPLETE_PREFIXES,
|
||||||
|
@ -43,6 +43,7 @@ import {
|
||||||
toPlainText,
|
toPlainText,
|
||||||
trimCustomHtml,
|
trimCustomHtml,
|
||||||
useEditor,
|
useEditor,
|
||||||
|
getMentions,
|
||||||
} from '../../../components/editor';
|
} from '../../../components/editor';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
import { useSetting } from '../../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../../state/settings';
|
import { settingsAtom } from '../../../state/settings';
|
||||||
|
@ -50,7 +51,7 @@ import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||||
import { EmojiBoard } from '../../../components/emoji-board';
|
import { EmojiBoard } from '../../../components/emoji-board';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { getEditedEvent, trimReplyFromFormattedBody } from '../../../utils/room';
|
import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room';
|
||||||
import { mobileOrTablet } from '../../../utils/user-agent';
|
import { mobileOrTablet } from '../../../utils/user-agent';
|
||||||
|
|
||||||
type MessageEditorProps = {
|
type MessageEditorProps = {
|
||||||
|
@ -74,19 +75,23 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
|
|
||||||
const getPrevBodyAndFormattedBody = useCallback((): [
|
const getPrevBodyAndFormattedBody = useCallback((): [
|
||||||
string | undefined,
|
string | undefined,
|
||||||
string | undefined
|
string | undefined,
|
||||||
|
IMentions | undefined
|
||||||
] => {
|
] => {
|
||||||
const evtId = mEvent.getId()!;
|
const evtId = mEvent.getId()!;
|
||||||
const evtTimeline = room.getTimelineForEvent(evtId);
|
const evtTimeline = room.getTimelineForEvent(evtId);
|
||||||
const editedEvent =
|
const editedEvent =
|
||||||
evtTimeline && getEditedEvent(evtId, mEvent, evtTimeline.getTimelineSet());
|
evtTimeline && getEditedEvent(evtId, mEvent, evtTimeline.getTimelineSet());
|
||||||
|
|
||||||
const { body, formatted_body: customHtml }: Record<string, unknown> =
|
const content: IContent = editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent();
|
||||||
editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent();
|
const { body, formatted_body: customHtml }: Record<string, unknown> = content;
|
||||||
|
|
||||||
|
const mMentions: IMentions | undefined = content['m.mentions'];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
typeof body === 'string' ? body : undefined,
|
typeof body === 'string' ? body : undefined,
|
||||||
typeof customHtml === 'string' ? customHtml : undefined,
|
typeof customHtml === 'string' ? customHtml : undefined,
|
||||||
|
mMentions,
|
||||||
];
|
];
|
||||||
}, [room, mEvent]);
|
}, [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 (plainText === '') return undefined;
|
||||||
if (prevBody) {
|
if (prevBody) {
|
||||||
|
@ -122,6 +127,15 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
body: plainText,
|
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)) {
|
if (!customHtmlEqualsPlainText(customHtml, plainText)) {
|
||||||
newContent.format = 'org.matrix.custom.html';
|
newContent.format = 'org.matrix.custom.html';
|
||||||
newContent.formatted_body = customHtml;
|
newContent.formatted_body = customHtml;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
EventType,
|
EventType,
|
||||||
|
IMentions,
|
||||||
IPushRule,
|
IPushRule,
|
||||||
IPushRules,
|
IPushRules,
|
||||||
JoinRule,
|
JoinRule,
|
||||||
|
@ -430,3 +431,15 @@ export const getLatestEditableEvt = (
|
||||||
export const reactionOrEditEvent = (mEvent: MatrixEvent) =>
|
export const reactionOrEditEvent = (mEvent: MatrixEvent) =>
|
||||||
mEvent.getRelation()?.rel_type === RelationType.Annotation ||
|
mEvent.getRelation()?.rel_type === RelationType.Annotation ||
|
||||||
mEvent.getRelation()?.rel_type === RelationType.Replace;
|
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…
Reference in a new issue