Skip to content

Commit 5cae141

Browse files
committed
Merge branch 'main' of github.com:Expensify/App into jsenyitko-suggested-search
2 parents 5853b82 + be29583 commit 5cae141

5 files changed

Lines changed: 150 additions & 16 deletions

File tree

src/components/Search/SearchList.tsx

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {useIsFocused} from '@react-navigation/native';
2+
import {FlashList} from '@shopify/flash-list';
3+
import type {FlashListProps, ViewToken} from '@shopify/flash-list';
24
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
35
import type {ForwardedRef} from 'react';
46
import {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';
68
import {useOnyx} from 'react-native-onyx';
79
import Animated from 'react-native-reanimated';
8-
import type {FlatListPropsWithLayout} from 'react-native-reanimated';
910
import Checkbox from '@components/Checkbox';
1011
import * as Expensicons from '@components/Icon/Expensicons';
1112
import MenuItem from '@components/MenuItem';
@@ -28,9 +29,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
2829
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
2930
import {isMobileChrome} from '@libs/Browser';
3031
import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener';
32+
import {isReportActionListItemType, isReportListItemType, isTransactionListItemType} from '@libs/SearchUIUtils';
3133
import variables from '@styles/variables';
3234
import CONST from '@src/CONST';
3335
import ONYXKEYS from '@src/ONYXKEYS';
36+
import ITEM_HEIGHTS from './itemHeights';
3437

3538
type SearchListItem = TransactionListItemType | ReportListItemType | ReportActionListItemType | TaskListItemType;
3639
type 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

8794
function 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

src/components/Search/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useIsFocused, useNavigation} from '@react-navigation/native';
2+
import type {ContentStyle} from '@shopify/flash-list';
23
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
3-
import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle, ViewToken} from 'react-native';
4+
import type {NativeScrollEvent, NativeSyntheticEvent, ViewToken} from 'react-native';
45
import {View} from 'react-native';
56
import {useOnyx} from 'react-native-onyx';
67
import FullPageErrorView from '@components/BlockingViews/FullPageErrorView';
@@ -55,7 +56,7 @@ import type {SearchColumnType, SearchParams, SearchQueryJSON, SelectedTransactio
5556
type SearchProps = {
5657
queryJSON: SearchQueryJSON;
5758
onSearchListScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
58-
contentContainerStyle?: StyleProp<ViewStyle>;
59+
contentContainerStyle?: ContentStyle;
5960
currentSearchResults?: SearchResults;
6061
lastNonEmptySearchResults?: SearchResults;
6162
handleSearch: (value: SearchParams) => void;
@@ -596,7 +597,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
596597
/>
597598
)
598599
}
599-
contentContainerStyle={[contentContainerStyle, styles.pb3]}
600+
contentContainerStyle={{...contentContainerStyle, ...styles.pb3}}
600601
containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]}
601602
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
602603
shouldGroupByReports={shouldGroupByReports}
@@ -612,6 +613,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
612613
) : undefined
613614
}
614615
queryJSONHash={hash}
616+
queryJSONType={type}
615617
onViewableItemsChanged={onViewableItemsChanged}
616618
onLayout={() => handleSelectionListScroll(sortedSelectedData, searchListRef.current)}
617619
/>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import variables from '@styles/variables';
2+
3+
const ITEM_HEIGHTS = {
4+
// Constants for wide screen layout
5+
WIDE: {
6+
STANDARD: variables.optionRowWideItemHeight + variables.optionRowListItemPadding,
7+
},
8+
9+
// Constants for narrow screen with drawer
10+
NARROW_WITH_DRAWER: {
11+
STANDARD: variables.optionRowNarrowWithDrawerItemHeight + variables.optionRowListItemPadding,
12+
WITH_BUTTON: variables.optionRowNarrowWithDrawerItemHeightWithButton + variables.optionRowListItemPadding,
13+
},
14+
15+
// Constants for narrow screen without drawer (mobile-like)
16+
NARROW_WITHOUT_DRAWER: {
17+
STANDARD: variables.optionRowNarrowWithoutDrawerItemHeight + variables.optionRowListItemPadding,
18+
WITH_BUTTON: variables.optionRowNarrowWithoutDrawerItemHeightWithButton + variables.optionRowListItemPadding,
19+
},
20+
21+
HEADER: variables.optionRowSearchHeaderHeight,
22+
} as const;
23+
24+
export default ITEM_HEIGHTS;

src/styles/variables.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,30 @@ export default {
318318
w96: 96,
319319
w184: 184,
320320
w191: 191,
321+
322+
// Transaction item row heights based on layout types
323+
// Wide screen (desktop) layout
324+
optionRowWideItemHeight: 64,
325+
326+
// Narrow screen with drawer layout
327+
optionRowNarrowWithDrawerItemHeight: 96,
328+
optionRowNarrowWithDrawerItemHeightWithButton: 104,
329+
330+
// Narrow screen without drawer (mobile-like) layout
331+
optionRowNarrowWithoutDrawerItemHeight: 92,
332+
optionRowNarrowWithoutDrawerItemHeightWithButton: 104,
333+
334+
optionRowListItemPadding: 8,
335+
optionRowSearchHeaderHeight: 54,
336+
337+
// SearchList item heights
338+
searchListItemHeightLargeScreen: 72,
339+
searchListItemHeightSmallScreen: 96,
340+
searchListItemHeightChat: 351,
341+
searchListItemHeightChatCompact: 105,
342+
searchOptionRowTransactionHeight: 52,
343+
searchOptionRowBaseHeight: 52,
344+
searchOptionRowSmallFooterHeight: 28,
345+
searchOptionRowLargeFooterHeight: 17,
346+
searchOptionRowMargin: 6,
321347
} as const;

src/types/onyx/SearchResults.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ type SearchReport = {
179179

180180
/** The policy name to use for an archived report */
181181
oldPolicyName?: string;
182+
183+
/** The ID of the chat report associated with this report item, if any */
184+
childReportID?: string;
182185
};
183186

184187
/** Model of report action search result */

0 commit comments

Comments
 (0)