diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index f7015fe8caa1..5b3db2bf0e8c 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -1,10 +1,13 @@ -import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import {InteractionManager} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import usePrevious from '@hooks/usePrevious'; +import getPlatform from '@libs/getPlatform'; import {createOptionFromReport, createOptionList, processReport} from '@libs/OptionsListUtils'; import type {OptionList, SearchOption} from '@libs/OptionsListUtils'; import {isSelfDM} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, Report} from '@src/types/onyx'; import {usePersonalDetails} from './OnyxProvider'; @@ -42,7 +45,8 @@ const isEqualPersonalDetail = (prevPersonalDetail: PersonalDetails, personalDeta prevPersonalDetail?.displayName === personalDetail?.displayName; function OptionsListContextProvider({children}: OptionsListProviderProps) { - const areOptionsInitialized = useRef(false); + const [areOptionsInitialized, setAreOptionsInitialized] = useState(false); + const [options, setOptions] = useState({ reports: [], personalDetails: [], @@ -67,12 +71,12 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { * This effect is responsible for generating the options list when their data is not yet initialized */ useEffect(() => { - if (!areOptionsInitialized.current || !reports || hasInitialData) { + if (!areOptionsInitialized || !reports || hasInitialData) { return; } loadOptions(); - }, [reports, personalDetails, hasInitialData, loadOptions]); + }, [reports, personalDetails, hasInitialData, loadOptions, areOptionsInitialized]); /** * This effect is responsible for generating the options list when the locale changes @@ -102,7 +106,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { * This effect is responsible for updating the options only for changed reports */ useEffect(() => { - if (!changedReportsEntries || !areOptionsInitialized.current) { + if (!changedReportsEntries || !areOptionsInitialized) { return; } @@ -130,10 +134,10 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { reports: Array.from(updatedReportsMap.values()), }; }); - }, [changedReportsEntries, personalDetails]); + }, [areOptionsInitialized, changedReportsEntries, personalDetails]); useEffect(() => { - if (!changedReportActions || !areOptionsInitialized.current) { + if (!changedReportActions || !areOptionsInitialized) { return; } @@ -162,14 +166,14 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { reports: Array.from(updatedReportsMap.values()), }; }); - }, [changedReportActions, personalDetails]); + }, [areOptionsInitialized, changedReportActions, personalDetails]); /** * This effect is used to update the options list when personal details change. */ useEffect(() => { // there is no need to update the options if the options are not initialized - if (!areOptionsInitialized.current) { + if (!areOptionsInitialized) { return; } @@ -233,24 +237,30 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) { const initializeOptions = useCallback(() => { loadOptions(); - areOptionsInitialized.current = true; + if (getPlatform() === CONST.PLATFORM.ANDROID || getPlatform() === CONST.PLATFORM.IOS) { + InteractionManager.runAfterInteractions(() => { + setAreOptionsInitialized(true); + }); + return; + } + setAreOptionsInitialized(true); }, [loadOptions]); const resetOptions = useCallback(() => { - if (!areOptionsInitialized.current) { + if (!areOptionsInitialized) { return; } - areOptionsInitialized.current = false; + setAreOptionsInitialized(false); setOptions({ reports: [], personalDetails: [], }); - }, []); + }, [areOptionsInitialized]); return ( ({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current, resetOptions}), [options, initializeOptions, resetOptions])} + value={useMemo(() => ({options, initializeOptions, areOptionsInitialized, resetOptions}), [options, initializeOptions, areOptionsInitialized, resetOptions])} > {children} @@ -263,15 +273,14 @@ const useOptionsListContext = () => useContext(OptionsListContext); const useOptionsList = (options?: {shouldInitialize: boolean}) => { const {shouldInitialize = true} = options ?? {}; const {initializeOptions, options: optionsList, areOptionsInitialized, resetOptions} = useOptionsListContext(); - const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false}); useEffect(() => { - if (!shouldInitialize || areOptionsInitialized || isLoadingApp) { + if (!shouldInitialize || areOptionsInitialized) { return; } initializeOptions(); - }, [shouldInitialize, initializeOptions, areOptionsInitialized, isLoadingApp]); + }, [shouldInitialize, initializeOptions, areOptionsInitialized]); return { initializeOptions, diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index cf9be5102e77..9d7091646b97 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -5,6 +5,7 @@ import {useOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; +import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import SelectionList from '@components/SelectionList'; import type {SearchQueryItem, SearchQueryListItemProps} from '@components/SelectionList/Search/SearchQueryListItem'; @@ -369,13 +370,19 @@ function SearchAutocompleteList( .filter((type) => type.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(type.toLowerCase())) .sort(); - return filteredTypes.map((type) => ({filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TYPE, text: type})); + return filteredTypes.map((type) => ({ + filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TYPE, + text: type, + })); } case CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY: { const filteredGroupBy = groupByAutocompleteList.filter( (groupByValue) => groupByValue.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(groupByValue.toLowerCase()), ); - return filteredGroupBy.map((groupByValue) => ({filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.GROUP_BY, text: groupByValue})); + return filteredGroupBy.map((groupByValue) => ({ + filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.GROUP_BY, + text: groupByValue, + })); } case CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS: { const filteredStatuses = statusAutocompleteList @@ -383,7 +390,10 @@ function SearchAutocompleteList( .sort() .slice(0, 10); - return filteredStatuses.map((status) => ({filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.STATUS, text: status})); + return filteredStatuses.map((status) => ({ + filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.STATUS, + text: status, + })); } case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE: { const filteredExpenseTypes = expenseTypes @@ -549,7 +559,10 @@ function SearchAutocompleteList( text: StringUtils.lineBreaksToSpaces(item.text), wrapperStyle: [styles.pr3, styles.pl3], })); - sections.push({title: autocompleteQueryValue.trim() === '' ? translate('search.recentChats') : undefined, data: styledRecentReports}); + sections.push({ + title: autocompleteQueryValue.trim() === '' ? translate('search.recentChats') : undefined, + data: styledRecentReports, + }); if (autocompleteSuggestions.length > 0) { const autocompleteData = autocompleteSuggestions.map(({filterKey, text, autocompleteID, mapKey}) => { @@ -592,9 +605,14 @@ function SearchAutocompleteList( }, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]); return ( - // On page refresh, when the list is rendered before options are initialized the auto-focusing on initiallyFocusedOptionKey - // will fail because the list will be empty on first render so we only render after options are initialized. - areOptionsInitialized && ( + <> + {isInitialRender && ( + + )} showLoadingPlaceholder={!areOptionsInitialized} fixedNumItemsForLoader={4} @@ -623,7 +641,7 @@ function SearchAutocompleteList( shouldSubscribeToArrowKeyEvents={shouldSubscribeToArrowKeyEvents} disableKeyboardShortcuts={!shouldSubscribeToArrowKeyEvents} /> - ) + ); } diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index debf59b676ff..9c4084ff05f3 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -7,6 +7,8 @@ import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; +import {useOptionsList} from '@components/OptionListContextProvider'; +import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type {GetAdditionalSectionsCallback} from '@components/Search/SearchAutocompleteList'; import SearchAutocompleteList from '@components/Search/SearchAutocompleteList'; @@ -79,8 +81,10 @@ type SearchRouterProps = { function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDisplayed}: SearchRouterProps, ref: React.Ref) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [, recentSearchesMetadata] = useOnyx(ONYXKEYS.RECENT_SEARCHES, {canBeMissing: true}); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); + const [, recentSearchesMetadata] = useOnyx(ONYXKEYS.RECENT_SEARCHES, {canBeMissing: true}); + const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); + const {areOptionsInitialized} = useOptionsList(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const listRef = useRef(null); @@ -316,8 +320,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla }); const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth}; - const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata); - + const shouldShowSearchList = areOptionsInitialized && isRecentSearchesDataLoaded; return ( )} - {isRecentSearchesDataLoaded && ( - <> - { - const focusedOption = listRef.current?.getFocusedOption(); - - if (!focusedOption) { - submitSearch(textInputValue); - return; - } - - onListItemPress(focusedOption); - }} - caretHidden={shouldHideInputCaret} - autocompleteListRef={listRef} - shouldShowOfflineMessage - wrapperStyle={{...styles.border, ...styles.alignItemsCenter}} - outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]} - wrapperFocusedStyle={styles.borderColorFocus} - isSearchingForReports={isSearchingForReports} - selection={selection} - substitutionMap={autocompleteSubstitutions} - ref={textInputRef} - /> + <> + { + const focusedOption = listRef.current?.getFocusedOption(); + + if (!focusedOption) { + submitSearch(textInputValue); + return; + } + + onListItemPress(focusedOption); + }} + caretHidden={shouldHideInputCaret} + autocompleteListRef={listRef} + shouldShowOfflineMessage + wrapperStyle={{...styles.border, ...styles.alignItemsCenter}} + outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]} + wrapperFocusedStyle={styles.borderColorFocus} + isSearchingForReports={isSearchingForReports} + selection={selection} + substitutionMap={autocompleteSubstitutions} + ref={textInputRef} + /> + {shouldShowSearchList && ( - - )} + )} + {!shouldShowSearchList && ( + + )} + ); }