diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index da2fc1a9b29f..9b886aa49c1d 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,14 +1,13 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import mimeDb from 'mime-db'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native'; import {StyleSheet} from 'react-native'; import type {FileObject} from '@components/AttachmentModal'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useKeyboardState from '@hooks/useKeyboardState'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -38,7 +37,6 @@ function Composer( selection, value, isGroupPolicyReport = false, - showSoftInputOnFocus, ...props }: ComposerProps, ref: ForwardedRef, @@ -50,11 +48,8 @@ function Composer( const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const [contextMenuHidden, setContextMenuHidden] = useState(!showSoftInputOnFocus); const {inputCallbackRef, inputRef: autoFocusInputRef} = useAutoFocusInput(); - const keyboardState = useKeyboardState(); - const isKeyboardShown = keyboardState?.isKeyboardShown ?? false; useEffect(() => { if (autoFocus === !!autoFocusInputRef.current) { @@ -114,13 +109,6 @@ function Composer( const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]); - useEffect(() => { - if (!showSoftInputOnFocus || !isKeyboardShown) { - return; - } - setContextMenuHidden(false); - }, [showSoftInputOnFocus, isKeyboardShown]); - return ( ); } diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 55d14a116c3e..72116a346c00 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -73,7 +73,6 @@ function Composer( isComposerFullSize = false, shouldContainScroll = true, isGroupPolicyReport = false, - showSoftInputOnFocus = true, ...props }: ComposerProps, ref: ForwardedRef, @@ -389,7 +388,6 @@ function Composer( value={value} defaultValue={defaultValue} autoFocus={autoFocus} - inputMode={!showSoftInputOnFocus ? 'none' : 'text'} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 616bc102c0f1..41138970c547 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -77,8 +77,6 @@ type ComposerProps = Omit & { /** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */ isGroupPolicyReport?: boolean; - - showSoftInputOnFocus?: boolean; }; export type {TextSelection, ComposerProps, CustomSelectionChangeEvent}; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index c9287f871aaa..69c4402e959b 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -238,6 +238,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; + const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]); const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const indexOfLinkedMessage = useMemo( (): number => reportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)), @@ -801,6 +802,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro policy={policy} pendingAction={reportPendingAction} isComposerFullSize={!!isComposerFullSize} + isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} workspaceTooltip={workspaceTooltip} /> diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 752d35a6cbe0..e63bd952b4ab 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -29,6 +29,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; 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'; @@ -39,6 +40,7 @@ import getPlatform from '@libs/getPlatform'; import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; @@ -64,6 +66,9 @@ type SyncSelection = { type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsOnyxProps = { + /** The parent report actions for the report */ + parentReportActions: OnyxEntry; + /** The modal state */ modal: OnyxEntry; @@ -145,12 +150,22 @@ type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & /** Whether the edit is focused */ editFocused: boolean; + /** Wheater chat is empty */ + isEmptyChat?: boolean; + /** The last report action */ lastReportAction?: OnyxEntry; /** Whether to include chronos */ includeChronos?: boolean; + /** The parent report action ID */ + parentReportActionID?: string; + + /** The parent report ID */ + // eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC + parentReportID: string | undefined; + /** Whether report is from group policy */ isGroupPolicyReport: boolean; @@ -196,6 +211,10 @@ const debouncedBroadcastUserIsTyping = lodashDebounce( const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); +// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will +// prevent auto focus on existing chat for mobile device +const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); + /** * This component holds the value and selection state. * If a component really needs access to these state values it should be put here. @@ -207,11 +226,14 @@ function ComposerWithSuggestions( // Onyx modal, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + parentReportActions, // Props: Report reportID, includeChronos, + isEmptyChat, lastReportAction, + parentReportActionID, isGroupPolicyReport, policyID, @@ -276,13 +298,17 @@ function ComposerWithSuggestions( const {shouldUseNarrowLayout} = useResponsiveLayout(); const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const shouldAutoFocus = !modal?.isVisible && shouldShowComposeInput && Modal.areAllModalsHidden() && isFocused; + const parentReportAction = parentReportActions?.[parentReportActionID ?? '-1']; + const shouldAutoFocus = + !modal?.isVisible && + Modal.areAllModalsHidden() && + isFocused && + (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && + shouldShowComposeInput; const valueRef = useRef(value); valueRef.current = value; - const [showSoftInputOnFocus, setShowSoftInputOnFocus] = useState(false); - const [selection, setSelection] = useState(() => ({start: value.length, end: value.length, positionX: 0, positionY: 0})); const [composerHeight, setComposerHeight] = useState(0); @@ -772,19 +798,6 @@ function ComposerWithSuggestions( shouldCalculateCaretPosition onLayout={onLayout} onScroll={hideSuggestionMenu} - showSoftInputOnFocus={showSoftInputOnFocus} - onTouchStart={() => { - if (showSoftInputOnFocus) { - return; - } - if (Browser.isMobileSafari()) { - setTimeout(() => { - setShowSoftInputOnFocus(true); - }, CONST.ANIMATED_TRANSITION); - return; - } - setShowSoftInputOnFocus(true); - }} shouldContainScroll={Browser.isMobileSafari()} isGroupPolicyReport={isGroupPolicyReport} /> @@ -835,6 +848,11 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, + canEvict: false, + initWithStoredValues: false, + }, })(memo(ComposerWithSuggestionsWithRef)); export type {ComposerWithSuggestionsProps, ComposerRef}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 3f1e5431f3ef..e4b0eef6ca52 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -28,6 +28,7 @@ import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import {getDraftComment} from '@libs/DraftCommentUtils'; import getModalState from '@libs/getModalState'; @@ -63,7 +64,7 @@ type SuggestionsRef = { }; type ReportActionComposeProps = WithCurrentUserPersonalDetailsProps & - Pick & { + Pick & { /** A method to call when the form is submitted */ onSubmit: (newComment: string) => void; @@ -89,6 +90,10 @@ type ReportActionComposeProps = WithCurrentUserPersonalDetailsProps & shouldShowEducationalTooltip?: boolean; }; +// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will +// prevent auto focus on existing chat for mobile device +const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); + const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); // eslint-disable-next-line import/no-mutable-exports @@ -103,6 +108,7 @@ function ReportActionCompose({ report, reportID, isReportReadyForDisplay = true, + isEmptyChat, lastReportAction, shouldShowEducationalTooltip, onComposerFocus, @@ -123,7 +129,7 @@ function ReportActionCompose({ */ const [isFocused, setIsFocused] = useState(() => { const initialModalState = getModalState(); - return shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; + return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; }); const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize); const [shouldHideEducationalTooltip, setShouldHideEducationalTooltip] = useState(false); @@ -459,8 +465,11 @@ function ReportActionCompose({ raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered} reportID={reportID} policyID={report?.policyID ?? '-1'} + parentReportID={report?.parentReportID} + parentReportActionID={report?.parentReportActionID} includeChronos={ReportUtils.chatIncludesChronos(report)} isGroupPolicyReport={isGroupPolicyReport} + isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} isMenuVisible={isMenuVisible} inputPlaceholder={inputPlaceholder} diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 90746efa3b68..7c4ec786b633 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -48,6 +48,9 @@ type ReportFooterProps = { /** Whether to show educational tooltip in workspace chat for first-time user */ workspaceTooltip: OnyxEntry; + /** Whether the chat is empty */ + isEmptyChat?: boolean; + /** The pending action when we are adding a chat */ pendingAction?: PendingAction; @@ -70,6 +73,7 @@ function ReportFooter({ report = {reportID: '-1'}, reportMetadata, policy, + isEmptyChat = true, isReportReadyForDisplay = true, isComposerFullSize = false, workspaceTooltip, @@ -220,6 +224,7 @@ function ReportFooter({ onComposerBlur={onComposerBlur} reportID={report.reportID} report={report} + isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} pendingAction={pendingAction} isComposerFullSize={isComposerFullSize} @@ -241,6 +246,7 @@ export default memo( lodashIsEqual(prevProps.report, nextProps.report) && prevProps.pendingAction === nextProps.pendingAction && prevProps.isComposerFullSize === nextProps.isComposerFullSize && + prevProps.isEmptyChat === nextProps.isEmptyChat && prevProps.lastReportAction === nextProps.lastReportAction && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay && prevProps.workspaceTooltip?.shouldShow === nextProps.workspaceTooltip?.shouldShow &&