From 0370382b611b3f292e7fa25d7d831550d038a044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 24 Jul 2024 09:53:07 +0200 Subject: [PATCH 01/18] Revert "Revert "Composer: add clear command that bypasses the event count"" This reverts commit c1ceada27c383635248ca8fcf3d39b71e31a5751. --- src/libs/ComponentUtils/index.native.ts | 15 ++++++++++++++- src/libs/ComponentUtils/index.ts | 11 ++++++++++- .../ReportActionCompose/ReportActionCompose.tsx | 5 +++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/libs/ComponentUtils/index.native.ts b/src/libs/ComponentUtils/index.native.ts index 5ad39162e1a0..7a3492c20ded 100644 --- a/src/libs/ComponentUtils/index.native.ts +++ b/src/libs/ComponentUtils/index.native.ts @@ -1,7 +1,20 @@ +import type {Component} from 'react'; +import type {AnimatedRef} from 'react-native-reanimated'; +import {dispatchCommand} from 'react-native-reanimated'; import type {AccessibilityRoleForm, NewPasswordAutocompleteType, PasswordAutocompleteType} from './types'; const PASSWORD_AUTOCOMPLETE_TYPE: PasswordAutocompleteType = 'password'; const NEW_PASSWORD_AUTOCOMPLETE_TYPE: NewPasswordAutocompleteType = 'password-new'; const ACCESSIBILITY_ROLE_FORM: AccessibilityRoleForm = 'none'; -export {PASSWORD_AUTOCOMPLETE_TYPE, ACCESSIBILITY_ROLE_FORM, NEW_PASSWORD_AUTOCOMPLETE_TYPE}; +/** + * Clears a text input on the UI thread using a custom clear command + * that bypasses the event count check. + */ +function forceClearInput(animatedInputRef: AnimatedRef) { + 'worklet'; + + dispatchCommand(animatedInputRef, 'clear'); +} + +export {PASSWORD_AUTOCOMPLETE_TYPE, ACCESSIBILITY_ROLE_FORM, NEW_PASSWORD_AUTOCOMPLETE_TYPE, forceClearInput}; diff --git a/src/libs/ComponentUtils/index.ts b/src/libs/ComponentUtils/index.ts index 38abb98594da..38179d0fe361 100644 --- a/src/libs/ComponentUtils/index.ts +++ b/src/libs/ComponentUtils/index.ts @@ -1,3 +1,6 @@ +import type {Component} from 'react'; +import type {AnimatedRef} from 'react-native-reanimated'; +import {setNativeProps} from 'react-native-reanimated'; import type {AccessibilityRoleForm, NewPasswordAutocompleteType, PasswordAutocompleteType} from './types'; /** @@ -7,4 +10,10 @@ const PASSWORD_AUTOCOMPLETE_TYPE: PasswordAutocompleteType = 'current-password'; const NEW_PASSWORD_AUTOCOMPLETE_TYPE: NewPasswordAutocompleteType = 'new-password'; const ACCESSIBILITY_ROLE_FORM: AccessibilityRoleForm = 'form'; -export {PASSWORD_AUTOCOMPLETE_TYPE, ACCESSIBILITY_ROLE_FORM, NEW_PASSWORD_AUTOCOMPLETE_TYPE}; +function forceClearInput(animatedInputRef: AnimatedRef) { + 'worklet'; + + setNativeProps(animatedInputRef, {text: ''}); +} + +export {PASSWORD_AUTOCOMPLETE_TYPE, ACCESSIBILITY_ROLE_FORM, NEW_PASSWORD_AUTOCOMPLETE_TYPE, forceClearInput}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 6ff163f6ec37..479d7faddc1f 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -4,7 +4,7 @@ import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFo import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; +import {runOnJS, useAnimatedRef} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentModal from '@components/AttachmentModal'; @@ -23,6 +23,7 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; +import {forceClearInput} from '@libs/ComponentUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import {getDraftComment} from '@libs/DraftCommentUtils'; import getModalState from '@libs/getModalState'; @@ -366,7 +367,7 @@ function ReportActionCompose({ // We are setting the isCommentEmpty flag to true so the status of it will be in sync of the native text input state runOnJS(setIsCommentEmpty)(true); runOnJS(resetFullComposerSize)(); - setNativeProps(animatedRef, {text: ''}); // clears native text input on the UI thread + forceClearInput(animatedRef); runOnJS(submitForm)(); }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); From 0c7af9a7bb590560dc41963d54713c2f79bb8d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 11:07:27 +0200 Subject: [PATCH 02/18] wip: fix mobile clearing + sending --- src/components/Composer/index.native.tsx | 17 +-- src/components/Composer/index.tsx | 1 + src/components/Composer/types.ts | 3 - .../ComposerWithSuggestions.tsx | 100 ++++++++---------- .../ReportActionCompose.tsx | 46 +++----- 5 files changed, 72 insertions(+), 95 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index d5dc4c12afc0..6e5ddb612a7c 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,6 +1,6 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; @@ -64,13 +64,13 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); - useEffect(() => { - if (!shouldClear) { - return; - } - textInput.current?.clear(); - onClear(); - }, [shouldClear, onClear]); + // useEffect(() => { + // if (!shouldClear) { + // return; + // } + // textInput.current?.clear(); + // onClear(); + // }, [shouldClear, onClear]); const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]); @@ -99,6 +99,7 @@ function Composer( } props?.onBlur?.(e); }} + onClear={onClear} /> ); } diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3889c8597843..d6b7db77b665 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -111,6 +111,7 @@ function Composer( if (!shouldClear) { return; } + console.log('>>> Clearing composer'); textInput.current?.clear(); onClear(); }, [shouldClear, onClear]); diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 9c7a5a215c1c..ef28f5e37ff8 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -40,9 +40,6 @@ type ComposerProps = TextInputProps & { /** If the input should clear, it actually gets intercepted instead of .clear() */ shouldClear?: boolean; - /** When the input has cleared whoever owns this input should know about it */ - onClear?: () => void; - /** Whether or not this TextInput is disabled. */ isDisabled?: boolean; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 72d387b07f52..be0e1d3c8dbe 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -7,6 +7,7 @@ import type { MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, + TextInputChangeEventData, TextInputFocusEventData, TextInputKeyPressEventData, TextInputScrollEventData, @@ -15,8 +16,7 @@ import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, V import {useFocusedInputHandler} from 'react-native-keyboard-controller'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import {useSharedValue} from 'react-native-reanimated'; -import type {useAnimatedRef} from 'react-native-reanimated'; +import {useAnimatedRef, useSharedValue} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types'; @@ -31,6 +31,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; +import {forceClearInput} from '@libs/ComponentUtils'; import * as ComposerUtils from '@libs/ComposerUtils'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import {getDraftComment} from '@libs/DraftCommentUtils'; @@ -63,8 +64,6 @@ type SyncSelection = { value: string; }; -type AnimatedRef = ReturnType; - type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsOnyxProps = { @@ -95,6 +94,9 @@ type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & /** Callback to update the value of the composer */ onValueChange: (value: string) => void; + /** Callback when the composer got cleared on the UI thread */ + onCleared?: (text: string) => void; + /** Whether the composer is full size */ isComposerFullSize: boolean; @@ -146,9 +148,6 @@ type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & /** The ref to the suggestions */ suggestionsRef: React.RefObject; - /** The ref to the animated input */ - animatedRef: AnimatedRef; - /** The ref to the next modal will open */ isNextModalWillOpenRef: MutableRefObject; @@ -251,10 +250,10 @@ function ComposerWithSuggestions( measureParentContainer = () => {}, isScrollLikelyLayoutTriggered, raiseIsScrollLikelyLayoutTriggered, + onCleared = () => {}, // Refs suggestionsRef, - animatedRef, isNextModalWillOpenRef, editFocused, @@ -282,7 +281,11 @@ function ComposerWithSuggestions( return draftComment; }); const commentRef = useRef(value); + const lastTextRef = useRef(value); + useEffect(() => { + lastTextRef.current = value; + }, [value]); const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; @@ -309,6 +312,7 @@ function ComposerWithSuggestions( // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); + const animatedRef = useAnimatedRef(); /** * Set the TextInput Ref */ @@ -397,7 +401,7 @@ function ComposerWithSuggestions( * Update the value of the comment in Onyx */ const updateComment = useCallback( - (commentValue: string, shouldDebounceSaveComment?: boolean) => { + (commentValue: string, shouldDebounceSaveComment?: boolean, skipTextInputStateUpdates = false) => { raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); @@ -421,20 +425,23 @@ function ComposerWithSuggestions( setIsCommentEmpty(isNewCommentEmpty); } emojisPresentBefore.current = emojis; - setValue(newCommentConverted); - 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}; - } + if (!skipTextInputStateUpdates) { + setValue(newCommentConverted); + 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, - positionX: prevSelection.positionX, - positionY: prevSelection.positionY, - })); + setSelection((prevSelection) => ({ + start: position, + end: position, + positionX: prevSelection.positionX, + positionY: prevSelection.positionY, + })); + } } commentRef.current = newCommentConverted; @@ -451,30 +458,10 @@ function ComposerWithSuggestions( [findNewlyAddedChars, preferredLocale, preferredSkinTone, reportID, setIsCommentEmpty, suggestionsRef, raiseIsScrollLikelyLayoutTriggered, debouncedSaveReportComment, selection.end], ); + // TODO: its almost like this function should receive the comment to send const prepareCommentAndResetComposer = useCallback((): string => { - const trimmedComment = commentRef.current.trim(); - const commentLength = ReportUtils.getCommentLength(trimmedComment, {reportID}); - - // Don't submit empty comments or comments that exceed the character limit - if (!commentLength || commentLength > CONST.MAX_COMMENT_LENGTH) { - return ''; - } - - // Since we're submitting the form here which should clear the composer - // We don't really care about saving the draft the user was typing - // We need to make sure an empty draft gets saved instead - debouncedSaveReportComment.cancel(); - isCommentPendingSaved.current = false; - - setSelection({start: 0, end: 0, positionX: 0, positionY: 0}); - updateComment(''); - setTextInputShouldClear(true); - if (isComposerFullSize) { - Report.setIsComposerFullSize(reportID, false); - } - setIsFullComposerAvailable(false); - return trimmedComment; - }, [updateComment, setTextInputShouldClear, isComposerFullSize, setIsFullComposerAvailable, reportID, debouncedSaveReportComment]); + throw new Error('DEPRECATED, REFACTOR'); + }, []); /** * Callback to add whatever text is chosen into the main input (used f.e as callback for the emoji picker) @@ -694,14 +681,15 @@ function ComposerWithSuggestions( replaceSelectionWithText, prepareCommentAndResetComposer, isFocused: () => !!textInputRef.current?.isFocused(), + clear: () => { + 'worklet'; + + forceClearInput(animatedRef); + }, }), - [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], + [animatedRef, blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); - useEffect(() => { - lastTextRef.current = value; - }, [value]); - useEffect(() => { onValueChange(value); }, [onValueChange, value]); @@ -717,11 +705,15 @@ function ComposerWithSuggestions( [composerHeight], ); - const onClear = useCallback(() => { - mobileInputScrollPosition.current = 0; - setTextInputShouldClear(false); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); + const onClear = useCallback( + ({nativeEvent}: NativeSyntheticEvent) => { + mobileInputScrollPosition.current = 0; + // Note: use the value when the clear happened, not the current value which might have changed already + onCleared(nativeEvent.text); + updateComment('', true, true); + }, + [onCleared, updateComment], + ); useEffect(() => { // We use the tag to store the native ID of the text input. Later, we use it in onSelectionChange to pick up the proper text input data. diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 479d7faddc1f..adf0bde0f9ed 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,10 +1,8 @@ -import type {SyntheticEvent} from 'react'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import {runOnJS, useAnimatedRef} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentModal from '@components/AttachmentModal'; @@ -23,7 +21,6 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import {forceClearInput} from '@libs/ComponentUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import {getDraftComment} from '@libs/DraftCommentUtils'; import getModalState from '@libs/getModalState'; @@ -46,12 +43,18 @@ import ComposerWithSuggestions from './ComposerWithSuggestions'; import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions/ComposerWithSuggestions'; import SendButton from './SendButton'; +// TODO: move this to composer?! type ComposerRef = { blur: () => void; focus: (shouldDelay?: boolean) => void; replaceSelectionWithText: EmojiPickerActions.OnEmojiSelected; prepareCommentAndResetComposer: () => string; isFocused: () => boolean; + /** + * Calling clear will immediately clear the input on the UI thread (its a worklet). + * Once the composer ahs cleared onCleared will be called with the value that was cleared. + */ + clear: () => void; }; type SuggestionsRef = { @@ -123,7 +126,6 @@ function ReportActionCompose({ const {translate} = useLocalize(); const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); - const animatedRef = useAnimatedRef(); const actionButtonRef = useRef(null); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; @@ -287,16 +289,11 @@ function ReportActionCompose({ * Add a new comment to this chat */ const submitForm = useCallback( - (event?: SyntheticEvent) => { - event?.preventDefault(); - - const newComment = composerRef.current?.prepareCommentAndResetComposer(); - if (!newComment) { - return; - } - + (newComment: string) => { playSound(SOUNDS.DONE); - onSubmit(newComment); + + const newCommentTrimmed = newComment.trim(); + onSubmit(newCommentTrimmed); }, [onSubmit], ); @@ -326,15 +323,6 @@ function ReportActionCompose({ onComposerFocus?.(); }, [onComposerFocus]); - // resets the composer to normal size when - // the send button is pressed. - const resetFullComposerSize = useCallback(() => { - if (isComposerFullSize) { - Report.setIsComposerFullSize(reportID, false); - } - setIsFullComposerAvailable(false); - }, [isComposerFullSize, reportID]); - // We are returning a callback here as we want to incoke the method on unmount only useEffect( () => () => { @@ -357,19 +345,17 @@ function ReportActionCompose({ const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; + const clearComposer = composerRef.current?.clear; const handleSendMessage = useCallback(() => { 'worklet'; - if (isSendDisabled || !isReportReadyForDisplay) { + if (isSendDisabled || !isReportReadyForDisplay || !clearComposer) { return; } - // We are setting the isCommentEmpty flag to true so the status of it will be in sync of the native text input state - runOnJS(setIsCommentEmpty)(true); - runOnJS(resetFullComposerSize)(); - forceClearInput(animatedRef); - runOnJS(submitForm)(); - }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); + // This will cause onCleared to be triggered where we actually send the message + clearComposer(); + }, [isSendDisabled, isReportReadyForDisplay, clearComposer]); const emojiShiftVertical = useMemo(() => { const chatItemComposeSecondaryRowHeight = styles.chatItemComposeSecondaryRow.height + styles.chatItemComposeSecondaryRow.marginTop + styles.chatItemComposeSecondaryRow.marginBottom; @@ -432,7 +418,6 @@ function ReportActionCompose({ /> { if (value.length === 0 && isComposerFullSize) { From 969365593b669fd90dbf94a8bd70384aee47dae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 11:15:19 +0200 Subject: [PATCH 03/18] add todo notice --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index be0e1d3c8dbe..786be397b5a3 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -239,6 +239,7 @@ function ComposerWithSuggestions( inputPlaceholder, displayFileInModal, textInputShouldClear, + // TODO: remove this clearing mechanism and use the clear method on the ref setTextInputShouldClear, isBlockedFromConcierge, disabled, From 60af307f768228fd9b95fc406cd5e43b804c0ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 12:52:27 +0200 Subject: [PATCH 04/18] fix web --- src/components/Composer/index.tsx | 46 ++++++++++++++----- src/libs/ComponentUtils/index.ts | 6 +-- .../ComposerWithSuggestions.tsx | 14 +++--- .../ReportActionCompose.tsx | 28 ++++++++--- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index d6b7db77b665..6c2d32e8457f 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -1,7 +1,7 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import lodashDebounce from 'lodash/debounce'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; @@ -12,6 +12,7 @@ import Text from '@components/Text'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; +import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -107,14 +108,16 @@ function Composer( const [prevScroll, setPrevScroll] = useState(); const isReportFlatListScrolling = useRef(false); - useEffect(() => { - if (!shouldClear) { - return; - } - console.log('>>> Clearing composer'); - textInput.current?.clear(); - onClear(); - }, [shouldClear, onClear]); + // useEffect(() => { + // if (!shouldClear) { + // return; + // } + + // textInput.current?.clear(); + // onClear(); + // }, [shouldClear, onClear]); + + const prevValue = usePrevious(value); useEffect(() => { if (!!selection && selectionProp.start === selection.start && selectionProp.end === selection.end) { @@ -285,9 +288,6 @@ function Composer( useHtmlPaste(textInput, handlePaste, true); useEffect(() => { - if (typeof ref === 'function') { - ref(textInput.current); - } setIsRendered(true); return () => { @@ -299,6 +299,28 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + useImperativeHandle( + ref, + () => { + if (!textInput.current) { + throw new Error('TextInput ref is not available'); + } + + return { + ...textInput.current, + clear: () => { + textInput.current?.clear(); + onClear({ + nativeEvent: { + text: prevValue, + }, + }); + }, + }; + }, + [onClear, prevValue], + ); + const handleKeyPress = useCallback( (e: NativeSyntheticEvent) => { // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed diff --git a/src/libs/ComponentUtils/index.ts b/src/libs/ComponentUtils/index.ts index 38179d0fe361..f7c48f87af5a 100644 --- a/src/libs/ComponentUtils/index.ts +++ b/src/libs/ComponentUtils/index.ts @@ -1,6 +1,6 @@ import type {Component} from 'react'; +import type {TextInput} from 'react-native'; import type {AnimatedRef} from 'react-native-reanimated'; -import {setNativeProps} from 'react-native-reanimated'; import type {AccessibilityRoleForm, NewPasswordAutocompleteType, PasswordAutocompleteType} from './types'; /** @@ -10,10 +10,10 @@ const PASSWORD_AUTOCOMPLETE_TYPE: PasswordAutocompleteType = 'current-password'; const NEW_PASSWORD_AUTOCOMPLETE_TYPE: NewPasswordAutocompleteType = 'new-password'; const ACCESSIBILITY_ROLE_FORM: AccessibilityRoleForm = 'form'; -function forceClearInput(animatedInputRef: AnimatedRef) { +function forceClearInput(_: AnimatedRef, textInputRef: React.RefObject) { 'worklet'; - setNativeProps(animatedInputRef, {text: ''}); + textInputRef.current?.clear(); } export {PASSWORD_AUTOCOMPLETE_TYPE, ACCESSIBILITY_ROLE_FORM, NEW_PASSWORD_AUTOCOMPLETE_TYPE, forceClearInput}; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 786be397b5a3..6738070749cd 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -621,6 +621,12 @@ function ComposerWithSuggestions( textInputRef.current.blur(); }, []); + const clear = useCallback(() => { + 'worklet'; + + forceClearInput(animatedRef, textInputRef); + }, [animatedRef]); + useEffect(() => { const unsubscribeNavigationBlur = navigation.addListener('blur', () => KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress)); const unsubscribeNavigationFocus = navigation.addListener('focus', () => { @@ -682,13 +688,9 @@ function ComposerWithSuggestions( replaceSelectionWithText, prepareCommentAndResetComposer, isFocused: () => !!textInputRef.current?.isFocused(), - clear: () => { - 'worklet'; - - forceClearInput(animatedRef); - }, + clear, }), - [animatedRef, blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], + [blur, clear, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); useEffect(() => { diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index adf0bde0f9ed..280de7f2c6e2 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -3,6 +3,7 @@ import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFo import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import {useSharedValue} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentModal from '@components/AttachmentModal'; @@ -180,7 +181,7 @@ function ReportActionCompose({ const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); const suggestionsRef = useRef(null); - const composerRef = useRef(null); + const composerRef = useRef(); const reportParticipantIDs = useMemo( () => Object.keys(report?.participants ?? {}) @@ -222,7 +223,7 @@ function ReportActionCompose({ if (composerRef.current === null) { return; } - composerRef.current.focus(true); + composerRef.current?.focus(true); }; const isKeyboardVisibleWhenShowingModalRef = useRef(false); @@ -345,17 +346,26 @@ function ReportActionCompose({ const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; - const clearComposer = composerRef.current?.clear; + // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value + // useSharedValue on web doesn't support functions, so we need to wrap it in an object. + const composerRefShared = useSharedValue<{ + clear: (() => void) | undefined; + }>({clear: undefined}); const handleSendMessage = useCallback(() => { 'worklet'; - if (isSendDisabled || !isReportReadyForDisplay || !clearComposer) { + const clearComposer = composerRefShared.value.clear; + if (!clearComposer) { + throw new Error('The composerRefShared.clear function is not set yet. This should never happen, and indicates a developer error.'); + } + + if (isSendDisabled || !isReportReadyForDisplay) { return; } // This will cause onCleared to be triggered where we actually send the message clearComposer(); - }, [isSendDisabled, isReportReadyForDisplay, clearComposer]); + }, [isSendDisabled, isReportReadyForDisplay, composerRefShared]); const emojiShiftVertical = useMemo(() => { const chatItemComposeSecondaryRowHeight = styles.chatItemComposeSecondaryRow.height + styles.chatItemComposeSecondaryRow.marginTop + styles.chatItemComposeSecondaryRow.marginBottom; @@ -417,7 +427,13 @@ function ReportActionCompose({ actionButtonRef={actionButtonRef} /> { + composerRef.current = ref ?? undefined; + // eslint-disable-next-line react-compiler/react-compiler + composerRefShared.value = { + clear: ref?.clear, + }; + }} suggestionsRef={suggestionsRef} isNextModalWillOpenRef={isNextModalWillOpenRef} isScrollLikelyLayoutTriggered={isScrollLikelyLayoutTriggered} From f188e40c12b1f3a67bbad1259d243b5e7c354816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 13:17:16 +0200 Subject: [PATCH 05/18] simplify onClear API --- src/components/Composer/index.native.tsx | 11 ++++++-- src/components/Composer/index.tsx | 26 ++++++++++--------- src/components/Composer/types.ts | 8 +++++- .../ComposerWithSuggestions.tsx | 4 +-- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 6e5ddb612a7c..2e523b3b28ce 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,7 +1,7 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; import React, {useCallback, useMemo, useRef} from 'react'; -import type {TextInput} from 'react-native'; +import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData} from 'react-native'; import {StyleSheet} from 'react-native'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; @@ -20,7 +20,7 @@ const excludeReportMentionStyle: Array = ['mentionReport']; function Composer( { shouldClear = false, - onClear = () => {}, + onClear: onClearProp = () => {}, isDisabled = false, maxLines, isComposerFullSize = false, @@ -72,6 +72,13 @@ function Composer( // onClear(); // }, [shouldClear, onClear]); + const onClear = useCallback( + ({nativeEvent}: NativeSyntheticEvent) => { + onClearProp(nativeEvent.text); + }, + [onClearProp], + ); + const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]); diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 6c2d32e8457f..9a93ae238c42 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -117,8 +117,6 @@ function Composer( // onClear(); // }, [shouldClear, onClear]); - const prevValue = usePrevious(value); - useEffect(() => { if (!!selection && selectionProp.start === selection.start && selectionProp.end === selection.end) { return; @@ -299,26 +297,30 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + const prevValue = usePrevious(value); + const clear = useCallback(() => { + if (!textInput.current || prevValue === undefined) { + return; + } + + textInput.current.clear(); + console.log('prevValue', prevValue); + onClear(prevValue); + }, [onClear, prevValue]); + useImperativeHandle( ref, () => { if (!textInput.current) { - throw new Error('TextInput ref is not available'); + throw new Error('TextInput ref is not available. This should never happen and indicates a developer error.'); } return { ...textInput.current, - clear: () => { - textInput.current?.clear(); - onClear({ - nativeEvent: { - text: prevValue, - }, - }); - }, + clear, }; }, - [onClear, prevValue], + [clear], ); const handleKeyPress = useCallback( diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index ef28f5e37ff8..95c300310289 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -11,7 +11,7 @@ type CustomSelectionChangeEvent = NativeSyntheticEvent & { /** identify id in the text input */ id?: string; @@ -27,6 +27,12 @@ type ComposerProps = TextInputProps & { /** The value of the comment box */ value?: string; + /** + * Callback when the input was cleared using the .clear ref method. + * The text parameter will be the value of the text that was cleared. + */ + onClear?: (text: string) => void; + /** Callback method handle when the input is changed */ onChangeText?: (numberOfLines: string) => void; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 6738070749cd..55a552df652e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -709,10 +709,10 @@ function ComposerWithSuggestions( ); const onClear = useCallback( - ({nativeEvent}: NativeSyntheticEvent) => { + (text: string) => { mobileInputScrollPosition.current = 0; // Note: use the value when the clear happened, not the current value which might have changed already - onCleared(nativeEvent.text); + onCleared(text); updateComment('', true, true); }, [onCleared, updateComment], From 283fad29892fe841cb2d9055454104f44899708c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 13:49:36 +0200 Subject: [PATCH 06/18] wip: fix web plus menu not working --- src/components/Composer/index.tsx | 12 ++++++++---- .../ComposerWithSuggestions.tsx | 14 ++++++-------- .../ReportActionCompose/ReportActionCompose.tsx | 4 ++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 9a93ae238c42..5953970ed766 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -304,20 +304,24 @@ function Composer( } textInput.current.clear(); - console.log('prevValue', prevValue); onClear(prevValue); }, [onClear, prevValue]); useImperativeHandle( ref, () => { - if (!textInput.current) { - throw new Error('TextInput ref is not available. This should never happen and indicates a developer error.'); + const textInputRef = textInput.current; + if (!textInputRef) { + throw new Error('textInputRef is not available. This should never happen and indicates a developer error.'); } return { - ...textInput.current, + ...textInputRef, + // Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works clear, + // We have to redefine these methods as they are inherited by prototype chain and are not accessible directly + blur: () => textInputRef.blur(), + focus: () => textInputRef.focus(), }; }, [clear], diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 55a552df652e..ea03d57a3dd9 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -7,7 +7,6 @@ import type { MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, - TextInputChangeEventData, TextInputFocusEventData, TextInputKeyPressEventData, TextInputScrollEventData, @@ -459,11 +458,6 @@ function ComposerWithSuggestions( [findNewlyAddedChars, preferredLocale, preferredSkinTone, reportID, setIsCommentEmpty, suggestionsRef, raiseIsScrollLikelyLayoutTriggered, debouncedSaveReportComment, selection.end], ); - // TODO: its almost like this function should receive the comment to send - const prepareCommentAndResetComposer = useCallback((): string => { - throw new Error('DEPRECATED, REFACTOR'); - }, []); - /** * Callback to add whatever text is chosen into the main input (used f.e as callback for the emoji picker) */ @@ -627,6 +621,10 @@ function ComposerWithSuggestions( forceClearInput(animatedRef, textInputRef); }, [animatedRef]); + const getCurrentText = useCallback(() => { + return commentRef.current; + }, []); + useEffect(() => { const unsubscribeNavigationBlur = navigation.addListener('blur', () => KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress)); const unsubscribeNavigationFocus = navigation.addListener('focus', () => { @@ -686,11 +684,11 @@ function ComposerWithSuggestions( blur, focus, replaceSelectionWithText, - prepareCommentAndResetComposer, isFocused: () => !!textInputRef.current?.isFocused(), clear, + getCurrentText, }), - [blur, clear, focus, prepareCommentAndResetComposer, replaceSelectionWithText], + [blur, clear, focus, replaceSelectionWithText, getCurrentText], ); useEffect(() => { diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 280de7f2c6e2..37a7b6f637f9 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -49,7 +49,7 @@ type ComposerRef = { blur: () => void; focus: (shouldDelay?: boolean) => void; replaceSelectionWithText: EmojiPickerActions.OnEmojiSelected; - prepareCommentAndResetComposer: () => string; + getCurrentText: () => string; isFocused: () => boolean; /** * Calling clear will immediately clear the input on the UI thread (its a worklet). @@ -270,7 +270,7 @@ function ReportActionCompose({ const addAttachment = useCallback( (file: FileObject) => { playSound(SOUNDS.DONE); - const newComment = composerRef?.current?.prepareCommentAndResetComposer(); + const newComment = composerRef?.current?.getCurrentText().trim(); Report.addAttachment(reportID, file, newComment); setTextInputShouldClear(false); }, From ec6ab19156d6d376b8c8c6d8045c26bf9bc392b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 14:00:31 +0200 Subject: [PATCH 07/18] fix sending attachments --- .../ReportActionCompose.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 37a7b6f637f9..4cef1b5f0b0b 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -3,7 +3,7 @@ import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFo import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import {useSharedValue} from 'react-native-reanimated'; +import {runOnUI, useSharedValue} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentModal from '@components/AttachmentModal'; @@ -267,15 +267,16 @@ function ReportActionCompose({ suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(false); }, []); - const addAttachment = useCallback( - (file: FileObject) => { - playSound(SOUNDS.DONE); - const newComment = composerRef?.current?.getCurrentText().trim(); - Report.addAttachment(reportID, file, newComment); - setTextInputShouldClear(false); - }, - [reportID], - ); + const attachmentFileRef = useRef(null); + const addAttachment = useCallback((file: FileObject) => { + attachmentFileRef.current = file; + const clear = composerRef.current?.clear; + if (!clear) { + throw new Error('The composerRef.clear function is not set yet. This should never happen, and indicates a developer error.'); + } + + runOnUI(clear)(); + }, []); /** * Event handler to update the state after the attachment preview is closed. @@ -294,9 +295,15 @@ function ReportActionCompose({ playSound(SOUNDS.DONE); const newCommentTrimmed = newComment.trim(); - onSubmit(newCommentTrimmed); + + if (attachmentFileRef.current) { + Report.addAttachment(reportID, attachmentFileRef.current, newCommentTrimmed); + attachmentFileRef.current = null; + } else { + onSubmit(newCommentTrimmed); + } }, - [onSubmit], + [onSubmit, reportID], ); const onTriggerAttachmentPicker = useCallback(() => { From 7e08dc9ce5429244e02cfbbbbf849250605b429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 14:07:24 +0200 Subject: [PATCH 08/18] remove textInputShouldClear --- src/components/Composer/index.native.tsx | 9 --------- src/components/Composer/index.tsx | 10 ---------- src/components/Composer/types.ts | 3 --- .../ComposerWithSuggestions.tsx | 10 ---------- .../report/ReportActionCompose/ReportActionCompose.tsx | 6 ------ 5 files changed, 38 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 2e523b3b28ce..68a8c56c4df9 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -19,7 +19,6 @@ const excludeReportMentionStyle: Array = ['mentionReport']; function Composer( { - shouldClear = false, onClear: onClearProp = () => {}, isDisabled = false, maxLines, @@ -64,14 +63,6 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); - // useEffect(() => { - // if (!shouldClear) { - // return; - // } - // textInput.current?.clear(); - // onClear(); - // }, [shouldClear, onClear]); - const onClear = useCallback( ({nativeEvent}: NativeSyntheticEvent) => { onClearProp(nativeEvent.text); diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 5953970ed766..54fec270df87 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -60,7 +60,6 @@ function Composer( maxLines = -1, onKeyPress = () => {}, style, - shouldClear = false, autoFocus = false, shouldCalculateCaretPosition = false, isDisabled = false, @@ -108,15 +107,6 @@ function Composer( const [prevScroll, setPrevScroll] = useState(); const isReportFlatListScrolling = useRef(false); - // useEffect(() => { - // if (!shouldClear) { - // return; - // } - - // textInput.current?.clear(); - // onClear(); - // }, [shouldClear, onClear]); - useEffect(() => { if (!!selection && selectionProp.start === selection.start && selectionProp.end === selection.end) { return; diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 95c300310289..e6d8a882f3b8 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -43,9 +43,6 @@ type ComposerProps = Omit & { // eslint-disable-next-line react/forbid-prop-types style?: StyleProp; - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear?: boolean; - /** Whether or not this TextInput is disabled. */ isDisabled?: boolean; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index ea03d57a3dd9..b926a5a83234 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -108,12 +108,6 @@ type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & /** Function to display a file in a modal */ displayFileInModal: (file: FileObject) => void; - /** Whether the text input should clear */ - textInputShouldClear: boolean; - - /** Function to set the text input should clear */ - setTextInputShouldClear: (shouldClear: boolean) => void; - /** Whether the user is blocked from concierge */ isBlockedFromConcierge: boolean; @@ -237,9 +231,6 @@ function ComposerWithSuggestions( isMenuVisible, inputPlaceholder, displayFileInModal, - textInputShouldClear, - // TODO: remove this clearing mechanism and use the clear method on the ref - setTextInputShouldClear, isBlockedFromConcierge, disabled, isFullComposerAvailable, @@ -774,7 +765,6 @@ function ComposerWithSuggestions( onBlur={onBlur} onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={displayFileInModal} - shouldClear={textInputShouldClear} onClear={onClear} isDisabled={isBlockedFromConcierge || disabled} isReportActionCompose diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 4cef1b5f0b0b..0c4e3fd3eb1f 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -159,10 +159,6 @@ function ReportActionCompose({ debouncedLowerIsScrollLikelyLayoutTriggered(); }, [debouncedLowerIsScrollLikelyLayoutTriggered]); - /** - * Updates the should clear state of the composer - */ - const [textInputShouldClear, setTextInputShouldClear] = useState(false); const [isCommentEmpty, setIsCommentEmpty] = useState(() => { const draftComment = getDraftComment(reportID); return !draftComment || !!draftComment.match(/^(\s)*$/); @@ -457,8 +453,6 @@ function ReportActionCompose({ inputPlaceholder={inputPlaceholder} isComposerFullSize={isComposerFullSize} displayFileInModal={displayFileInModal} - textInputShouldClear={textInputShouldClear} - setTextInputShouldClear={setTextInputShouldClear} isBlockedFromConcierge={isBlockedFromConcierge} disabled={!!disabled} isFullComposerAvailable={isFullComposerAvailable} From 94f0856a8ef14f3d2bb82fece8a30a81cdb5f45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 25 Jul 2024 16:42:55 +0200 Subject: [PATCH 09/18] add native patch --- ...djustFontSizeToFit-new-architecture.patch} | 0 ...act-native+0.73.4+022+textInputClear.patch | 66 ----- ...e+0.73.4+023+textinput-clear-command.patch | 273 ++++++++++++++++++ 3 files changed, 273 insertions(+), 66 deletions(-) rename patches/{react-native+0.73.4+023+iOS-fix-adjustFontSizeToFit-new-architecture.patch => react-native+0.73.4+022+iOS-fix-adjustFontSizeToFit-new-architecture.patch} (100%) delete mode 100644 patches/react-native+0.73.4+022+textInputClear.patch create mode 100644 patches/react-native+0.73.4+023+textinput-clear-command.patch diff --git a/patches/react-native+0.73.4+023+iOS-fix-adjustFontSizeToFit-new-architecture.patch b/patches/react-native+0.73.4+022+iOS-fix-adjustFontSizeToFit-new-architecture.patch similarity index 100% rename from patches/react-native+0.73.4+023+iOS-fix-adjustFontSizeToFit-new-architecture.patch rename to patches/react-native+0.73.4+022+iOS-fix-adjustFontSizeToFit-new-architecture.patch diff --git a/patches/react-native+0.73.4+022+textInputClear.patch b/patches/react-native+0.73.4+022+textInputClear.patch deleted file mode 100644 index 1cadce6a0783..000000000000 --- a/patches/react-native+0.73.4+022+textInputClear.patch +++ /dev/null @@ -1,66 +0,0 @@ -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -index 7ce04da..123968f 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -@@ -452,6 +452,12 @@ - (void)blur - [_backedTextInputView resignFirstResponder]; - } - -+- (void)clear -+{ -+ [self setTextAndSelection:_mostRecentEventCount value:@"" start:0 end:0]; -+ _mostRecentEventCount++; -+} -+ - - (void)setTextAndSelection:(NSInteger)eventCount - value:(NSString *__nullable)value - start:(NSInteger)start -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -index fe3376a..6a9a45f 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN - @protocol RCTTextInputViewProtocol - - (void)focus; - - (void)blur; -+- (void)clear; - - (void)setTextAndSelection:(NSInteger)eventCount - value:(NSString *__nullable)value - start:(NSInteger)start -@@ -49,6 +50,19 @@ RCTTextInputHandleCommand(id componentView, const NSSt - return; - } - -+ if ([commandName isEqualToString:@"clear"]) { -+#if RCT_DEBUG -+ if ([args count] != 0) { -+ RCTLogError( -+ @"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0); -+ return; -+ } -+#endif -+ -+ [componentView clear]; -+ return; -+ } -+ - if ([commandName isEqualToString:@"setTextAndSelection"]) { - #if RCT_DEBUG - if ([args count] != 4) { -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index 8496a7d..e6bcfc4 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -331,6 +331,12 @@ public class ReactTextInputManager extends BaseViewManager) => void) + | undefined; + ++ /** ++ * Callback that is called when the text input was cleared using the native clear command. ++ */ ++ onClear?: ++ | ((e: NativeSyntheticEvent) => void) ++ | undefined; ++ + /** + * Callback that is called when the text input's text changes. + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +index 481938f..c4b7177 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -1329,6 +1329,11 @@ function InternalTextInput(props: Props): React.Node { + }); + }; + ++ const _onClear = (event: ChangeEvent) => { ++ setMostRecentEventCount(event.nativeEvent.eventCount); ++ props.onClear && props.onClear(event); ++ } ++ + const _onFocus = (event: FocusEvent) => { + TextInputState.focusInput(inputRef.current); + if (props.onFocus) { +@@ -1462,6 +1467,7 @@ function InternalTextInput(props: Props): React.Node { + nativeID={id ?? props.nativeID} + onBlur={_onBlur} + onKeyPressSync={props.unstable_onKeyPressSync} ++ onClear={_onClear} + onChange={_onChange} + onChangeSync={useOnChangeSync === true ? _onChangeSync : null} + onContentSizeChange={props.onContentSizeChange} +diff --git a/node_modules/react-native/Libraries/Text/.DS_Store b/node_modules/react-native/Libraries/Text/.DS_Store +new file mode 100644 +index 0000000..7c455d8 +Binary files /dev/null and b/node_modules/react-native/Libraries/Text/.DS_Store differ +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +index a19b555..4785987 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +@@ -62,6 +62,7 @@ @implementation RCTBaseTextInputViewManager { + + RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock) ++RCT_EXPORT_VIEW_PROPERTY(onClear, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/.DS_Store b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/.DS_Store +new file mode 100644 +index 0000000..5b10679 +Binary files /dev/null and b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/.DS_Store differ +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +index 7ce04da..70754bf 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +@@ -452,6 +452,19 @@ - (void)blur + [_backedTextInputView resignFirstResponder]; + } + ++- (void)clear ++{ ++ auto metrics = [self _textInputMetrics]; ++ [self setTextAndSelection:_mostRecentEventCount value:@"" start:0 end:0]; ++ ++ _mostRecentEventCount++; ++ metrics.eventCount = _mostRecentEventCount; ++ ++ // Notify JS that the event counter has changed ++ const auto &textInputEventEmitter = static_cast(*_eventEmitter); ++ textInputEventEmitter.onClear(metrics); ++} ++ + - (void)setTextAndSelection:(NSInteger)eventCount + value:(NSString *__nullable)value + start:(NSInteger)start +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h +index fe3376a..6889eed 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h +@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN + @protocol RCTTextInputViewProtocol + - (void)focus; + - (void)blur; ++- (void)clear; + - (void)setTextAndSelection:(NSInteger)eventCount + value:(NSString *__nullable)value + start:(NSInteger)start +@@ -49,6 +50,19 @@ RCTTextInputHandleCommand(id componentView, const NSSt + return; + } + ++ if ([commandName isEqualToString:@"clear"]) { ++#if RCT_DEBUG ++ if ([args count] != 0) { ++ RCTLogError( ++ @"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0); ++ return; ++ } ++#endif ++ ++ [componentView clear]; ++ return; ++ } ++ + if ([commandName isEqualToString:@"setTextAndSelection"]) { + #if RCT_DEBUG + if ([args count] != 4) { +diff --git a/node_modules/react-native/ReactAndroid/.DS_Store b/node_modules/react-native/ReactAndroid/.DS_Store +new file mode 100644 +index 0000000..bbbb59c +Binary files /dev/null and b/node_modules/react-native/ReactAndroid/.DS_Store differ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java +new file mode 100644 +index 0000000..0c142a0 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java +@@ -0,0 +1,53 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.textinput; ++ ++import androidx.annotation.Nullable; ++ ++import com.facebook.react.bridge.Arguments; ++import com.facebook.react.bridge.WritableMap; ++import com.facebook.react.uimanager.common.ViewUtil; ++import com.facebook.react.uimanager.events.Event; ++ ++/** ++ * Event emitted by EditText native view when text changes. VisibleForTesting from {@link ++ * TextInputEventsTestCase}. ++ */ ++public class ReactTextClearEvent extends Event { ++ ++ public static final String EVENT_NAME = "topClear"; ++ ++ private String mText; ++ private int mEventCount; ++ ++ @Deprecated ++ public ReactTextClearEvent(int viewId, String text, int eventCount) { ++ this(ViewUtil.NO_SURFACE_ID, viewId, text, eventCount); ++ } ++ ++ public ReactTextClearEvent(int surfaceId, int viewId, String text, int eventCount) { ++ super(surfaceId, viewId); ++ mText = text; ++ mEventCount = eventCount; ++ } ++ ++ @Override ++ public String getEventName() { ++ return EVENT_NAME; ++ } ++ ++ @Nullable ++ @Override ++ protected WritableMap getEventData() { ++ WritableMap eventData = Arguments.createMap(); ++ eventData.putString("text", mText); ++ eventData.putInt("eventCount", mEventCount); ++ eventData.putInt("target", getViewTag()); ++ return eventData; ++ } ++} +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +index 8496a7d..764a392 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +@@ -8,6 +8,7 @@ + package com.facebook.react.views.textinput; + + import static com.facebook.react.uimanager.UIManagerHelper.getReactContext; ++import static com.facebook.react.uimanager.UIManagerHelper.getSurfaceId; + + import android.content.Context; + import android.content.res.ColorStateList; +@@ -273,6 +274,9 @@ public class ReactTextInputManager extends BaseViewManager Date: Thu, 25 Jul 2024 17:58:18 +0200 Subject: [PATCH 10/18] fix android onClear not called --- ...e+0.73.4+023+textinput-clear-command.patch | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/patches/react-native+0.73.4+023+textinput-clear-command.patch b/patches/react-native+0.73.4+023+textinput-clear-command.patch index c75b5cdc9eb0..f07a2e24d39d 100644 --- a/patches/react-native+0.73.4+023+textinput-clear-command.patch +++ b/patches/react-native+0.73.4+023+textinput-clear-command.patch @@ -31,7 +31,7 @@ index 2c0c099..26a477f 100644 * Callback that is called when the text input's text changes. */ diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -index 481938f..c4b7177 100644 +index 481938f..346acaa 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js @@ -1329,6 +1329,11 @@ function InternalTextInput(props: Props): React.Node { @@ -41,7 +41,7 @@ index 481938f..c4b7177 100644 + const _onClear = (event: ChangeEvent) => { + setMostRecentEventCount(event.nativeEvent.eventCount); + props.onClear && props.onClear(event); -+ } ++ }; + const _onFocus = (event: FocusEvent) => { TextInputState.focusInput(inputRef.current); @@ -54,6 +54,14 @@ index 481938f..c4b7177 100644 onChange={_onChange} onChangeSync={useOnChangeSync === true ? _onChangeSync : null} onContentSizeChange={props.onContentSizeChange} +@@ -1516,6 +1522,7 @@ function InternalTextInput(props: Props): React.Node { + nativeID={id ?? props.nativeID} + numberOfLines={props.rows ?? props.numberOfLines} + onBlur={_onBlur} ++ onClear={_onClear} + onChange={_onChange} + onFocus={_onFocus} + /* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match diff --git a/node_modules/react-native/Libraries/Text/.DS_Store b/node_modules/react-native/Libraries/Text/.DS_Store new file mode 100644 index 0000000..7c455d8 @@ -194,7 +202,7 @@ index 0000000..0c142a0 + } +} diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index 8496a7d..764a392 100644 +index 8496a7d..53e5c49 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -8,6 +8,7 @@ @@ -224,10 +232,10 @@ index 8496a7d..764a392 100644 + // Capture the current text + Editable text = reactEditText.getText(); + -+ int newEventCount = reactEditText.incrementAndGetEventCounter(); -+ ReactTextUpdate textUpdate = getReactTextUpdate("", newEventCount); ++ // Reset the edit text ++ ReactTextUpdate textUpdate = getReactTextUpdate("", reactEditText.incrementAndGetEventCounter()); + reactEditText.maybeSetTextFromJS(textUpdate); -+ reactEditText.maybeSetSelection(newEventCount, 0, 0); ++ reactEditText.maybeSetSelection(reactEditText.incrementAndGetEventCounter(), 0, 0); + + // Dispatch the clear event + EventDispatcher eventDispatcher = getEventDispatcher(getReactContext(reactEditText), reactEditText); @@ -236,7 +244,7 @@ index 8496a7d..764a392 100644 + getSurfaceId(reactEditText), + reactEditText.getId(), + text.toString(), -+ newEventCount ++ reactEditText.incrementAndGetEventCounter() + ) + ); + From 3912d552a7d627cb1ede494980633863c4ff8d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 26 Jul 2024 11:13:19 +0200 Subject: [PATCH 11/18] update value state in onClear --- .../ComposerWithSuggestions.tsx | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index e71c4829110a..496a621404c7 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -392,7 +392,7 @@ function ComposerWithSuggestions( * Update the value of the comment in Onyx */ const updateComment = useCallback( - (commentValue: string, shouldDebounceSaveComment?: boolean, skipTextInputStateUpdates = false) => { + (commentValue: string, shouldDebounceSaveComment?: boolean) => { raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); @@ -417,22 +417,20 @@ function ComposerWithSuggestions( } emojisPresentBefore.current = emojis; - if (!skipTextInputStateUpdates) { - setValue(newCommentConverted); - if (commentValue !== newComment) { - const position = Math.max((selection.end ?? 0) + (newComment.length - commentRef.current.length), cursorPosition ?? 0); + setValue(newCommentConverted); + 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, - positionX: prevSelection.positionX, - positionY: prevSelection.positionY, - })); + if (commentWithSpaceInserted !== newComment && isIOSNative) { + syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; } + + setSelection((prevSelection) => ({ + start: position, + end: position, + positionX: prevSelection.positionX, + positionY: prevSelection.positionY, + })); } commentRef.current = newCommentConverted; @@ -702,7 +700,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, true); + updateComment('', true); }, [onCleared, updateComment], ); From 80d0b0695f1b26ceb9d07f28465b8239045346f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 26 Jul 2024 12:15:15 +0200 Subject: [PATCH 12/18] add selection fix for web --- src/components/Composer/index.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 54fec270df87..418cb487e937 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -294,8 +294,21 @@ function Composer( } textInput.current.clear(); + + // We need to reset the selection to 0,0 manually after clearing the text input on web + const selectionEvent = { + nativeEvent: { + selection: { + start: 0, + end: 0, + }, + }, + } as NativeSyntheticEvent; + onSelectionChange(selectionEvent); + setSelection({start: 0, end: 0}); + onClear(prevValue); - }, [onClear, prevValue]); + }, [onClear, onSelectionChange, prevValue]); useImperativeHandle( ref, @@ -370,6 +383,10 @@ function Composer( [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], ); + console.log({ + value, + selection, + }); return ( <> Date: Fri, 26 Jul 2024 12:30:49 +0200 Subject: [PATCH 13/18] remove debugging code --- src/components/Composer/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 418cb487e937..8f0051eb552b 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -383,10 +383,6 @@ function Composer( [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], ); - console.log({ - value, - selection, - }); return ( <> Date: Fri, 26 Jul 2024 13:02:39 +0200 Subject: [PATCH 14/18] clean patch file --- ...t-native+0.73.4+023+textinput-clear-command.patch | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/patches/react-native+0.73.4+023+textinput-clear-command.patch b/patches/react-native+0.73.4+023+textinput-clear-command.patch index f07a2e24d39d..a18e8c4f4333 100644 --- a/patches/react-native+0.73.4+023+textinput-clear-command.patch +++ b/patches/react-native+0.73.4+023+textinput-clear-command.patch @@ -62,10 +62,6 @@ index 481938f..346acaa 100644 onChange={_onChange} onFocus={_onFocus} /* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match -diff --git a/node_modules/react-native/Libraries/Text/.DS_Store b/node_modules/react-native/Libraries/Text/.DS_Store -new file mode 100644 -index 0000000..7c455d8 -Binary files /dev/null and b/node_modules/react-native/Libraries/Text/.DS_Store differ diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm index a19b555..4785987 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm @@ -78,10 +74,6 @@ index a19b555..4785987 100644 RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/.DS_Store b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/.DS_Store -new file mode 100644 -index 0000000..5b10679 -Binary files /dev/null and b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/.DS_Store differ diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 7ce04da..70754bf 100644 --- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -138,10 +130,6 @@ index fe3376a..6889eed 100644 if ([commandName isEqualToString:@"setTextAndSelection"]) { #if RCT_DEBUG if ([args count] != 4) { -diff --git a/node_modules/react-native/ReactAndroid/.DS_Store b/node_modules/react-native/ReactAndroid/.DS_Store -new file mode 100644 -index 0000000..bbbb59c -Binary files /dev/null and b/node_modules/react-native/ReactAndroid/.DS_Store differ diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java new file mode 100644 index 0000000..0c142a0 From ef1410914e8bea0dc053e4d8e1d4f3fdd6f73eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 26 Jul 2024 18:09:39 +0200 Subject: [PATCH 15/18] don't ue prev value but current text input value --- src/components/Composer/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 8f0051eb552b..4a4ae6ef418e 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -287,12 +287,12 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); - const prevValue = usePrevious(value); const clear = useCallback(() => { - if (!textInput.current || prevValue === undefined) { + if (!textInput.current) { return; } + const currentText = textInput.current.value; textInput.current.clear(); // We need to reset the selection to 0,0 manually after clearing the text input on web @@ -307,8 +307,8 @@ function Composer( onSelectionChange(selectionEvent); setSelection({start: 0, end: 0}); - onClear(prevValue); - }, [onClear, onSelectionChange, prevValue]); + onClear(currentText); + }, [onClear, onSelectionChange]); useImperativeHandle( ref, From 7693d93dab4f599f49eff6622311e557a74a8877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 29 Jul 2024 08:23:09 +0200 Subject: [PATCH 16/18] fix lint --- src/components/Composer/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 4a4ae6ef418e..1ab34654a10d 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -12,7 +12,6 @@ import Text from '@components/Text'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; -import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; From 583f44e4eda7a656c2a6e83830aa8107c3bcbc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 29 Jul 2024 08:57:30 +0200 Subject: [PATCH 17/18] fix(web): use `innerText` instead of value --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 1ab34654a10d..35f805c5ea0f 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -291,7 +291,7 @@ function Composer( return; } - const currentText = textInput.current.value; + const currentText = textInput.current.innerText; textInput.current.clear(); // We need to reset the selection to 0,0 manually after clearing the text input on web From 28bafb42e438cf8f6ee2307e98d77b8698ed4542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 30 Jul 2024 14:46:23 +0200 Subject: [PATCH 18/18] remove todo comment --- .../home/report/ReportActionCompose/ReportActionCompose.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 0c4e3fd3eb1f..5bb1ccddbaa4 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -44,7 +44,6 @@ import ComposerWithSuggestions from './ComposerWithSuggestions'; import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions/ComposerWithSuggestions'; import SendButton from './SendButton'; -// TODO: move this to composer?! type ComposerRef = { blur: () => void; focus: (shouldDelay?: boolean) => void;