diff --git a/ts/components/conversation/composition/CompositionTextArea.tsx b/ts/components/conversation/composition/CompositionTextArea.tsx index eb3b35af69..e599735f1e 100644 --- a/ts/components/conversation/composition/CompositionTextArea.tsx +++ b/ts/components/conversation/composition/CompositionTextArea.tsx @@ -66,6 +66,25 @@ type Props = { type SearchableSuggestion = SessionSuggestionDataItem & { searchable?: Array }; +function isEditableTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false; + } + + const editableSelector = 'input, textarea, select, [contenteditable="true"], [contenteditable="plaintext-only"]'; + return !!target.closest(editableSelector); +} + +function isPrintableTypingEvent(event: globalThis.KeyboardEvent) { + return ( + event.key.length === 1 && + !event.altKey && + !event.ctrlKey && + !event.metaKey && + !event.defaultPrevented + ); +} + function useMembersInThisChat(): Array { const selectedConvoKey = useSelectedConversationKey(); const isPrivate = useSelectedIsPrivate(); @@ -585,6 +604,33 @@ export function CompositionTextArea(props: Props) { }, }); + useEffect(() => { + if (!selectedConversationKey || !typingEnabled) { + return undefined; + } + + const handleTypingFocus = (event: globalThis.KeyboardEvent) => { + if (!isPrintableTypingEvent(event) || isEditableTarget(event.target)) { + return; + } + + const input = inputRef.current; + if (!input) { + return; + } + + event.preventDefault(); + input.focus(); + input.typeAtCaret(event.key); + }; + + document.addEventListener('keydown', handleTypingFocus, true); + + return () => { + document.removeEventListener('keydown', handleTypingFocus, true); + }; + }, [inputRef, selectedConversationKey, typingEnabled]); + const onFocus = () => { dispatch(setIsCompositionTextAreaFocused(true)); };