From be2dff6425a94777819e64bf26364981c151651f Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Tue, 11 Nov 2025 15:06:32 -0300 Subject: [PATCH 1/9] Revert to old updateComment timing behavior --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 3c062deaadbd3..9e7518ce8b561 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -750,6 +750,7 @@ function ComposerWithSuggestions({ // Note: use the value when the clear happened, not the current value which might have changed already onCleared(text); updateComment('', true); + updateComment('', true); }, [onCleared, updateComment], ); @@ -792,6 +793,8 @@ function ComposerWithSuggestions({ [measureParentContainer, cursorPositionValue, selection], ); + const debouncedUpdateComment = useCallback((newComment: string) => updateComment(newComment, true), [updateComment]); + const isTouchEndedRef = useRef(false); const containerComposeStyles = StyleSheet.flatten(StyleUtils.getContainerComposeStyles()); @@ -827,7 +830,7 @@ function ComposerWithSuggestions({ ref={setTextInputRef} placeholder={inputPlaceholder} placeholderTextColor={theme.placeholderText} - onChangeText={onChangeText} + onChangeText={debouncedUpdateComment} onKeyPress={handleKeyPress} textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} From e43aa5fa660388b6208fe57f9f58fb5a55ede7a0 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Wed, 12 Nov 2025 16:42:04 -0300 Subject: [PATCH 2/9] Fix cursor not moving to the end of selected suggestion --- .../ComposerWithSuggestions.tsx | 52 ++++++------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 9e7518ce8b561..b114533267e37 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -27,7 +27,6 @@ import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSp import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getPreferredSkinToneIndex, replaceAndExtractEmojis} from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; -import getPlatform from '@libs/getPlatform'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import {detectAndRewritePaste} from '@libs/MarkdownLinkHelpers'; import Parser from '@libs/Parser'; @@ -53,11 +52,6 @@ import type ChildrenProps from '@src/types/utils/ChildrenProps'; // eslint-disable-next-line no-restricted-imports import findNodeHandle from '@src/utils/findNodeHandle'; -type SyncSelection = { - position: number; - value: string; -}; - type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsProps = Partial & { @@ -160,8 +154,6 @@ type ComposerRef = { const {RNTextInputReset} = NativeModules; -const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; - /** * Broadcast that the user is typing. Debounced to limit how often we publish client events. */ @@ -271,8 +263,6 @@ function ComposerWithSuggestions({ const textInputRef = useRef(null); - const syncSelectionWithOnChangeTextRef = useRef(null); - // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -415,10 +405,6 @@ function ComposerWithSuggestions({ if (commentValue !== newComment) { const position = Math.max((selection.end ?? 0) + (newComment.length - commentRef.current.length), cursorPosition ?? 0); - if (commentWithSpaceInserted !== newComment && isIOSNative) { - syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; - } - setSelection((prevSelection) => ({ start: position, end: position, @@ -524,26 +510,6 @@ function ComposerWithSuggestions({ [shouldUseNarrowLayout, isKeyboardShown, suggestionsRef, selection.start, includeChronos, handleSendMessage, lastReportAction, reportID, updateComment, selection.end], ); - const onChangeText = useCallback( - (commentValue: string) => { - updateComment(commentValue, true); - - if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { - const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; - syncSelectionWithOnChangeTextRef.current = null; - - // ensure that selection is set imperatively after all state changes are effective - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - // note: this implementation is only available on non-web RN, thus the wrapping - // 'if' block contains a redundant (since the ref is only used on iOS) platform check - textInputRef.current?.setSelection(positionSnapshot, positionSnapshot); - }); - } - }, - [updateComment], - ); - const onSelectionChange = useCallback( (e: CustomSelectionChangeEvent) => { setSelection(e.nativeEvent.selection); @@ -795,6 +761,22 @@ function ComposerWithSuggestions({ const debouncedUpdateComment = useCallback((newComment: string) => updateComment(newComment, true), [updateComment]); + // When using the suggestions box we need to imperatively set the cursor + // to the end of the suggestion after it's selected to follow the expected behavior. + const onSuggestionSelected = useCallback((suggestionSelection: TextSelection) => { + const endOfSuggestionSelection = suggestionSelection.end; + setSelection(suggestionSelection); + + if (typeof endOfSuggestionSelection === 'undefined') { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + textInputRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection); + }); + }, []); + const isTouchEndedRef = useRef(false); const containerComposeStyles = StyleSheet.flatten(StyleUtils.getContainerComposeStyles()); @@ -868,7 +850,7 @@ function ComposerWithSuggestions({ // Input value={value} selection={selection} - setSelection={setSelection} + setSelection={onSuggestionSelected} resetKeyboardInput={resetKeyboardInput} /> From ff4bbb2b3b8565cb6f9af4842cc91779aba5b825 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Thu, 13 Nov 2025 15:08:44 -0300 Subject: [PATCH 3/9] Add old comments back and drop typeof when checking for undefined --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index b114533267e37..fca13a10136c9 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -767,10 +767,12 @@ function ComposerWithSuggestions({ const endOfSuggestionSelection = suggestionSelection.end; setSelection(suggestionSelection); - if (typeof endOfSuggestionSelection === 'undefined') { + if (endOfSuggestionSelection === undefined) { return; } + // Ensure that selection is set imperatively after all state changes are effective. + // Note: this implementation is only available on non-web RN // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { textInputRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection); From 107eff57fee27a2bb9970deabbd7f419ceda8da2 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sun, 16 Nov 2025 07:22:23 -0300 Subject: [PATCH 4/9] Revert "Fix cursor not moving to the end of selected suggestion" This reverts commit 0d94f72ad8b600c13e3790a5715d5398cde31cd2. --- .../ComposerWithSuggestions.tsx | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index fca13a10136c9..9e7518ce8b561 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -27,6 +27,7 @@ import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSp import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getPreferredSkinToneIndex, replaceAndExtractEmojis} from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; +import getPlatform from '@libs/getPlatform'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import {detectAndRewritePaste} from '@libs/MarkdownLinkHelpers'; import Parser from '@libs/Parser'; @@ -52,6 +53,11 @@ import type ChildrenProps from '@src/types/utils/ChildrenProps'; // eslint-disable-next-line no-restricted-imports import findNodeHandle from '@src/utils/findNodeHandle'; +type SyncSelection = { + position: number; + value: string; +}; + type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsProps = Partial & { @@ -154,6 +160,8 @@ type ComposerRef = { const {RNTextInputReset} = NativeModules; +const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; + /** * Broadcast that the user is typing. Debounced to limit how often we publish client events. */ @@ -263,6 +271,8 @@ function ComposerWithSuggestions({ const textInputRef = useRef(null); + const syncSelectionWithOnChangeTextRef = useRef(null); + // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -405,6 +415,10 @@ function ComposerWithSuggestions({ if (commentValue !== newComment) { const position = Math.max((selection.end ?? 0) + (newComment.length - commentRef.current.length), cursorPosition ?? 0); + if (commentWithSpaceInserted !== newComment && isIOSNative) { + syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; + } + setSelection((prevSelection) => ({ start: position, end: position, @@ -510,6 +524,26 @@ function ComposerWithSuggestions({ [shouldUseNarrowLayout, isKeyboardShown, suggestionsRef, selection.start, includeChronos, handleSendMessage, lastReportAction, reportID, updateComment, selection.end], ); + const onChangeText = useCallback( + (commentValue: string) => { + updateComment(commentValue, true); + + if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { + const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; + syncSelectionWithOnChangeTextRef.current = null; + + // ensure that selection is set imperatively after all state changes are effective + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + // note: this implementation is only available on non-web RN, thus the wrapping + // 'if' block contains a redundant (since the ref is only used on iOS) platform check + textInputRef.current?.setSelection(positionSnapshot, positionSnapshot); + }); + } + }, + [updateComment], + ); + const onSelectionChange = useCallback( (e: CustomSelectionChangeEvent) => { setSelection(e.nativeEvent.selection); @@ -761,24 +795,6 @@ function ComposerWithSuggestions({ const debouncedUpdateComment = useCallback((newComment: string) => updateComment(newComment, true), [updateComment]); - // When using the suggestions box we need to imperatively set the cursor - // to the end of the suggestion after it's selected to follow the expected behavior. - const onSuggestionSelected = useCallback((suggestionSelection: TextSelection) => { - const endOfSuggestionSelection = suggestionSelection.end; - setSelection(suggestionSelection); - - if (endOfSuggestionSelection === undefined) { - return; - } - - // Ensure that selection is set imperatively after all state changes are effective. - // Note: this implementation is only available on non-web RN - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - textInputRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection); - }); - }, []); - const isTouchEndedRef = useRef(false); const containerComposeStyles = StyleSheet.flatten(StyleUtils.getContainerComposeStyles()); @@ -852,7 +868,7 @@ function ComposerWithSuggestions({ // Input value={value} selection={selection} - setSelection={onSuggestionSelected} + setSelection={setSelection} resetKeyboardInput={resetKeyboardInput} /> From 014a95edae5e0ed5b58aa92be883aa66682081cf Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sun, 16 Nov 2025 07:22:24 -0300 Subject: [PATCH 5/9] Revert "Revert to old updateComment timing behavior" This reverts commit 622a99f9ee4ab9acd889b2e3b23c94cde87d3e6f. --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 9e7518ce8b561..6b72c153b479f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -749,8 +749,7 @@ function ComposerWithSuggestions({ mobileInputScrollPosition.current = 0; // Note: use the value when the clear happened, not the current value which might have changed already onCleared(text); - updateComment('', true); - updateComment('', true); + updateComment(''); }, [onCleared, updateComment], ); @@ -793,8 +792,6 @@ function ComposerWithSuggestions({ [measureParentContainer, cursorPositionValue, selection], ); - const debouncedUpdateComment = useCallback((newComment: string) => updateComment(newComment, true), [updateComment]); - const isTouchEndedRef = useRef(false); const containerComposeStyles = StyleSheet.flatten(StyleUtils.getContainerComposeStyles()); @@ -830,7 +827,7 @@ function ComposerWithSuggestions({ ref={setTextInputRef} placeholder={inputPlaceholder} placeholderTextColor={theme.placeholderText} - onChangeText={debouncedUpdateComment} + onChangeText={updateComment} onKeyPress={handleKeyPress} textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} From 7386914a80506d523db3d52cf7ad853ec1147af0 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sun, 16 Nov 2025 07:32:08 -0300 Subject: [PATCH 6/9] Fix cursor not moving to the end of selected suggestion/mention --- .../ComposerWithSuggestions.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 6b72c153b479f..8d36d575f6caf 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -749,7 +749,7 @@ function ComposerWithSuggestions({ mobileInputScrollPosition.current = 0; // Note: use the value when the clear happened, not the current value which might have changed already onCleared(text); - updateComment(''); + updateComment('', true); }, [onCleared, updateComment], ); @@ -812,6 +812,22 @@ function ComposerWithSuggestions({ onFocus(); }, [onFocus, setUpComposeFocusManager]); + // When using the suggestions box we need to imperatively set the cursor + // to the end of the suggestion after it's selected to follow the expected behavior. + const onSuggestionSelected = useCallback((suggestionSelection: TextSelection) => { + const endOfSuggestionSelection = suggestionSelection.end; + setSelection(suggestionSelection); + + if (typeof endOfSuggestionSelection === 'undefined') { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + textInputRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection); + }); + }, []); + return ( <> From d97973bdf78c01ec0d9e7de28934cc3a071be30f Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sun, 16 Nov 2025 08:36:27 -0300 Subject: [PATCH 7/9] Change comment --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8d36d575f6caf..55640840fb3c7 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -812,8 +812,8 @@ function ComposerWithSuggestions({ onFocus(); }, [onFocus, setUpComposeFocusManager]); - // When using the suggestions box we need to imperatively set the cursor - // to the end of the suggestion after it's selected to follow the expected behavior. + // When using the suggestions box (Suggestions) we need to imperatively + // set the cursor to the end of the suggestion/mention after it's selected. const onSuggestionSelected = useCallback((suggestionSelection: TextSelection) => { const endOfSuggestionSelection = suggestionSelection.end; setSelection(suggestionSelection); From 47eaca6fb04709de61e3b69dec22e54fb9b6a334 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sun, 16 Nov 2025 14:53:08 -0300 Subject: [PATCH 8/9] use queueMicrotask instead of deprecated runAfterInteractions --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 55640840fb3c7..69ee0c46ed0de 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -822,8 +822,7 @@ function ComposerWithSuggestions({ return; } - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { + queueMicrotask(() => { textInputRef.current?.setSelection?.(endOfSuggestionSelection, endOfSuggestionSelection); }); }, []); From a9674fccb106a3c87adf51bd481106abab270e7d Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Fri, 21 Nov 2025 13:27:31 -0300 Subject: [PATCH 9/9] Drop 'typeof' operator when checking for undefined --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 69ee0c46ed0de..e1226f2737f75 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -818,7 +818,7 @@ function ComposerWithSuggestions({ const endOfSuggestionSelection = suggestionSelection.end; setSelection(suggestionSelection); - if (typeof endOfSuggestionSelection === 'undefined') { + if (endOfSuggestionSelection === undefined) { return; }