From 28a41a676431bf133668cdb0effb0b0b5f4eee71 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 6 Nov 2025 11:36:33 +0100 Subject: [PATCH] Attachment Modal: Send attachments on enter when input not focused (#6397) --- .../middle/composer/MessageInput.tsx | 76 ++++++++++--------- src/util/captureKeyboardListeners.ts | 5 ++ 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/components/middle/composer/MessageInput.tsx b/src/components/middle/composer/MessageInput.tsx index f542d5875..1eb4261e5 100644 --- a/src/components/middle/composer/MessageInput.tsx +++ b/src/components/middle/composer/MessageInput.tsx @@ -14,7 +14,7 @@ import type { } from '../../../types'; import type { Signal } from '../../../util/signals'; -import { EDITABLE_INPUT_ID } from '../../../config'; +import { EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../config'; import { requestForcedReflow, requestMutation } from '../../../lib/fasterdom/fasterdom'; import { selectCanPlayAnimatedEmojis, selectDraft, selectIsInSelectMode } from '../../../global/selectors'; import { selectSharedSettings } from '../../../global/selectors/sharedState'; @@ -23,7 +23,7 @@ import { IS_ANDROID, IS_EMOJI_SUPPORTED, IS_IOS, IS_TOUCH_ENV, } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; -import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; +import captureKeyboardListeners, { hasActiveHandler } from '../../../util/captureKeyboardListeners'; import { getIsDirectTextInputDisabled } from '../../../util/directInputManager'; import parseEmojiOnlyString from '../../../util/emoji/parseEmojiOnlyString'; import focusEditableElement from '../../../util/focusEditableElement'; @@ -379,6 +379,22 @@ const MessageInput: FC = ({ document.addEventListener('keydown', handleCloseContextMenu); } + const isSendShortcut = useLastCallback((e: KeyboardEvent | React.KeyboardEvent) => { + return e.key === 'Enter' + && !e.shiftKey + && !isMobileDevice + && ( + (messageSendKeyCombo === 'enter' && !e.shiftKey) + || (messageSendKeyCombo === 'ctrl-enter' && (e.ctrlKey || e.metaKey)) + ); + }); + + const handleSendShortcut = useLastCallback((e: KeyboardEvent | React.KeyboardEvent) => { + e.preventDefault(); + closeTextFormatter(); + onSend(); + }); + function handleKeyDown(e: React.KeyboardEvent) { // https://levelup.gitconnected.com/javascript-events-handlers-keyboard-and-load-events-1b3e46a6b0c3#1960 const { isComposing } = e; @@ -394,19 +410,8 @@ const MessageInput: FC = ({ } } - if (!isComposing && e.key === 'Enter' && !e.shiftKey) { - if ( - !isMobileDevice - && ( - (messageSendKeyCombo === 'enter' && !e.shiftKey) - || (messageSendKeyCombo === 'ctrl-enter' && (e.ctrlKey || e.metaKey)) - ) - ) { - e.preventDefault(); - - closeTextFormatter(); - onSend(); - } + if (!isComposing && isSendShortcut(e)) { + handleSendShortcut(e); } else if (!isComposing && e.key === 'ArrowUp' && !html && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); editLastMessage(); @@ -474,8 +479,7 @@ const MessageInput: FC = ({ useEffect(() => { if ( !chatId - || editableInputId !== EDITABLE_INPUT_ID - || noFocusInterception + || (editableInputId !== EDITABLE_INPUT_ID && editableInputId !== EDITABLE_INPUT_MODAL_ID) || isMobileDevice || isSelectModeActive ) { @@ -483,18 +487,29 @@ const MessageInput: FC = ({ } const handleDocumentKeyDown = (e: KeyboardEvent) => { - if (getIsDirectTextInputDisabled()) { + const target = e.target as HTMLElement | undefined; + const input = inputRef.current!; + + const shouldHandleDocumentKeyDown = + isActive && input && target + && target !== input + && target.tagName !== 'INPUT' + && target.tagName !== 'TEXTAREA' + && !target.isContentEditable + && !hasActiveHandler('Enter'); + + if (!shouldHandleDocumentKeyDown) return; + + if (isSendShortcut(e)) { + handleSendShortcut(e); return; } const { key } = e; - const target = e.target as HTMLElement | undefined; - - if (!target || IGNORE_KEYS.includes(key)) { + if (noFocusInterception || getIsDirectTextInputDisabled() || IGNORE_KEYS.includes(key)) { return; } - const input = inputRef.current!; const isSelectionCollapsed = document.getSelection()?.isCollapsed; if ( @@ -504,18 +519,10 @@ const MessageInput: FC = ({ return; } - if ( - input - && target !== input - && target.tagName !== 'INPUT' - && target.tagName !== 'TEXTAREA' - && !target.isContentEditable - ) { - focusEditableElement(input, true, true); + focusEditableElement(input, true, true); - const newEvent = new KeyboardEvent(e.type, e as any); - input.dispatchEvent(newEvent); - } + const newEvent = new KeyboardEvent(e.type, e as any); + input.dispatchEvent(newEvent); }; document.addEventListener('keydown', handleDocumentKeyDown, true); @@ -523,7 +530,8 @@ const MessageInput: FC = ({ return () => { document.removeEventListener('keydown', handleDocumentKeyDown, true); }; - }, [chatId, editableInputId, isMobileDevice, isSelectModeActive, noFocusInterception]); + }, [chatId, editableInputId, isMobileDevice, + isActive, isSelectModeActive, noFocusInterception]); useEffect(() => { const captureFirstTab = debounce((e: KeyboardEvent) => { diff --git a/src/util/captureKeyboardListeners.ts b/src/util/captureKeyboardListeners.ts index 6e4ce2c7d..989d272b2 100644 --- a/src/util/captureKeyboardListeners.ts +++ b/src/util/captureKeyboardListeners.ts @@ -62,6 +62,11 @@ function hasActiveHandlers() { return Object.values(handlers).some((keyHandlers) => Boolean(keyHandlers.length)); } +export function hasActiveHandler(key: string) { + const handlerName = keyToHandlerName[key]; + return handlerName ? Boolean(handlers[handlerName].length) : false; +} + function handleKeyDown(e: KeyboardEvent) { const handlerName = keyToHandlerName[e.key]; if (!handlerName) {