11import { useIsFocused } from '@react-navigation/native' ;
2+ import { FlashList } from '@shopify/flash-list' ;
3+ import type { FlashListProps , ViewToken } from '@shopify/flash-list' ;
24import React , { forwardRef , useCallback , useEffect , useImperativeHandle , useRef , useState } from 'react' ;
35import type { ForwardedRef } from 'react' ;
46import { View } from 'react-native' ;
5- import type { FlatList , ListRenderItemInfo , NativeSyntheticEvent , StyleProp , ViewStyle , ViewToken } from 'react-native' ;
7+ import type { NativeSyntheticEvent , StyleProp , ViewStyle } from 'react-native' ;
68import { useOnyx } from 'react-native-onyx' ;
79import Animated from 'react-native-reanimated' ;
8- import type { FlatListPropsWithLayout } from 'react-native-reanimated' ;
910import Checkbox from '@components/Checkbox' ;
1011import * as Expensicons from '@components/Icon/Expensicons' ;
1112import MenuItem from '@components/MenuItem' ;
@@ -28,9 +29,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
2829import { turnOffMobileSelectionMode , turnOnMobileSelectionMode } from '@libs/actions/MobileSelectionMode' ;
2930import { isMobileChrome } from '@libs/Browser' ;
3031import { addKeyDownPressListener , removeKeyDownPressListener } from '@libs/KeyboardShortcut/KeyDownPressListener' ;
32+ import { isReportActionListItemType , isReportListItemType , isTransactionListItemType } from '@libs/SearchUIUtils' ;
3133import variables from '@styles/variables' ;
3234import CONST from '@src/CONST' ;
3335import ONYXKEYS from '@src/ONYXKEYS' ;
36+ import ITEM_HEIGHTS from './itemHeights' ;
3437
3538type SearchListItem = TransactionListItemType | ReportListItemType | ReportActionListItemType | TaskListItemType ;
3639type SearchListItemComponentType = typeof TransactionListItem | typeof ChatListItem | typeof ReportListItem | typeof TaskListItem ;
@@ -40,7 +43,7 @@ type SearchListHandle = {
4043 scrollToIndex : ( index : number , animated ?: boolean ) => void ;
4144} ;
4245
43- type SearchListProps = Pick < FlatListPropsWithLayout < SearchListItem > , 'onScroll' | 'contentContainerStyle' | 'onEndReached' | 'onEndReachedThreshold' | 'ListFooterComponent' > & {
46+ type SearchListProps = Pick < FlashListProps < SearchListItem > , 'onScroll' | 'contentContainerStyle' | 'onEndReached' | 'onEndReachedThreshold' | 'ListFooterComponent' | 'estimatedItemSize '> & {
4447 data : SearchListItem [ ] ;
4548
4649 /** Default renderer for every item in the list */
@@ -72,6 +75,9 @@ type SearchListProps = Pick<FlatListPropsWithLayout<SearchListItem>, 'onScroll'
7275 /** The hash of the queryJSON */
7376 queryJSONHash : number ;
7477
78+ /** The type of the queryJSON */
79+ queryJSONType : string ;
80+
7581 /** Whether to group the list by reports */
7682 shouldGroupByReports ?: boolean ;
7783
@@ -82,7 +88,8 @@ type SearchListProps = Pick<FlatListPropsWithLayout<SearchListItem>, 'onScroll'
8288 onLayout ?: ( ) => void ;
8389} ;
8490
85- const onScrollToIndexFailed = ( ) => { } ;
91+ const AnimatedFlashList = Animated . createAnimatedComponent < FlashListProps < SearchListItem > > ( FlashList ) ;
92+ const keyExtractor = ( item : SearchListItem , index : number ) => item . keyForList ?? `${ index } ` ;
8693
8794function SearchList (
8895 {
@@ -104,7 +111,9 @@ function SearchList(
104111 queryJSONHash,
105112 shouldGroupByReports,
106113 onViewableItemsChanged,
114+ estimatedItemSize = ITEM_HEIGHTS . NARROW_WITHOUT_DRAWER . STANDARD ,
107115 onLayout,
116+ queryJSONType,
108117 } : SearchListProps ,
109118 ref : ForwardedRef < SearchListHandle > ,
110119) {
@@ -115,7 +124,7 @@ function SearchList(
115124 } , 0 ) ;
116125 const { translate} = useLocalize ( ) ;
117126 const isFocused = useIsFocused ( ) ;
118- const listRef = useRef < FlatList < SearchListItem > > ( null ) ;
127+ const listRef = useRef < FlashList < SearchListItem > > ( null ) ;
119128 const hasKeyBeenPressed = useRef ( false ) ;
120129 const [ itemsToHighlight , setItemsToHighlight ] = useState < Set < string > | null > ( null ) ;
121130 const itemFocusTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
@@ -124,7 +133,7 @@ function SearchList(
124133 // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here because there is a race condition that causes shouldUseNarrowLayout to change indefinitely in this component
125134 // See https://github.com/Expensify/App/issues/48675 for more details
126135 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
127- const { isSmallScreenWidth} = useResponsiveLayout ( ) ;
136+ const { isSmallScreenWidth, isLargeScreenWidth , shouldUseNarrowLayout } = useResponsiveLayout ( ) ;
128137
129138 const [ isModalVisible , setIsModalVisible ] = useState ( false ) ;
130139 const { selectionMode} = useMobileSelectionMode ( ) ;
@@ -299,8 +308,77 @@ function SearchList(
299308
300309 useImperativeHandle ( ref , ( ) => ( { scrollAndHighlightItem, scrollToIndex} ) , [ scrollAndHighlightItem , scrollToIndex ] ) ;
301310
311+ const getItemHeight = useCallback (
312+ ( item : SearchListItem ) : number => {
313+ try {
314+ const reportListItem = item as ReportListItemType ;
315+ const transactionListItem = item as TransactionListItemType ;
316+ const reportActionListItem = item as ReportActionListItemType ;
317+
318+ const isTransaction = isTransactionListItemType ( transactionListItem ) ;
319+ const isReportAction = isReportActionListItemType ( reportActionListItem ) ;
320+
321+ if ( isTransaction || isReportAction ) {
322+ if ( queryJSONType === CONST . SEARCH . DATA_TYPES . CHAT ) {
323+ return reportListItem ?. childReportID ? variables . searchListItemHeightChat : variables . searchListItemHeightChatCompact ;
324+ }
325+ const itemAction = transactionListItem ?. action ;
326+ // VIEW is the only action type that should be compact
327+ const isItemActionView = isTransaction && itemAction === CONST . SEARCH . ACTION_TYPES . VIEW ;
328+
329+ // Determine which layout to use based on screen size and drawer state
330+ let heightConstants ;
331+
332+ if ( shouldUseNarrowLayout ) {
333+ // For narrow screens without drawer (mobile or collapsed desktop)
334+ heightConstants = isItemActionView ? ITEM_HEIGHTS . NARROW_WITHOUT_DRAWER . STANDARD : ITEM_HEIGHTS . NARROW_WITHOUT_DRAWER . WITH_BUTTON ;
335+ } else if ( ! isLargeScreenWidth ) {
336+ // For narrow screens with drawer
337+ heightConstants = isItemActionView ? ITEM_HEIGHTS . NARROW_WITH_DRAWER . STANDARD : ITEM_HEIGHTS . NARROW_WITH_DRAWER . WITH_BUTTON ;
338+ } else {
339+ // For wide screens (desktop)
340+ heightConstants = ITEM_HEIGHTS . WIDE . STANDARD ;
341+ }
342+
343+ return heightConstants ;
344+ }
345+ if ( isReportListItemType ( reportListItem ) ) {
346+ if ( ! reportListItem . transactions || reportListItem . transactions . length === 0 ) {
347+ return Math . max ( ITEM_HEIGHTS . HEADER , 1 ) ;
348+ }
349+ const baseReportItemHeight = isLargeScreenWidth
350+ ? variables . searchOptionRowMargin + variables . searchOptionRowBaseHeight + variables . searchOptionRowLargeFooterHeight
351+ : variables . searchOptionRowMargin + variables . searchOptionRowBaseHeight + variables . searchOptionRowSmallFooterHeight ;
352+ const transactionHeight = variables . searchOptionRowTransactionHeight ;
353+ const calculatedHeight =
354+ baseReportItemHeight + reportListItem . transactions . length * transactionHeight + variables . optionRowListItemPadding + variables . searchOptionRowMargin ;
355+ return Math . max ( calculatedHeight , ITEM_HEIGHTS . HEADER , 1 ) ;
356+ }
357+
358+ return isLargeScreenWidth ? variables . searchListItemHeightLargeScreen : variables . searchListItemHeightSmallScreen ;
359+ } catch ( error ) {
360+ console . error ( 'SearchList: Error calculating item height, returning estimated size.' , error , item ) ;
361+ return estimatedItemSize ;
362+ }
363+ } ,
364+ [ isLargeScreenWidth , estimatedItemSize , shouldUseNarrowLayout , queryJSONType ] ,
365+ ) ;
366+
367+ const overrideItemLayout = useCallback (
368+ ( layout : { span ?: number ; size ?: number } , item : SearchListItem ) => {
369+ const height = getItemHeight ( item ) ;
370+ if ( ! layout ) {
371+ return ;
372+ }
373+ // eslint-disable-next-line no-param-reassign
374+ layout . size = height > 0 ? height : estimatedItemSize ;
375+ } ,
376+ [ getItemHeight , estimatedItemSize ] ,
377+ ) ;
378+
302379 const renderItem = useCallback (
303- ( { item, index} : ListRenderItemInfo < SearchListItem > ) => {
380+ // eslint-disable-next-line react/no-unused-prop-types
381+ ( { item, index} : { item : SearchListItem ; index : number } ) => {
304382 const isItemFocused = focusedIndex === index ;
305383 const isItemHighlighted = ! ! itemsToHighlight ?. has ( item . keyForList ?? '' ) ;
306384
@@ -386,22 +464,23 @@ function SearchList(
386464 ) }
387465 </ View >
388466 ) }
389-
390- < Animated . FlatList
467+ < AnimatedFlashList
468+ ref = { listRef }
391469 data = { data }
392470 renderItem = { renderItem }
393- keyExtractor = { ( item , index ) => item . keyForList ?? ` ${ index } ` }
471+ keyExtractor = { keyExtractor }
394472 onScroll = { onScroll }
395473 contentContainerStyle = { contentContainerStyle }
396474 showsVerticalScrollIndicator = { false }
397- ref = { listRef }
398- extraData = { focusedIndex }
475+ estimatedItemSize = { estimatedItemSize }
476+ overrideItemLayout = { overrideItemLayout }
399477 onEndReached = { onEndReached }
400478 onEndReachedThreshold = { onEndReachedThreshold }
401479 ListFooterComponent = { ListFooterComponent }
480+ drawDistance = { 1000 }
481+ extraData = { focusedIndex }
402482 removeClippedSubviews
403483 onViewableItemsChanged = { onViewableItemsChanged }
404- onScrollToIndexFailed = { onScrollToIndexFailed }
405484 onLayout = { onLayout }
406485 />
407486 < Modal
0 commit comments