Composer: Don't insert images from clipboard if text is present (#3455)
This commit is contained in:
parent
f774cf5e43
commit
eb7840c251
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user