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

View File

@ -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<boolean>(false);
@ -41,6 +43,7 @@ export default function useAttachmentModal({
const handleClearAttachments = useLastCallback(() => {
setAttachments(MEMO_EMPTY_ARRAY);
insertNextText();
});
const handleSetAttachments = useLastCallback(

View File

@ -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 = /<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) {
let fragment = document.createElement('div');
@ -67,6 +70,7 @@ const useClipboardPaste = (
isActive: boolean,
insertTextAndUpdateCursor: (text: ApiFormattedText, inputId?: string) => void,
setAttachments: StateHookSetter<ApiAttachment[]>,
setNextText: StateHookSetter<ApiFormattedText | undefined>,
editedMessage: ApiMessage | undefined,
shouldStripCustomEmoji?: boolean,
onCustomEmojiStripped?: VoidFunction,
@ -88,6 +92,7 @@ const useClipboardPaste = (
const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH);
const html = e.clipboardData.getData('text/html');
let pastedFormattedText = html ? parseMessageInput(
preparePastedHtml(html), undefined, true,
) : undefined;
@ -109,17 +114,34 @@ const useClipboardPaste = (
return;
}
if (files?.length && !editedMessage) {
const newAttachments = await Promise.all(files.map((file) => {
const textToPaste = pastedFormattedText?.entities?.length ? pastedFormattedText : { text: pastedText };
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);
}));
setAttachments((attachments) => attachments.concat(newAttachments));
}
const textToPaste = pastedFormattedText?.entities?.length ? pastedFormattedText : { text: pastedText };
if (textToPaste) {
insertTextAndUpdateCursor(textToPaste, input?.id);
if (hasText) {
if (shouldSetAttachments) {
setNextText(textToPaste);
} else {
insertTextAndUpdateCursor(textToPaste, input?.id);
}
}
}
@ -130,6 +152,7 @@ const useClipboardPaste = (
};
}, [
insertTextAndUpdateCursor, editedMessage, setAttachments, isActive, shouldStripCustomEmoji, onCustomEmojiStripped,
setNextText,
]);
};