import React, { KeyboardEventHandler, MouseEventHandler, useCallback, useEffect, useState, } from 'react'; import { Box, Chip, Icon, IconButton, Icons, Line, PopOut, RectCords, Spinner, Text, as, config, } from 'folds'; import { Editor, Transforms } from 'slate'; import { ReactEditor } from 'slate-react'; import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk'; import { isKeyHotkey } from 'is-hotkey'; import { AUTOCOMPLETE_PREFIXES, AutocompletePrefix, AutocompleteQuery, CustomEditor, EmoticonAutocomplete, RoomMentionAutocomplete, Toolbar, UserMentionAutocomplete, createEmoticonElement, customHtmlEqualsPlainText, getAutocompleteQuery, getPrevWorldRange, htmlToEditorInput, moveCursor, plainToEditorInput, toMatrixCustomHTML, toPlainText, trimCustomHtml, useEditor, getMentions, } from '../../../components/editor'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { UseStateProvider } from '../../../components/UseStateProvider'; import { EmojiBoard } from '../../../components/emoji-board'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room'; import { mobileOrTablet } from '../../../utils/user-agent'; import { useComposingCheck } from '../../../hooks/useComposingCheck'; type MessageEditorProps = { roomId: string; room: Room; mEvent: MatrixEvent; imagePackRooms?: Room[]; onCancel: () => void; }; export const MessageEditor = as<'div', MessageEditorProps>( ({ room, roomId, mEvent, imagePackRooms, onCancel, ...props }, ref) => { const mx = useMatrixClient(); const editor = useEditor(); const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [globalToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [toolbar, setToolbar] = useState(globalToolbar); const isComposing = useComposingCheck(); const [autocompleteQuery, setAutocompleteQuery] = useState>(); const getPrevBodyAndFormattedBody = useCallback((): [ string | undefined, string | undefined, IMentions | undefined ] => { const evtId = mEvent.getId()!; const evtTimeline = room.getTimelineForEvent(evtId); const editedEvent = evtTimeline && getEditedEvent(evtId, mEvent, evtTimeline.getTimelineSet()); 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]); const [saveState, save] = useAsyncCallback( useCallback(async () => { const plainText = toPlainText(editor.children, isMarkdown).trim(); const customHtml = trimCustomHtml( toMatrixCustomHTML(editor.children, { allowTextFormatting: true, allowBlockMarkdown: isMarkdown, allowInlineMarkdown: isMarkdown, }) ); const [prevBody, prevCustomHtml, prevMentions] = getPrevBodyAndFormattedBody(); if (plainText === '') return undefined; if (prevBody) { if (prevCustomHtml && trimReplyFromFormattedBody(prevCustomHtml) === customHtml) { return undefined; } if ( !prevCustomHtml && prevBody === plainText && customHtmlEqualsPlainText(customHtml, plainText) ) { return undefined; } } const newContent: IContent = { msgtype: mEvent.getContent().msgtype, 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; } const content: IContent = { ...newContent, body: `* ${plainText}`, 'm.new_content': newContent, 'm.relates_to': { event_id: mEvent.getId(), rel_type: RelationType.Replace, }, }; return mx.sendMessage(roomId, content); }, [mx, editor, roomId, mEvent, isMarkdown, getPrevBodyAndFormattedBody]) ); const handleSave = useCallback(() => { if (saveState.status !== AsyncStatus.Loading) { save(); } }, [saveState, save]); const handleKeyDown: KeyboardEventHandler = useCallback( (evt) => { if ( (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) && !isComposing(evt) ) { evt.preventDefault(); handleSave(); } if (isKeyHotkey('escape', evt)) { evt.preventDefault(); onCancel(); } }, [onCancel, handleSave, enterForNewline, isComposing] ); const handleKeyUp: KeyboardEventHandler = useCallback( (evt) => { if (isKeyHotkey('escape', evt)) { evt.preventDefault(); return; } const prevWordRange = getPrevWorldRange(editor); const query = prevWordRange ? getAutocompleteQuery(editor, prevWordRange, AUTOCOMPLETE_PREFIXES) : undefined; setAutocompleteQuery(query); }, [editor] ); const handleCloseAutocomplete = useCallback(() => { ReactEditor.focus(editor); setAutocompleteQuery(undefined); }, [editor]); const handleEmoticonSelect = (key: string, shortcode: string) => { editor.insertNode(createEmoticonElement(key, shortcode)); moveCursor(editor); }; useEffect(() => { const [body, customHtml] = getPrevBodyAndFormattedBody(); const initialValue = typeof customHtml === 'string' ? htmlToEditorInput(customHtml, isMarkdown) : plainToEditorInput(typeof body === 'string' ? body : '', isMarkdown); Transforms.select(editor, { anchor: Editor.start(editor, []), focus: Editor.end(editor, []), }); editor.insertFragment(initialValue); if (!mobileOrTablet()) ReactEditor.focus(editor); }, [editor, getPrevBodyAndFormattedBody, isMarkdown]); useEffect(() => { if (saveState.status === AsyncStatus.Success) { onCancel(); } }, [saveState, onCancel]); return (
{autocompleteQuery?.prefix === AutocompletePrefix.RoomMention && ( )} {autocompleteQuery?.prefix === AutocompletePrefix.UserMention && ( )} {autocompleteQuery?.prefix === AutocompletePrefix.Emoticon && ( )} ) : undefined } > Save Cancel setToolbar(!toolbar)} > {(anchor: RectCords | undefined, setAnchor) => ( { setAnchor((v) => { if (v) { if (!mobileOrTablet()) ReactEditor.focus(editor); return undefined; } return v; }); }} /> } > setAnchor( evt.currentTarget.getBoundingClientRect() )) as MouseEventHandler } variant="SurfaceVariant" size="300" radii="300" > )} {toolbar && (
)} } />
); } );