Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 17 additions & 26 deletions src/components/OptionListContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
import {InteractionManager} from 'react-native';
import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
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';
Expand Down Expand Up @@ -45,8 +42,7 @@ const isEqualPersonalDetail = (prevPersonalDetail: PersonalDetails, personalDeta
prevPersonalDetail?.displayName === personalDetail?.displayName;

function OptionsListContextProvider({children}: OptionsListProviderProps) {
const [areOptionsInitialized, setAreOptionsInitialized] = useState(false);

const areOptionsInitialized = useRef(false);
const [options, setOptions] = useState<OptionList>({
reports: [],
personalDetails: [],
Expand All @@ -71,12 +67,12 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
* This effect is responsible for generating the options list when their data is not yet initialized
*/
useEffect(() => {
if (!areOptionsInitialized || !reports || hasInitialData) {
if (!areOptionsInitialized.current || !reports || hasInitialData) {
return;
}

loadOptions();
}, [reports, personalDetails, hasInitialData, loadOptions, areOptionsInitialized]);
}, [reports, personalDetails, hasInitialData, loadOptions]);

/**
* This effect is responsible for generating the options list when the locale changes
Expand Down Expand Up @@ -106,7 +102,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
* This effect is responsible for updating the options only for changed reports
*/
useEffect(() => {
if (!changedReportsEntries || !areOptionsInitialized) {
if (!changedReportsEntries || !areOptionsInitialized.current) {
return;
}

Expand Down Expand Up @@ -134,10 +130,10 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
reports: Array.from(updatedReportsMap.values()),
};
});
}, [areOptionsInitialized, changedReportsEntries, personalDetails]);
}, [changedReportsEntries, personalDetails]);

useEffect(() => {
if (!changedReportActions || !areOptionsInitialized) {
if (!changedReportActions || !areOptionsInitialized.current) {
return;
}

Expand Down Expand Up @@ -166,14 +162,14 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
reports: Array.from(updatedReportsMap.values()),
};
});
}, [areOptionsInitialized, changedReportActions, personalDetails]);
}, [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) {
if (!areOptionsInitialized.current) {
return;
}

Expand Down Expand Up @@ -237,30 +233,24 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {

const initializeOptions = useCallback(() => {
loadOptions();
if (getPlatform() === CONST.PLATFORM.ANDROID || getPlatform() === CONST.PLATFORM.IOS) {
InteractionManager.runAfterInteractions(() => {
setAreOptionsInitialized(true);
});
return;
}
setAreOptionsInitialized(true);
areOptionsInitialized.current = true;
}, [loadOptions]);

const resetOptions = useCallback(() => {
if (!areOptionsInitialized) {
if (!areOptionsInitialized.current) {
return;
}

setAreOptionsInitialized(false);
areOptionsInitialized.current = false;
setOptions({
reports: [],
personalDetails: [],
});
}, [areOptionsInitialized]);
}, []);

return (
<OptionsListContext.Provider // eslint-disable-next-line react-compiler/react-compiler
value={useMemo(() => ({options, initializeOptions, areOptionsInitialized, resetOptions}), [options, initializeOptions, areOptionsInitialized, resetOptions])}
value={useMemo(() => ({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current, resetOptions}), [options, initializeOptions, resetOptions])}
>
{children}
</OptionsListContext.Provider>
Expand All @@ -273,14 +263,15 @@ 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) {
if (!shouldInitialize || areOptionsInitialized || isLoadingApp) {
return;
}

initializeOptions();
}, [shouldInitialize, initializeOptions, areOptionsInitialized]);
}, [shouldInitialize, initializeOptions, areOptionsInitialized, isLoadingApp]);

return {
initializeOptions,
Expand Down
34 changes: 8 additions & 26 deletions src/components/Search/SearchAutocompleteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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';
Expand Down Expand Up @@ -370,30 +369,21 @@ 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
.filter((status) => status.includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(status))
.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
Expand Down Expand Up @@ -559,10 +549,7 @@ 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}) => {
Expand Down Expand Up @@ -605,14 +592,9 @@ function SearchAutocompleteList(
}, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]);

return (
<>
{isInitialRender && (
<OptionsListSkeletonView
fixedNumItems={4}
shouldStyleAsTable
speed={CONST.TIMING.SKELETON_ANIMATION_SPEED}
/>
)}
// 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 && (
<SelectionList<OptionData | SearchQueryItem>
showLoadingPlaceholder={!areOptionsInitialized}
fixedNumItemsForLoader={4}
Expand Down Expand Up @@ -641,7 +623,7 @@ function SearchAutocompleteList(
shouldSubscribeToArrowKeyEvents={shouldSubscribeToArrowKeyEvents}
disableKeyboardShortcuts={!shouldSubscribeToArrowKeyEvents}
/>
</>
)
);
}

Expand Down
74 changes: 32 additions & 42 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ 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';
Expand Down Expand Up @@ -81,10 +79,8 @@ type SearchRouterProps = {
function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDisplayed}: SearchRouterProps, ref: React.Ref<View>) {
const {translate} = useLocalize();
const styles = useThemeStyles();
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 [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true});

const {shouldUseNarrowLayout} = useResponsiveLayout();
const listRef = useRef<SelectionListHandle>(null);
Expand Down Expand Up @@ -320,7 +316,8 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
});

const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth};
const shouldShowSearchList = areOptionsInitialized && isRecentSearchesDataLoaded;
const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata);

return (
<View
style={[styles.flex1, modalWidth, styles.h100, !shouldUseNarrowLayout && styles.mh85vh]}
Expand All @@ -336,33 +333,33 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
shouldDisplayHelpButton={false}
/>
)}
<>
<SearchInputSelectionWrapper
value={textInputValue}
isFullWidth={shouldUseNarrowLayout}
onSearchQueryChange={onSearchQueryChange}
onSubmit={() => {
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 && (
{isRecentSearchesDataLoaded && (
<>
<SearchInputSelectionWrapper
value={textInputValue}
isFullWidth={shouldUseNarrowLayout}
onSearchQueryChange={onSearchQueryChange}
onSubmit={() => {
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}
/>
<SearchAutocompleteList
autocompleteQueryValue={autocompleteQueryValue || textInputValue}
handleSearch={searchInServer}
Expand All @@ -375,15 +372,8 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
ref={listRef}
textInputRef={textInputRef}
/>
)}
{!shouldShowSearchList && (
<OptionsListSkeletonView
fixedNumItems={4}
shouldStyleAsTable
speed={CONST.TIMING.SKELETON_ANIMATION_SPEED}
/>
)}
</>
</>
)}
</View>
);
}
Expand Down
Loading