From 89c73cfd0caebb4bd782e32476e736ff67615d95 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 28 Oct 2024 16:03:10 +0100 Subject: [PATCH 1/2] Migrate ComposerWithSuggestions to useOnyx --- .../ComposerWithSuggestions.tsx | 182 +++++++----------- 1 file changed, 74 insertions(+), 108 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 9ae2eaa2eaad..a49a09dd5260 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,6 +1,6 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; -import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react'; +import type {ForwardedRef, MutableRefObject, RefObject} from 'react'; import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type { LayoutChangeEvent, @@ -14,7 +14,7 @@ import type { import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import {useFocusedInputHandler} from 'react-native-keyboard-controller'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import {useAnimatedRef, useSharedValue} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; @@ -65,113 +65,95 @@ type SyncSelection = { type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; -type ComposerWithSuggestionsOnyxProps = { - /** The parent report actions for the report */ - parentReportActions: OnyxEntry; +type ComposerWithSuggestionsProps = Partial & { + /** Report ID */ + reportID: string; - /** The modal state */ - modal: OnyxEntry; + /** Callback to focus composer */ + onFocus: () => void; - /** The preferred skin tone of the user */ - preferredSkinTone: number; + /** Callback to blur composer */ + onBlur: (event: NativeSyntheticEvent) => void; - /** Whether the input is focused */ - editFocused: OnyxEntry; -}; - -type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & - Partial & { - /** Report ID */ - reportID: string; - - /** Callback to focus composer */ - onFocus: () => void; - - /** Callback to blur composer */ - onBlur: (event: NativeSyntheticEvent) => void; - - /** Callback when layout of composer changes */ - onLayout?: (event: LayoutChangeEvent) => void; + /** Callback when layout of composer changes */ + onLayout?: (event: LayoutChangeEvent) => void; - /** Callback to update the value of the composer */ - onValueChange: (value: string) => void; + /** 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; + /** Callback when the composer got cleared on the UI thread */ + onCleared?: (text: string) => void; - /** Whether the composer is full size */ - isComposerFullSize: boolean; + /** Whether the composer is full size */ + isComposerFullSize: boolean; - /** Whether the menu is visible */ - isMenuVisible: boolean; + /** Whether the menu is visible */ + isMenuVisible: boolean; - /** The placeholder for the input */ - inputPlaceholder: string; + /** The placeholder for the input */ + inputPlaceholder: string; - /** Function to display a file in a modal */ - displayFileInModal: (file: FileObject) => void; + /** Function to display a file in a modal */ + displayFileInModal: (file: FileObject) => void; - /** Whether the user is blocked from concierge */ - isBlockedFromConcierge: boolean; + /** Whether the user is blocked from concierge */ + isBlockedFromConcierge: boolean; - /** Whether the input is disabled */ - disabled: boolean; + /** Whether the input is disabled */ + disabled: boolean; - /** Whether the full composer is available */ - isFullComposerAvailable: boolean; + /** Whether the full composer is available */ + isFullComposerAvailable: boolean; - /** Function to set whether the full composer is available */ - setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + /** Function to set whether the full composer is available */ + setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; - /** Function to set whether the comment is empty */ - setIsCommentEmpty: (isCommentEmpty: boolean) => void; + /** Function to set whether the comment is empty */ + setIsCommentEmpty: (isCommentEmpty: boolean) => void; - /** Function to handle sending a message */ - handleSendMessage: () => void; + /** Function to handle sending a message */ + handleSendMessage: () => void; - /** Whether the compose input should show */ - shouldShowComposeInput: OnyxEntry; + /** Whether the compose input should show */ + shouldShowComposeInput: OnyxEntry; - /** Function to measure the parent container */ - measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + /** Function to measure the parent container */ + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; - /** Whether the scroll is likely to trigger a layout */ - isScrollLikelyLayoutTriggered: RefObject; + /** Whether the scroll is likely to trigger a layout */ + isScrollLikelyLayoutTriggered: RefObject; - /** Function to raise the scroll is likely layout triggered */ - raiseIsScrollLikelyLayoutTriggered: () => void; + /** Function to raise the scroll is likely layout triggered */ + raiseIsScrollLikelyLayoutTriggered: () => void; - /** The ref to the suggestions */ - suggestionsRef: React.RefObject; + /** The ref to the suggestions */ + suggestionsRef: React.RefObject; - /** The ref to the next modal will open */ - isNextModalWillOpenRef: MutableRefObject; + /** The ref to the next modal will open */ + isNextModalWillOpenRef: MutableRefObject; - /** Whether the edit is focused */ - editFocused: boolean; + /** Wheater chat is empty */ + isEmptyChat?: boolean; - /** Wheater chat is empty */ - isEmptyChat?: boolean; + /** The last report action */ + lastReportAction?: OnyxEntry; - /** The last report action */ - lastReportAction?: OnyxEntry; + /** Whether to include chronos */ + includeChronos?: boolean; - /** Whether to include chronos */ - includeChronos?: boolean; + /** The parent report action ID */ + parentReportActionID?: string; - /** 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; - /** 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; - /** Whether report is from group policy */ - isGroupPolicyReport: boolean; - - /** policy ID of the report */ - policyID: string; - }; + /** policy ID of the report */ + policyID: string; +}; type SwitchToCurrentReportProps = { preexistingReportID: string; @@ -223,13 +205,9 @@ const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); */ function ComposerWithSuggestions( { - // Onyx - modal, - preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, - parentReportActions, - // Props: Report reportID, + parentReportID, includeChronos, isEmptyChat, lastReportAction, @@ -263,7 +241,6 @@ function ComposerWithSuggestions( // Refs suggestionsRef, isNextModalWillOpenRef, - editFocused, // For testing children, @@ -290,6 +267,12 @@ function ComposerWithSuggestions( }); const commentRef = useRef(value); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [modal] = useOnyx(ONYXKEYS.MODAL); + const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {selector: EmojiUtils.getPreferredSkinToneIndex}); + const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {canEvict: false, initWithStoredValues: false}); + const lastTextRef = useRef(value); useEffect(() => { lastTextRef.current = value; @@ -298,8 +281,7 @@ function ComposerWithSuggestions( const {shouldUseNarrowLayout} = useResponsiveLayout(); const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); - const parentReportAction = parentReportActions?.[parentReportActionID ?? '-1']; + const parentReportAction = useMemo(() => parentReportActions?.[parentReportActionID ?? '-1'], [parentReportActionID, parentReportActions]); const shouldAutoFocus = !modal?.isVisible && Modal.areAllModalsHidden() && @@ -653,6 +635,7 @@ function ComposerWithSuggestions( const prevIsModalVisible = usePrevious(modal?.isVisible); const prevIsFocused = usePrevious(isFocused); + useEffect(() => { if (modal?.isVisible && !prevIsModalVisible) { // eslint-disable-next-line react-compiler/react-compiler, no-param-reassign @@ -683,6 +666,7 @@ function ComposerWithSuggestions( updateMultilineInputRange(textInputRef.current, !!shouldAutoFocus); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + useImperativeHandle( ref, () => ({ @@ -836,24 +820,6 @@ function ComposerWithSuggestions( ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; -const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); - -export default withOnyx, ComposerWithSuggestionsOnyxProps>({ - modal: { - key: ONYXKEYS.MODAL, - }, - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - selector: EmojiUtils.getPreferredSkinToneIndex, - }, - editFocused: { - key: ONYXKEYS.INPUT_FOCUSED, - }, - parentReportActions: { - key: ({parentReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, - canEvict: false, - initWithStoredValues: false, - }, -})(memo(ComposerWithSuggestionsWithRef)); +export default memo(forwardRef(ComposerWithSuggestions)); export type {ComposerWithSuggestionsProps, ComposerRef}; From abf3c5cc13b65effd9e5fc7eb4925f244a342f73 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 28 Oct 2024 17:13:56 +0100 Subject: [PATCH 2/2] Add useCallback to fix perf tests --- src/pages/home/ReportScreen.tsx | 7 +++++-- .../ReportActionCompose/ReportActionCompose.tsx | 17 +++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index d141920ac99b..4e91f7ca8403 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -698,6 +698,9 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const lastRoute = usePrevious(route); const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute); + const onComposerFocus = useCallback(() => setIsComposerFocus(true), []); + const onComposerBlur = useCallback(() => setIsComposerFocus(false), []); + // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. // If we have cached reportActions, they will be shown immediately. // We aim to display a loader first, then fetch relevant reportActions, and finally show them. @@ -800,8 +803,8 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro {isCurrentReportLoadedFromOnyx ? ( setIsComposerFocus(true)} - onComposerBlur={() => setIsComposerFocus(false)} + onComposerFocus={onComposerFocus} + onComposerBlur={onComposerBlur} report={report} reportMetadata={reportMetadata} policy={policy} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 14908014ca03..23b059f2fda2 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -393,6 +393,16 @@ function ReportActionCompose({ ], ); + const onValueChange = useCallback( + (value: string) => { + if (value.length === 0 && isComposerFullSize) { + Report.setIsComposerFullSize(reportID, false); + } + validateCommentMaxLength(value, {reportID}); + }, + [isComposerFullSize, reportID, validateCommentMaxLength], + ); + return ( @@ -490,12 +500,7 @@ function ReportActionCompose({ onFocus={onFocus} onBlur={onBlur} measureParentContainer={measureContainer} - onValueChange={(value) => { - if (value.length === 0 && isComposerFullSize) { - Report.setIsComposerFullSize(reportID, false); - } - validateCommentMaxLength(value, {reportID}); - }} + onValueChange={onValueChange} /> {