From eb7840c25143a0a5385d7308c6baf0103b45d666 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 5 Jul 2023 13:16:23 +0200 Subject: [PATCH] Composer: Don't insert images from clipboard if text is present (#3455) --- src/components/middle/composer/Composer.tsx | 100 ++++++++++-------- .../composer/hooks/useAttachmentModal.ts | 3 + .../composer/hooks/useClipboardPaste.ts | 37 +++++-- 3 files changed, 88 insertions(+), 52 deletions(-) diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index fea6e4915..c8c32d35d 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -370,6 +370,7 @@ const Composer: FC = ({ const [attachments, setAttachments] = useState([]); const hasAttachments = Boolean(attachments.length); + const [nextText, setNextText] = useState(undefined); const { canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks, @@ -378,6 +379,57 @@ const Composer: FC = ({ const isComposerBlocked = !canSendPlainText && !editingMessage; + const insertHtmlAndUpdateCursor = useLastCallback((newHtml: string, inputId: string = EDITABLE_INPUT_ID) => { + if (inputId === EDITABLE_INPUT_ID && isComposerBlocked) return; + const selection = window.getSelection()!; + let messageInput: HTMLDivElement; + if (inputId === EDITABLE_INPUT_ID) { + messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR)!; + } else { + messageInput = document.getElementById(inputId) as HTMLDivElement; + } + + if (selection.rangeCount) { + const selectionRange = selection.getRangeAt(0); + if (isSelectionInsideInput(selectionRange, inputId)) { + insertHtmlInSelection(newHtml); + messageInput.dispatchEvent(new Event('input', { bubbles: true })); + return; + } + } + + setHtml(`${getHtml()}${newHtml}`); + + // If selection is outside of input, set cursor at the end of input + requestNextMutation(() => { + focusEditableElement(messageInput); + }); + }); + + const insertTextAndUpdateCursor = useLastCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => { + const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html']) + .join('') + .replace(/\u200b+/g, '\u200b'); + insertHtmlAndUpdateCursor(newHtml, inputId); + }); + + const insertFormattedTextAndUpdateCursor = useLastCallback(( + text: ApiFormattedText, inputId: string = EDITABLE_INPUT_ID, + ) => { + const newHtml = getTextWithEntitiesAsHtml(text); + insertHtmlAndUpdateCursor(newHtml, inputId); + }); + + const insertCustomEmojiAndUpdateCursor = useLastCallback((emoji: ApiSticker, inputId: string = EDITABLE_INPUT_ID) => { + insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inputId); + }); + + const insertNextText = useLastCallback(() => { + if (!nextText) return; + insertFormattedTextAndUpdateCursor(nextText, EDITABLE_INPUT_ID); + setNextText(undefined); + }); + const { shouldSuggestCompression, shouldForceCompression, @@ -397,6 +449,7 @@ const Composer: FC = ({ canSendVideos, canSendPhotos, canSendDocuments, + insertNextText, }); const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag(); @@ -522,44 +575,6 @@ const Composer: FC = ({ chatBotCommands, ); - const insertHtmlAndUpdateCursor = useLastCallback((newHtml: string, inputId: string = EDITABLE_INPUT_ID) => { - if (inputId === EDITABLE_INPUT_ID && isComposerBlocked) return; - const selection = window.getSelection()!; - let messageInput: HTMLDivElement; - if (inputId === EDITABLE_INPUT_ID) { - messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR)!; - } else { - messageInput = document.getElementById(inputId) as HTMLDivElement; - } - - if (selection.rangeCount) { - const selectionRange = selection.getRangeAt(0); - if (isSelectionInsideInput(selectionRange, inputId)) { - insertHtmlInSelection(newHtml); - messageInput.dispatchEvent(new Event('input', { bubbles: true })); - return; - } - } - - setHtml(`${getHtml()}${newHtml}`); - - // If selection is outside of input, set cursor at the end of input - requestNextMutation(() => { - focusEditableElement(messageInput); - }); - }); - - const insertFormattedTextAndUpdateCursor = useLastCallback(( - text: ApiFormattedText, inputId: string = EDITABLE_INPUT_ID, - ) => { - const newHtml = getTextWithEntitiesAsHtml(text); - insertHtmlAndUpdateCursor(newHtml, inputId); - }); - - const insertCustomEmojiAndUpdateCursor = useLastCallback((emoji: ApiSticker, inputId: string = EDITABLE_INPUT_ID) => { - insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inputId); - }); - useDraft(draft, chatId, threadId, getHtml, setHtml, editingMessage); const resetComposer = useLastCallback((shouldPreserveInput = false) => { @@ -568,6 +583,7 @@ const Composer: FC = ({ } setAttachments(MEMO_EMPTY_ARRAY); + setNextText(undefined); closeEmojiTooltip(); closeCustomEmojiTooltip(); @@ -662,6 +678,7 @@ const Composer: FC = ({ isForCurrentMessageList, insertFormattedTextAndUpdateCursor, handleSetAttachments, + setNextText, editingMessage, !isCurrentUserPremium && !isChatWithSelf, showCustomEmojiPremiumNotification, @@ -1090,13 +1107,6 @@ const Composer: FC = ({ }, MOBILE_KEYBOARD_HIDE_DELAY_MS); }); - const insertTextAndUpdateCursor = useLastCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => { - const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html']) - .join('') - .replace(/\u200b+/g, '\u200b'); - insertHtmlAndUpdateCursor(newHtml, inputId); - }); - useEffect(() => { if (!isComposerBlocked) return; diff --git a/src/components/middle/composer/hooks/useAttachmentModal.ts b/src/components/middle/composer/hooks/useAttachmentModal.ts index 3b94ba067..cefb0ed2f 100644 --- a/src/components/middle/composer/hooks/useAttachmentModal.ts +++ b/src/components/middle/composer/hooks/useAttachmentModal.ts @@ -23,6 +23,7 @@ export default function useAttachmentModal({ canSendVideos, canSendPhotos, canSendDocuments, + insertNextText, }: { attachments: ApiAttachment[]; fileSizeLimit: number; @@ -33,6 +34,7 @@ export default function useAttachmentModal({ canSendVideos?: boolean; canSendPhotos?: boolean; canSendDocuments?: boolean; + insertNextText: VoidFunction; }) { const { openLimitReachedModal, showAllowedMessageTypesNotification } = getActions(); const [shouldForceAsFile, setShouldForceAsFile] = useState(false); @@ -41,6 +43,7 @@ export default function useAttachmentModal({ const handleClearAttachments = useLastCallback(() => { setAttachments(MEMO_EMPTY_ARRAY); + insertNextText(); }); const handleSetAttachments = useLastCallback( diff --git a/src/components/middle/composer/hooks/useClipboardPaste.ts b/src/components/middle/composer/hooks/useClipboardPaste.ts index 378a88c40..7c2506dc0 100644 --- a/src/components/middle/composer/hooks/useClipboardPaste.ts +++ b/src/components/middle/composer/hooks/useClipboardPaste.ts @@ -1,6 +1,6 @@ -import type { StateHookSetter } from '../../../../lib/teact/teact'; import { useEffect } from '../../../../lib/teact/teact'; +import type { StateHookSetter } from '../../../../lib/teact/teact'; import type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../api/types'; import { ApiMessageEntityTypes } from '../../../../api/types'; @@ -14,6 +14,9 @@ import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helper const MAX_MESSAGE_LENGTH = 4096; const STYLE_TAG_REGEX = /