Composer: Don't insert images from clipboard if text is present (#3455)

This commit is contained in:
Alexander Zinchuk 2023-07-05 13:16:23 +02:00
parent f774cf5e43
commit eb7840c251
3 changed files with 88 additions and 52 deletions

View File

@ -370,6 +370,7 @@ const Composer: FC<OwnProps & StateProps> = ({
const [attachments, setAttachments] = useState<ApiAttachment[]>([]); const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
const hasAttachments = Boolean(attachments.length); const hasAttachments = Boolean(attachments.length);
const [nextText, setNextText] = useState<ApiFormattedText | undefined>(undefined);
const { const {
canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks, canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks,
@ -378,6 +379,57 @@ const Composer: FC<OwnProps & StateProps> = ({
const isComposerBlocked = !canSendPlainText && !editingMessage; 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<HTMLDivElement>(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 { const {
shouldSuggestCompression, shouldSuggestCompression,
shouldForceCompression, shouldForceCompression,
@ -397,6 +449,7 @@ const Composer: FC<OwnProps & StateProps> = ({
canSendVideos, canSendVideos,
canSendPhotos, canSendPhotos,
canSendDocuments, canSendDocuments,
insertNextText,
}); });
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag(); const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
@ -522,44 +575,6 @@ const Composer: FC<OwnProps & StateProps> = ({
chatBotCommands, 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<HTMLDivElement>(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); useDraft(draft, chatId, threadId, getHtml, setHtml, editingMessage);
const resetComposer = useLastCallback((shouldPreserveInput = false) => { const resetComposer = useLastCallback((shouldPreserveInput = false) => {
@ -568,6 +583,7 @@ const Composer: FC<OwnProps & StateProps> = ({
} }
setAttachments(MEMO_EMPTY_ARRAY); setAttachments(MEMO_EMPTY_ARRAY);
setNextText(undefined);
closeEmojiTooltip(); closeEmojiTooltip();
closeCustomEmojiTooltip(); closeCustomEmojiTooltip();
@ -662,6 +678,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isForCurrentMessageList, isForCurrentMessageList,
insertFormattedTextAndUpdateCursor, insertFormattedTextAndUpdateCursor,
handleSetAttachments, handleSetAttachments,
setNextText,
editingMessage, editingMessage,
!isCurrentUserPremium && !isChatWithSelf, !isCurrentUserPremium && !isChatWithSelf,
showCustomEmojiPremiumNotification, showCustomEmojiPremiumNotification,
@ -1090,13 +1107,6 @@ const Composer: FC<OwnProps & StateProps> = ({
}, MOBILE_KEYBOARD_HIDE_DELAY_MS); }, 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(() => { useEffect(() => {
if (!isComposerBlocked) return; if (!isComposerBlocked) return;

View File

@ -23,6 +23,7 @@ export default function useAttachmentModal({
canSendVideos, canSendVideos,
canSendPhotos, canSendPhotos,
canSendDocuments, canSendDocuments,
insertNextText,
}: { }: {
attachments: ApiAttachment[]; attachments: ApiAttachment[];
fileSizeLimit: number; fileSizeLimit: number;
@ -33,6 +34,7 @@ export default function useAttachmentModal({
canSendVideos?: boolean; canSendVideos?: boolean;
canSendPhotos?: boolean; canSendPhotos?: boolean;
canSendDocuments?: boolean; canSendDocuments?: boolean;
insertNextText: VoidFunction;
}) { }) {
const { openLimitReachedModal, showAllowedMessageTypesNotification } = getActions(); const { openLimitReachedModal, showAllowedMessageTypesNotification } = getActions();
const [shouldForceAsFile, setShouldForceAsFile] = useState<boolean>(false); const [shouldForceAsFile, setShouldForceAsFile] = useState<boolean>(false);
@ -41,6 +43,7 @@ export default function useAttachmentModal({
const handleClearAttachments = useLastCallback(() => { const handleClearAttachments = useLastCallback(() => {
setAttachments(MEMO_EMPTY_ARRAY); setAttachments(MEMO_EMPTY_ARRAY);
insertNextText();
}); });
const handleSetAttachments = useLastCallback( const handleSetAttachments = useLastCallback(

View File

@ -1,6 +1,6 @@
import type { StateHookSetter } from '../../../../lib/teact/teact';
import { useEffect } 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 type { ApiAttachment, ApiFormattedText, ApiMessage } from '../../../../api/types';
import { ApiMessageEntityTypes } from '../../../../api/types'; import { ApiMessageEntityTypes } from '../../../../api/types';
@ -14,6 +14,9 @@ import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helper
const MAX_MESSAGE_LENGTH = 4096; const MAX_MESSAGE_LENGTH = 4096;
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs; const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;
const TYPE_HTML = 'text/html';
const DOCUMENT_TYPE_WORD = 'urn:schemas-microsoft-com:office:word';
const NAMESPACE_PREFIX_WORD = 'xmlns:w';
function preparePastedHtml(html: string) { function preparePastedHtml(html: string) {
let fragment = document.createElement('div'); let fragment = document.createElement('div');
@ -67,6 +70,7 @@ const useClipboardPaste = (
isActive: boolean, isActive: boolean,
insertTextAndUpdateCursor: (text: ApiFormattedText, inputId?: string) => void, insertTextAndUpdateCursor: (text: ApiFormattedText, inputId?: string) => void,
setAttachments: StateHookSetter<ApiAttachment[]>, setAttachments: StateHookSetter<ApiAttachment[]>,
setNextText: StateHookSetter<ApiFormattedText | undefined>,
editedMessage: ApiMessage | undefined, editedMessage: ApiMessage | undefined,
shouldStripCustomEmoji?: boolean, shouldStripCustomEmoji?: boolean,
onCustomEmojiStripped?: VoidFunction, onCustomEmojiStripped?: VoidFunction,
@ -88,6 +92,7 @@ const useClipboardPaste = (
const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH); const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH);
const html = e.clipboardData.getData('text/html'); const html = e.clipboardData.getData('text/html');
let pastedFormattedText = html ? parseMessageInput( let pastedFormattedText = html ? parseMessageInput(
preparePastedHtml(html), undefined, true, preparePastedHtml(html), undefined, true,
) : undefined; ) : undefined;
@ -109,17 +114,34 @@ const useClipboardPaste = (
return; return;
} }
if (files?.length && !editedMessage) { const textToPaste = pastedFormattedText?.entities?.length ? pastedFormattedText : { text: pastedText };
const newAttachments = await Promise.all(files.map((file) => {
let isWordDocument = false;
try {
const parser = new DOMParser();
const parsedDocument = parser.parseFromString(html, TYPE_HTML);
isWordDocument = parsedDocument.documentElement
.getAttribute(NAMESPACE_PREFIX_WORD) === DOCUMENT_TYPE_WORD;
} catch (err: any) {
// Ignore
}
const hasText = textToPaste && textToPaste.text;
const shouldSetAttachments = files?.length && !editedMessage && !isWordDocument;
if (shouldSetAttachments) {
const newAttachments = await Promise.all(files!.map((file) => {
return buildAttachment(file.name, file); return buildAttachment(file.name, file);
})); }));
setAttachments((attachments) => attachments.concat(newAttachments)); setAttachments((attachments) => attachments.concat(newAttachments));
} }
const textToPaste = pastedFormattedText?.entities?.length ? pastedFormattedText : { text: pastedText }; if (hasText) {
if (shouldSetAttachments) {
if (textToPaste) { setNextText(textToPaste);
insertTextAndUpdateCursor(textToPaste, input?.id); } else {
insertTextAndUpdateCursor(textToPaste, input?.id);
}
} }
} }
@ -130,6 +152,7 @@ const useClipboardPaste = (
}; };
}, [ }, [
insertTextAndUpdateCursor, editedMessage, setAttachments, isActive, shouldStripCustomEmoji, onCustomEmojiStripped, insertTextAndUpdateCursor, editedMessage, setAttachments, isActive, shouldStripCustomEmoji, onCustomEmojiStripped,
setNextText,
]); ]);
}; };