diff --git a/src/CONST.ts b/src/CONST.ts index a983a18e3d6a..6517ece4276d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4767,6 +4767,7 @@ const CONST = { DISTANCE: 'distance', }, + SEARCH_RESULTS_PAGE_SIZE: 50, SEARCH_BOTTOM_TAB_URL: '/Search_Bottom_Tab', SEARCH_DATA_TYPES: { diff --git a/src/components/Search.tsx b/src/components/Search.tsx index a8e469da7d99..27e87017bfee 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -9,6 +9,7 @@ import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import EmptySearchView from '@pages/Search/EmptySearchView'; import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -35,14 +36,15 @@ function Search({query, policyIDs}: SearchProps) { return; } - SearchActions.search(hash, query, policyIDs); + SearchActions.search(hash, query, 0, policyIDs); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hash, isOffline]); - const isLoading = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined; - const shouldShowEmptyState = !isLoading && isEmptyObject(searchResults?.data); + const isLoadingInitialItems = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined; + const isLoadingMoreItems = !isLoadingInitialItems && searchResults?.search?.isLoading; + const shouldShowEmptyState = !isLoadingInitialItems && isEmptyObject(searchResults?.data); - if (isLoading) { + if (isLoadingInitialItems) { return ; } @@ -58,6 +60,14 @@ function Search({query, policyIDs}: SearchProps) { Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(query, reportID)); }; + const fetchMoreResults = () => { + if (!searchResults?.search?.hasMoreResults || isLoadingInitialItems || isLoadingMoreItems) { + return; + } + const currentOffset = searchResults?.search?.offset ?? 0; + SearchActions.search(hash, query, currentOffset + CONST.SEARCH_RESULTS_PAGE_SIZE); + }; + const type = SearchUtils.getSearchType(searchResults?.search); if (type === undefined) { @@ -80,6 +90,16 @@ function Search({query, policyIDs}: SearchProps) { listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} containerStyle={[styles.pv0]} showScrollIndicator={false} + onEndReachedThreshold={0.75} + onEndReached={fetchMoreResults} + listFooterContent={ + isLoadingMoreItems ? ( + + ) : undefined + } /> ); } diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index cb8d06097633..77b296740f2c 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -76,6 +76,8 @@ function BaseSelectionList( sectionTitleStyles, textInputAutoFocus = true, shouldTextInputInterceptSwipe = false, + onEndReached = () => {}, + onEndReachedThreshold, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -618,6 +620,8 @@ function BaseSelectionList( onLayout={onSectionListLayout} style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0} ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} + onEndReached={onEndReached} + onEndReachedThreshold={onEndReachedThreshold} /> {children} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 79e47e4aa4d7..0a4b0532b581 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -365,6 +365,17 @@ type BaseSelectionListProps = Partial & { * When false, the list will render immediately and scroll to the bottom which works great for small lists. */ shouldHideListOnInitialRender?: boolean; + + /** Called once when the scroll position gets within onEndReachedThreshold of the rendered content. */ + onEndReached?: () => void; + + /** + * How far from the end (in units of visible length of the list) the bottom edge of the + * list must be from the end of the content to trigger the `onEndReached` callback. + * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is + * within half the visible length of the list. + */ + onEndReachedThreshold?: number; } & TRightHandSideComponent; type SelectionListHandle = { diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx index 00854471f2e3..5c46dbdddbfc 100644 --- a/src/components/Skeletons/ItemListSkeletonView.tsx +++ b/src/components/Skeletons/ItemListSkeletonView.tsx @@ -8,13 +8,14 @@ import CONST from '@src/CONST'; type ListItemSkeletonProps = { shouldAnimate?: boolean; renderSkeletonItem: (args: {itemIndex: number}) => React.ReactNode; + fixedNumItems?: number; }; -function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem}: ListItemSkeletonProps) { +function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNumItems}: ListItemSkeletonProps) { const theme = useTheme(); const themeStyles = useThemeStyles(); - const [numItems, setNumItems] = useState(0); + const [numItems, setNumItems] = useState(fixedNumItems ?? 0); const skeletonViewItems = useMemo(() => { const items = []; for (let i = 0; i < numItems; i++) { @@ -38,6 +39,10 @@ function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem}: ListIt { + if (fixedNumItems) { + return; + } + const newNumItems = Math.ceil(event.nativeEvent.layout.height / CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT); if (newNumItems === numItems) { return; diff --git a/src/components/Skeletons/TableListItemSkeleton.tsx b/src/components/Skeletons/TableListItemSkeleton.tsx index d24268521100..4b2c214921d0 100644 --- a/src/components/Skeletons/TableListItemSkeleton.tsx +++ b/src/components/Skeletons/TableListItemSkeleton.tsx @@ -4,16 +4,18 @@ import ItemListSkeletonView from './ItemListSkeletonView'; type TableListItemSkeletonProps = { shouldAnimate?: boolean; + fixedNumItems?: number; }; const barHeight = '10'; const shortBarWidth = '40'; const longBarWidth = '120'; -function TableListItemSkeleton({shouldAnimate = true}: TableListItemSkeletonProps) { +function TableListItemSkeleton({shouldAnimate = true, fixedNumItems}: TableListItemSkeletonProps) { return ( ( <>