diff --git a/src/components/Search/SearchList/BaseSearchList/index.native.tsx b/src/components/Search/SearchList/BaseSearchList/index.native.tsx index 792fc0dfc0c4a..7c37713b6117a 100644 --- a/src/components/Search/SearchList/BaseSearchList/index.native.tsx +++ b/src/components/Search/SearchList/BaseSearchList/index.native.tsx @@ -1,10 +1,10 @@ import {FlashList} from '@shopify/flash-list'; import React, {useCallback} from 'react'; import Animated from 'react-native-reanimated'; -import type {SearchListItem} from '@components/SelectionListWithSections/types'; import type BaseSearchListProps from './types'; +import type {SearchListDataItem} from './types'; -const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList); +const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList); function BaseSearchList({ data, @@ -20,7 +20,7 @@ function BaseSearchList({ contentContainerStyle, }: BaseSearchListProps) { const renderItemWithoutKeyboardFocus = useCallback( - ({item, index}: {item: SearchListItem; index: number}) => { + ({item, index}: {item: SearchListDataItem; index: number}) => { return renderItem(item, index, false, undefined); }, [renderItem], diff --git a/src/components/Search/SearchList/BaseSearchList/index.tsx b/src/components/Search/SearchList/BaseSearchList/index.tsx index e345b3a7c8ecf..5cee34479e3d5 100644 --- a/src/components/Search/SearchList/BaseSearchList/index.tsx +++ b/src/components/Search/SearchList/BaseSearchList/index.tsx @@ -3,15 +3,16 @@ import {FlashList} from '@shopify/flash-list'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {NativeSyntheticEvent} from 'react-native'; import Animated from 'react-native-reanimated'; -import type {ExtendedTargetedEvent, SearchListItem} from '@components/SelectionListWithSections/types'; +import type {ExtendedTargetedEvent} from '@components/SelectionListWithSections/types'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import {isMobileChrome} from '@libs/Browser'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import CONST from '@src/CONST'; import type BaseSearchListProps from './types'; +import type {SearchListDataItem} from './types'; -const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList); +const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList); function BaseSearchList({ data, @@ -58,7 +59,7 @@ function BaseSearchList({ }); const renderItemWithKeyboardFocus = useCallback( - ({item, index}: {item: SearchListItem; index: number}) => { + ({item, index}: {item: SearchListDataItem; index: number}) => { const isItemFocused = focusedIndex === index; const onFocus = (event: NativeSyntheticEvent) => { diff --git a/src/components/Search/SearchList/BaseSearchList/types.ts b/src/components/Search/SearchList/BaseSearchList/types.ts index e0fe33cadc937..c242b55410f16 100644 --- a/src/components/Search/SearchList/BaseSearchList/types.ts +++ b/src/components/Search/SearchList/BaseSearchList/types.ts @@ -1,12 +1,14 @@ import type {FlashListProps, FlashListRef} from '@shopify/flash-list'; import type {ForwardedRef} from 'react'; import type {NativeSyntheticEvent} from 'react-native'; -import type {SearchColumnType, SelectedTransactions} from '@components/Search/types'; +import type {SearchColumnType, SearchListItemDescriptor, SelectedTransactions} from '@components/Search/types'; import type {ExtendedTargetedEvent, SearchListItem} from '@components/SelectionListWithSections/types'; import type {Transaction} from '@src/types/onyx'; +export type SearchListDataItem = SearchListItem | SearchListItemDescriptor; + type BaseSearchListProps = Pick< - FlashListProps, + FlashListProps, | 'onScroll' | 'contentContainerStyle' | 'onEndReached' @@ -17,11 +19,11 @@ type BaseSearchListProps = Pick< | 'showsVerticalScrollIndicator' | 'onLayout' > & { - /** The data to display in the list */ - data: SearchListItem[]; + /** The data to display in the list (full items or descriptors when using useSearchListItem) */ + data: SearchListDataItem[]; /** The function to render each item in the list */ - renderItem: (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent) => void) => React.JSX.Element; + renderItem: (item: SearchListDataItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent) => void) => React.JSX.Element; /** The columns that might change to trigger re-render via extraData */ columns: SearchColumnType[]; @@ -32,11 +34,11 @@ type BaseSearchListProps = Pick< /** The length of the flattened items in the list */ flattenedItemsLength: number; - /** The callback, which is run when a row is pressed */ - onSelectRow: (item: SearchListItem) => void; + /** The callback, which is run when a row is pressed (receives item or descriptor when using descriptor flow) */ + onSelectRow: (item: SearchListDataItem) => void; /** The ref to the list */ - ref: ForwardedRef>; + ref: ForwardedRef>; /** The function to scroll to an index */ scrollToIndex?: (index: number, animated?: boolean) => void; diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index e89d7c3d3b519..0c3f8797d0ec2 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -1,6 +1,4 @@ import {useFocusEffect, useRoute} from '@react-navigation/native'; -import {isUserValidatedSelector} from '@selectors/Account'; -import {tierNameSelector} from '@selectors/UserWallet'; import type {FlashListProps, FlashListRef, ViewToken} from '@shopify/flash-list'; import React, {useCallback, useContext, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; @@ -11,11 +9,13 @@ import Animated, {Easing, FadeOutUp, LinearTransition} from 'react-native-reanim import Checkbox from '@components/Checkbox'; import MenuItem from '@components/MenuItem'; import Modal from '@components/Modal'; -import {usePersonalDetails} from '@components/OnyxListItemProvider'; import {PressableWithFeedback} from '@components/Pressable'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import ScrollView from '@components/ScrollView'; -import type {SearchColumnType, SearchGroupBy, SearchQueryJSON, SelectedTransactions} from '@components/Search/types'; +import {useSearchListItemsCacheRef} from '@components/Search/SearchListItemsCacheContext'; +import type {SearchColumnType, SearchGroupBy, SearchListItemDescriptor, SearchQueryJSON, SelectedTransactions} from '@components/Search/types'; +import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; +import useSearchListItem from '@hooks/useSearchListItem'; import type ChatListItem from '@components/SelectionListWithSections/ChatListItem'; import type TaskListItem from '@components/SelectionListWithSections/Search/TaskListItem'; import type TransactionGroupListItem from '@components/SelectionListWithSections/Search/TransactionGroupListItem'; @@ -39,7 +39,6 @@ import useKeyboardState from '@hooks/useKeyboardState'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; @@ -52,9 +51,10 @@ import {getTableMinWidth, isTransactionReportGroupListItemType} from '@libs/Sear import variables from '@styles/variables'; import type {TransactionPreviewData} from '@userActions/Search'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction, TransactionViolations} from '@src/types/onyx'; +import type {ThemeStyles} from '@styles/index'; import BaseSearchList from './BaseSearchList'; +import type BaseSearchListProps from './BaseSearchList/types'; const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0); @@ -68,8 +68,18 @@ type SearchListHandle = { scrollToIndex: (index: number, animated?: boolean) => void; }; +type SearchListDataItem = SearchListItem | SearchListItemDescriptor; + +function isDescriptor(item: SearchListDataItem): item is SearchListItemDescriptor { + return typeof item === 'object' && item !== null && 'type' in item && !('report' in item); +} + type SearchListProps = Pick, 'onScroll' | 'contentContainerStyle' | 'onEndReached' | 'onEndReachedThreshold' | 'ListFooterComponent'> & { - data: SearchListItem[]; + /** List data: either full items (legacy) or descriptors when sortedData is provided */ + data: SearchListDataItem[]; + + /** When provided, data is treated as descriptors and this is used for selection/flattened counts */ + sortedData?: SearchListItem[]; /** Default renderer for every item in the list */ ListItem: SearchListItemComponentType; @@ -142,7 +152,7 @@ type SearchListProps = Pick, 'onScroll' | 'conten ref?: ForwardedRef; }; -const keyExtractor = (item: SearchListItem, index: number) => item.keyForList ?? `${index}`; +const keyExtractor = (item: SearchListDataItem, index: number) => item.keyForList ?? `${index}`; function isTransactionGroupListItemArray(data: SearchListItem[]): data is TransactionGroupListItemType[] { if (data.length <= 0) { @@ -152,6 +162,100 @@ function isTransactionGroupListItemArray(data: SearchListItem[]): data is Transa return typeof firstElement === 'object' && 'transactions' in firstElement; } +/** Row wrapper that resolves full item from descriptor via useSearchListItem and renders ListItem */ +function SearchListItemRow({ + descriptor, + index, + isItemFocused, + onFocus, + ListItem, + onSelectRow, + handleLongPressRow, + onCheckboxPress, + canSelectMultiple, + shouldPreventDefaultFocusOnSelectRow, + hash, + columns, + groupBy, + type, + onDEWModalOpen, + isDEWBetaEnabled, + isOffline, + violations, + customCardNames, + newTransactions, + shouldAnimate, + hasItemsBeingRemoved, + dataLength, + styles, +}: { + descriptor: SearchListItemDescriptor; + index: number; + isItemFocused: boolean; + onFocus?: (event: NativeSyntheticEvent) => void; + ListItem: SearchListItemComponentType; + onSelectRow: SearchListProps['onSelectRow']; + handleLongPressRow: (item: SearchListItem, itemTransactions?: TransactionListItemType[]) => void; + onCheckboxPress: SearchListProps['onCheckboxPress']; + canSelectMultiple: boolean; + shouldPreventDefaultFocusOnSelectRow?: boolean; + hash: number; + columns: SearchColumnType[]; + groupBy: SearchGroupBy | undefined; + type: SearchDataTypes; + onDEWModalOpen?: () => void; + isDEWBetaEnabled?: boolean; + isOffline: boolean; + violations: Record | undefined; + customCardNames: Record | undefined; + newTransactions: Transaction[]; + shouldAnimate?: boolean; + hasItemsBeingRemoved: boolean; + dataLength: number; + styles: ThemeStyles; +}) { + const {item} = useSearchListItem(descriptor); + const isDisabled = item?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const shouldApplyAnimation = shouldAnimate && index < dataLength - 1; + const newTransactionID = newTransactions.find((transaction) => item && isTransactionMatchWithGroupItem(transaction, item, groupBy))?.transactionID; + + if (!item) { + return null; + } + + return ( + + + + ); +} + function isTransactionMatchWithGroupItem(transaction: Transaction, groupItem: SearchListItem, groupBy: SearchGroupBy | undefined) { if (groupBy === CONST.SEARCH.GROUP_BY.CARD) { return transaction.cardID === (groupItem as TransactionCardGroupListItemType).cardID; @@ -197,6 +301,7 @@ function isTransactionMatchWithGroupItem(transaction: Transaction, groupItem: Se function SearchList({ data, + sortedData, ListItem, SearchTableHeader, onSelectRow, @@ -228,23 +333,35 @@ function SearchList({ }: SearchListProps) { const styles = useThemeStyles(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['CheckSquare']); + const itemsCacheRef = useSearchListItemsCacheRef(); const {hash, groupBy, type} = queryJSON; + const fullData = sortedData ?? (data as SearchListItem[]); + + const onSelectRowResolved = useCallback( + (itemOrDescriptor: SearchListDataItem, transactionPreviewData?: TransactionPreviewData) => { + const item = isDescriptor(itemOrDescriptor) ? itemsCacheRef?.current?.get(itemOrDescriptor.keyForList) : itemOrDescriptor; + if (item) { + onSelectRow(item, transactionPreviewData); + } + }, + [itemsCacheRef, onSelectRow], + ); const flattenedItems = useMemo(() => { if (groupBy || type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { - if (!isTransactionGroupListItemArray(data)) { - return data; + if (!isTransactionGroupListItemArray(fullData)) { + return fullData; } - return data.flatMap((item) => item.transactions); + return fullData.flatMap((item) => item.transactions); } - return data; - }, [data, groupBy, type]); + return fullData; + }, [fullData, groupBy, type]); const emptyReports = useMemo(() => { - if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) { - return data.filter((item) => item.transactions.length === 0); + if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(fullData)) { + return fullData.filter((item) => item.transactions.length === 0); } return []; - }, [data, type]); + }, [fullData, type]); const selectedItemsLength = useMemo(() => { const selectedTransactionsCount = flattenedItems.reduce((acc, item) => { @@ -252,7 +369,7 @@ function SearchList({ return acc + (isTransactionSelected ? 1 : 0); }, 0); - if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) { + if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(fullData)) { const selectedEmptyReports = emptyReports.reduce((acc, item) => { const isEmptyReportSelected = !!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected); return acc + (isEmptyReportSelected ? 1 : 0); @@ -262,10 +379,10 @@ function SearchList({ } return selectedTransactionsCount; - }, [flattenedItems, type, data, emptyReports, selectedTransactions]); + }, [flattenedItems, type, fullData, emptyReports, selectedTransactions]); const totalItems = useMemo(() => { - if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) { + if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(fullData)) { const selectableEmptyReports = emptyReports.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); const selectableTransactions = flattenedItems.filter((item) => { if ('pendingAction' in item) { @@ -283,10 +400,10 @@ function SearchList({ return true; }); return selectableTransactions.length; - }, [data, type, flattenedItems, emptyReports]); + }, [fullData, type, flattenedItems, emptyReports]); const itemsWithSelection = useMemo(() => { - return data.map((item) => { + return fullData.map((item) => { let isSelected = false; let itemWithSelection: SearchListItem = item; @@ -332,11 +449,11 @@ function SearchList({ return {originalItem: item, itemWithSelection, isSelected}; }); - }, [data, canSelectMultiple, selectedTransactions]); + }, [fullData, canSelectMultiple, selectedTransactions]); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const listRef = useRef>(null); + const listRef = useRef>(null); const {isKeyboardShown} = useKeyboardState(); const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings(); const prevDataLength = usePrevious(data.length); @@ -348,18 +465,7 @@ function SearchList({ const [isModalVisible, setIsModalVisible] = useState(false); const [longPressedItem, setLongPressedItem] = useState(); - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { - canBeMissing: true, - }); - - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); - const hasItemsBeingRemoved = prevDataLength && prevDataLength > data.length; - const personalDetails = usePersonalDetails(); - - const [userWalletTierName] = useOnyx(ONYXKEYS.USER_WALLET, {selector: tierNameSelector, canBeMissing: false}); - const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true}); - const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true}); const route = useRoute(); const {getScrollOffset} = useContext(ScrollOffsetContext); @@ -450,15 +556,49 @@ function SearchList({ useImperativeHandle(ref, () => ({scrollToIndex}), [scrollToIndex]); + const useDescriptorFlow = !!sortedData; + const renderItem = useCallback( - (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent) => void) => { - const isDisabled = item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + (item: SearchListDataItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent) => void) => { + if (useDescriptorFlow && isDescriptor(item)) { + return ( + + ); + } + + const fullItem = item as SearchListItem; + const isDisabled = fullItem.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; const shouldApplyAnimation = shouldAnimate && index < data.length - 1; - const newTransactionID = newTransactions.find((transaction) => isTransactionMatchWithGroupItem(transaction, item, groupBy))?.transactionID; + const newTransactionID = newTransactions.find((transaction) => isTransactionMatchWithGroupItem(transaction, fullItem, groupBy))?.transactionID; const itemData = itemsWithSelection.at(index); - const itemWithSelection = itemData?.itemWithSelection ?? item; + const itemWithSelection = itemData?.itemWithSelection ?? fullItem; return ( { + onSelectRow(item as SearchListItem); + } + } keyExtractor={keyExtractor} onScroll={onScroll} showsVerticalScrollIndicator={false} @@ -581,7 +716,7 @@ function SearchList({ onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} ListFooterComponent={ListFooterComponent} - onViewableItemsChanged={onViewableItemsChanged} + onViewableItemsChanged={onViewableItemsChanged as BaseSearchListProps['onViewableItemsChanged']} onLayout={onLayout} contentContainerStyle={contentContainerStyle} newTransactions={newTransactions} diff --git a/src/components/Search/SearchListItemsCacheContext.tsx b/src/components/Search/SearchListItemsCacheContext.tsx new file mode 100644 index 0000000000000..0741b71cd1fe3 --- /dev/null +++ b/src/components/Search/SearchListItemsCacheContext.tsx @@ -0,0 +1,34 @@ +import React, {createContext, useCallback, useContext, useMemo, useRef} from 'react'; +import type {MutableRefObject} from 'react'; +import type {SearchListItem} from '@components/SelectionListWithSections/types'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type SearchListItemsCacheRef = MutableRefObject | null>; + +type SearchListItemsCacheContextValue = { + itemsCacheRef: SearchListItemsCacheRef; + setItemsCache: (items: SearchListItem[]) => void; +}; + +const SearchListItemsCacheContext = createContext(null); + +function SearchListItemsCacheProvider({children}: ChildrenProps) { + const itemsCacheRef = useRef | null>(null); + const setItemsCache = useCallback((items: SearchListItem[]) => { + itemsCacheRef.current = new Map(items.map((item) => [item.keyForList ?? '', item])); + }, []); + const value = useMemo(() => ({itemsCacheRef, setItemsCache}), [setItemsCache]); + return {children}; +} + +function useSearchListItemsCacheRef(): SearchListItemsCacheRef | null { + const context = useContext(SearchListItemsCacheContext); + return context?.itemsCacheRef ?? null; +} + +function useSearchListItemsCache(): SearchListItemsCacheContextValue | null { + return useContext(SearchListItemsCacheContext); +} + +export {SearchListItemsCacheProvider, useSearchListItemsCacheRef, useSearchListItemsCache}; +export type {SearchListItemsCacheRef, SearchListItemsCacheContextValue}; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 876c8b137cc6a..3fbdd93f85741 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,6 +1,7 @@ import {findFocusedRoute, useFocusEffect, useIsFocused, useNavigation} from '@react-navigation/native'; import * as Sentry from '@sentry/react-native'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import type {ReactNode} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -64,6 +65,7 @@ import { isTransactionReportGroupListItemType, shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, + toSearchListItemDescriptor, } from '@libs/SearchUIUtils'; import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import markNavigateAfterExpenseCreateEnd from '@libs/telemetry/markNavigateAfterExpenseCreateEnd'; @@ -84,6 +86,7 @@ import arraysEqual from '@src/utils/arraysEqual'; import SearchChartView from './SearchChartView'; import {useSearchContext} from './SearchContext'; import SearchList from './SearchList'; +import {SearchListItemsCacheProvider, useSearchListItemsCache} from './SearchListItemsCacheContext'; import {SearchScopeProvider} from './SearchScopeProvider'; import type {SearchColumnType, SearchParams, SearchQueryJSON, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types'; @@ -99,6 +102,15 @@ type SearchProps = { onDEWModalOpen?: () => void; }; +/** Syncs sortedData into the list items cache so useSearchListItem can resolve items by descriptor. */ +function SearchListCacheSync({sortedData, children}: {sortedData: SearchListItem[]; children: ReactNode}) { + const cache = useSearchListItemsCache(); + useLayoutEffect(() => { + cache?.setItemsCache(sortedData); + }, [sortedData, cache]); + return children; +} + function mapTransactionItemToSelectedEntry( item: TransactionListItemType, itemTransaction: OnyxEntry, @@ -1062,6 +1074,8 @@ function Search({ [type, status, filteredData, localeCompare, translate, sortBy, sortOrder, validGroupBy, isChat, newSearchResultKeys, hash], ); + const orderedDescriptors = useMemo(() => sortedData.map((item) => toSearchListItemDescriptor(item)), [sortedData]); + useEffect(() => { const currentRoute = Navigation.getActiveRouteWithoutParams(); if (hasErrors && (currentRoute === '/' || (shouldResetSearchQuery && currentRoute === '/search'))) { @@ -1275,67 +1289,72 @@ function Search({ return ( - - - - - ) - } - contentContainerStyle={[styles.pb3, contentContainerStyle]} - containerStyle={[styles.pv0, !tableHeaderVisible && !isSmallScreenWidth && styles.pt3]} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - onScroll={onSearchListScroll} - onEndReachedThreshold={0.75} - onEndReached={fetchMoreResults} - ListFooterComponent={ - shouldShowLoadingMoreItems ? ( - - ) : undefined - } - queryJSON={queryJSON} - columns={columnsToShow} - violations={violations} - onLayout={onLayout} - isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} - shouldAnimate={type === CONST.SEARCH.DATA_TYPES.EXPENSE} - newTransactions={newTransactions} - hasLoadedAllTransactions={hasLoadedAllTransactions} - customCardNames={customCardNames} - /> - + + + + + + + ) + } + contentContainerStyle={[styles.pb3, contentContainerStyle]} + containerStyle={[styles.pv0, !tableHeaderVisible && !isSmallScreenWidth && styles.pt3]} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} + onScroll={onSearchListScroll} + onEndReachedThreshold={0.75} + onEndReached={fetchMoreResults} + ListFooterComponent={ + shouldShowLoadingMoreItems ? ( + + ) : undefined + } + queryJSON={queryJSON} + columns={columnsToShow} + violations={violations} + onLayout={onLayout} + isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} + shouldAnimate={type === CONST.SEARCH.DATA_TYPES.EXPENSE} + newTransactions={newTransactions} + hasLoadedAllTransactions={hasLoadedAllTransactions} + customCardNames={customCardNames} + /> + + + ); } diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index e671df62f8d50..4b190c56351ee 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -322,6 +322,25 @@ type BankAccountMenuItem = { value: PaymentMethod; }; +/** + * Minimal descriptor for a search list item. Used to pass only IDs to the list + * so that full item data can be resolved per-row via useSearchListItem. + */ +type SearchListItemDescriptor = { + /** Stable key for list identity and selection */ + keyForList: string; + /** Search data type (expense, expense_report, chat, task) or group-by variant */ + type: SearchDataTypes | string; + transactionID?: string; + reportID?: string; + reportActionID?: string; + policyID?: string; + /** For group-by items: query hash to load transactions snapshot */ + transactionsQueryJSON?: SearchQueryJSON; + /** For group-by items: which group (e.g. EXPENSE_REPORT, FROM, CARD) */ + groupedBy?: SearchGroupBy; +}; + /** Union type representing all possible grouped transaction item types used in chart views */ type GroupedItem = | TransactionMemberGroupListItemType @@ -336,6 +355,7 @@ type GroupedItem = | TransactionQuarterGroupListItemType; export type { + SearchListItemDescriptor, SelectedTransactionInfo, SelectedTransactions, SearchColumnType, diff --git a/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx b/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx index 8e374b0a08c8e..23a5c4cddfd8b 100644 --- a/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx +++ b/src/components/SelectionListWithSections/BaseSelectionListItemRenderer.tsx @@ -1,11 +1,9 @@ import React from 'react'; import type {NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import type useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import type useSingleExecution from '@hooks/useSingleExecution'; import {isMobileChrome} from '@libs/Browser'; import {isTransactionGroupListItemType} from '@libs/SearchUIUtils'; -import type {PersonalDetailsList} from '@src/types/onyx'; import type {BaseListItemProps, ExtendedTargetedEvent, ListItem, SelectionListProps} from './types'; type BaseSelectionListItemRendererProps = Omit, 'onSelectRow'> & @@ -18,10 +16,6 @@ type BaseSelectionListItemRendererProps = Omit['singleExecution']; titleStyles?: StyleProp; titleContainerStyles?: StyleProp; - userWalletTierName?: string | undefined; - isUserValidated?: boolean | undefined; - personalDetails?: OnyxEntry; - userBillingFundID?: number | undefined; }; function BaseSelectionListItemRenderer({ @@ -53,10 +47,6 @@ function BaseSelectionListItemRenderer({ titleContainerStyles, shouldUseDefaultRightHandSideCheckmark, canShowProductTrainingTooltip = true, - userWalletTierName, - isUserValidated, - personalDetails, - userBillingFundID, shouldShowRightCaret, shouldHighlightSelectedItem = true, shouldDisableHoverStyle, @@ -112,10 +102,6 @@ function BaseSelectionListItemRenderer({ titleContainerStyles={titleContainerStyles} shouldUseDefaultRightHandSideCheckmark={shouldUseDefaultRightHandSideCheckmark} canShowProductTrainingTooltip={canShowProductTrainingTooltip} - userWalletTierName={userWalletTierName} - isUserValidated={isUserValidated} - personalDetails={personalDetails} - userBillingFundID={userBillingFundID} index={index} shouldShowRightCaret={shouldShowRightCaret} shouldHighlightSelectedItem={shouldHighlightSelectedItem} diff --git a/src/components/SelectionListWithSections/ChatListItem.tsx b/src/components/SelectionListWithSections/ChatListItem.tsx index bef3516aed611..aaaee81164e10 100644 --- a/src/components/SelectionListWithSections/ChatListItem.tsx +++ b/src/components/SelectionListWithSections/ChatListItem.tsx @@ -1,7 +1,12 @@ import React from 'react'; +import {isUserValidatedSelector} from '@selectors/Account'; +import {tierNameSelector} from '@selectors/UserWallet'; +import {usePersonalDetails} from '@components/OnyxListItemProvider'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useOnyx from '@hooks/useOnyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import FS from '@libs/Fullstory'; import ReportActionItem from '@pages/inbox/report/ReportActionItem'; import variables from '@styles/variables'; @@ -20,15 +25,14 @@ function ChatListItem({ onFocus, onLongPressRow, shouldSyncFocus, - policies, - allReports, - userWalletTierName, - isUserValidated, - personalDetails, - userBillingFundID, }: ChatListItemProps) { const reportActionItem = item as unknown as ReportActionListItemType; - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportActionItem?.reportID}`]; + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportActionItem?.reportID)}`, {canBeMissing: true}); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`, {canBeMissing: true}); + const personalDetails = usePersonalDetails(); + const [userWalletTierName] = useOnyx(ONYXKEYS.USER_WALLET, {selector: tierNameSelector, canBeMissing: false}); + const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true}); + const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true}); const styles = useThemeStyles(); const theme = useTheme(); const animatedHighlightStyle = useAnimatedHighlightStyle({ @@ -73,9 +77,8 @@ function ChatListItem({ forwardedFSClass={fsClass} > onSelectRow(item)} parentReportAction={undefined} displayAsGroup={false} @@ -85,7 +88,6 @@ function ChatListItem({ isFirstVisibleReportAction={false} shouldDisplayContextMenu={false} shouldShowDraftMessage={false} - policies={policies} shouldShowBorder userWalletTierName={userWalletTierName} isUserValidated={isUserValidated} diff --git a/src/components/SelectionListWithSections/Search/TaskListItem.tsx b/src/components/SelectionListWithSections/Search/TaskListItem.tsx index c91a193a91201..dedae619a2329 100644 --- a/src/components/SelectionListWithSections/Search/TaskListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TaskListItem.tsx @@ -2,9 +2,11 @@ import React from 'react'; import BaseListItem from '@components/SelectionListWithSections/BaseListItem'; import type {ListItem, TaskListItemProps, TaskListItemType} from '@components/SelectionListWithSections/types'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import FS from '@libs/Fullstory'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -20,10 +22,9 @@ function TaskListItem({ onFocus, onLongPressRow, shouldSyncFocus, - allReports, }: TaskListItemProps) { const taskItem = item as unknown as TaskListItemType; - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskItem?.parentReportID}`]; + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(taskItem?.parentReportID)}`, {canBeMissing: true}); const styles = useThemeStyles(); const theme = useTheme(); diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index 03ecfb999f286..e67f98cc94faf 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -674,12 +674,6 @@ type TransactionListItemProps = ListItemProps & { type TaskListItemProps = ListItemProps & { /** Whether the item's action is loading */ isLoading?: boolean; - - /** All the data of the report collection */ - allReports?: OnyxCollection; - - /** Personal details list */ - personalDetails: OnyxEntry; }; type ExpenseReportListItemProps = ListItemProps & { @@ -699,7 +693,6 @@ type ExpenseReportListItemProps = ListItemProps & type TransactionGroupListItemProps = ListItemProps & { groupBy?: SearchGroupBy; searchType?: SearchDataTypes; - policies?: OnyxCollection; accountID?: number; columns?: SearchColumnType[]; newTransactionID?: string; @@ -729,27 +722,6 @@ type TransactionGroupListExpandedProps = Pick< type ChatListItemProps = ListItemProps & { queryJSONHash?: number; - - /** The policies which the user has access to */ - policies?: OnyxCollection; - - /** All the data of the report collection */ - allReports?: OnyxCollection; - - /** The report data */ - report?: Report; - - /** The user wallet tierName */ - userWalletTierName: string | undefined; - - /** Whether the user is validated */ - isUserValidated: boolean | undefined; - - /** Personal details list */ - personalDetails: OnyxEntry; - - /** User billing fund ID */ - userBillingFundID: number | undefined; }; type ValidListItem = diff --git a/src/hooks/useSearchListItem.ts b/src/hooks/useSearchListItem.ts new file mode 100644 index 0000000000000..9a17c13e2a990 --- /dev/null +++ b/src/hooks/useSearchListItem.ts @@ -0,0 +1,40 @@ +import {useMemo} from 'react'; +import {useSearchContext} from '@components/Search/SearchContext'; +import {useSearchListItemsCacheRef} from '@components/Search/SearchListItemsCacheContext'; +import type {SearchListItemDescriptor} from '@components/Search/types'; +import type {SearchListItem} from '@components/SelectionListWithSections/types'; + +type UseSearchListItemResult = { + item: SearchListItem | null; + isSelected: boolean; +}; + +/** + * Resolves full list item and selection state for a search list row from a descriptor. + * Reads from the search list items cache (populated by Search when sortedData is computed) + * and selection state from SearchContext. Use in each list row so only that row re-renders + * when its data or selection changes, instead of the whole list. + */ +function useSearchListItem(descriptor: SearchListItemDescriptor): UseSearchListItemResult { + const itemsCacheRef = useSearchListItemsCacheRef(); + const {selectedTransactions} = useSearchContext(); + + return useMemo(() => { + // Reading ref during render is intentional: cache is synced by parent when sortedData changes; we need it to resolve the item for this row. + // eslint-disable-next-line react-hooks/refs -- cache ref is the source of truth for list items, updated by Search when sortedData changes + const cache = itemsCacheRef?.current; + const cachedItem = (cache?.get(descriptor.keyForList) ?? null) as SearchListItem | null; + const isSelected = !!(cachedItem && selectedTransactions[descriptor.keyForList]?.isSelected); + + if (!cachedItem) { + return {item: null, isSelected: false}; + } + + const itemWithSelection: SearchListItem = {...cachedItem, isSelected}; + return {item: itemWithSelection, isSelected}; + // eslint-disable-next-line rulesdir/prefer-narrow-hook-dependencies -- itemsCacheRef intentional: ref.current not in deps (ref updates don't trigger re-renders) + }, [descriptor.keyForList, itemsCacheRef, selectedTransactions]); +} + +export default useSearchListItem; +export type {UseSearchListItemResult}; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c1cac0beed9d2..277a9d7fc7649 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -21,6 +21,7 @@ import type { SearchDatePreset, SearchFilterKey, SearchGroupBy, + SearchListItemDescriptor, SearchQueryJSON, SearchStatus, SearchView, @@ -1148,6 +1149,60 @@ function isGroupedItemArray(data: ListItem[]): data is GroupedItem[] { return data.length === 0 || (first !== undefined && isTransactionGroupListItemType(first) && 'groupedBy' in first); } +/** + * Converts a full SearchListItem to a minimal descriptor (keyForList + type + IDs). + * Used so the list can pass only descriptors and resolve full items per-row. + */ +function toSearchListItemDescriptor(item: SearchListItem): SearchListItemDescriptor { + if (isTransactionListItemType(item)) { + return { + keyForList: item.keyForList, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + transactionID: item.transactionID, + reportID: item.reportID, + policyID: item.policyID, + }; + } + if (isReportActionListItemType(item)) { + return { + keyForList: item.keyForList, + type: CONST.SEARCH.DATA_TYPES.CHAT, + reportID: item.reportID, + reportActionID: item.reportActionID, + }; + } + if (isTaskListItemType(item)) { + return { + keyForList: item.keyForList, + type: CONST.SEARCH.DATA_TYPES.TASK, + reportID: item.reportID, + }; + } + if (isTransactionReportGroupListItemType(item)) { + return { + keyForList: item.keyForList ?? '', + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + reportID: item.reportID, + policyID: item.policyID, + transactionsQueryJSON: item.transactionsQueryJSON, + groupedBy: item.groupedBy as SearchListItemDescriptor['groupedBy'], + }; + } + if (isTransactionGroupListItemType(item)) { + return { + keyForList: item.keyForList ?? '', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + reportID: 'reportID' in item ? item.reportID : undefined, + transactionsQueryJSON: item.transactionsQueryJSON, + groupedBy: ('groupedBy' in item ? item.groupedBy : undefined) as SearchListItemDescriptor['groupedBy'], + }; + } + return { + keyForList: (item as ListItem).keyForList ?? '', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + }; +} + /** * Type guard that checks if something is a TransactionListItemType */ @@ -4424,5 +4479,6 @@ export { getToFieldValueForTransaction, isTodoSearch, adjustTimeRangeToDateFilters, + toSearchListItemDescriptor, }; export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, ArchivedReportsIDSet}; diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index baa357264828d..06c6a2875d049 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useBlockedFromConcierge} from '@components/OnyxListItemProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -8,6 +8,7 @@ import useOriginalReportID from '@hooks/useOriginalReportID'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useReportTransactions from '@hooks/useReportTransactions'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import { @@ -38,13 +39,13 @@ import PureReportActionItem from './PureReportActionItem'; type ReportActionItemProps = Omit< PureReportActionItemProps, - 'taskReport' | 'linkedReport' | 'iouReportOfLinkedReport' | 'currentUserAccountID' | 'personalPolicyID' | 'allTransactionDrafts' + 'taskReport' | 'linkedReport' | 'iouReportOfLinkedReport' | 'currentUserAccountID' | 'personalPolicyID' | 'allTransactionDrafts' | 'allReports' | 'policies' > & { - /** All the data of the report collection */ - allReports: OnyxCollection; + /** All the data of the report collection (optional when used from Search list; component subscribes at row level) */ + allReports?: OnyxCollection; - /** All the data of the policy collection */ - policies: OnyxCollection; + /** All the data of the policy collection (optional when used from Search list; component subscribes at row level) */ + policies?: OnyxCollection; /** Whether to show the draft message or not */ shouldShowDraftMessage?: boolean; @@ -72,8 +73,8 @@ type ReportActionItemProps = Omit< }; function ReportActionItem({ - allReports, - policies, + allReports: allReportsProp, + policies: policiesProp, action, report, draftMessage, @@ -90,7 +91,102 @@ function ReportActionItem({ const reportID = report?.reportID; const originalMessage = getOriginalMessage(action); const originalReportID = useOriginalReportID(reportID, action); - const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; + const iouReportID = getIOUReportIDFromReportActionPreview(action); + const movedFromReportID = getMovedReportID(action, CONST.REPORT.MOVE_TYPE.FROM); + const movedToReportID = getMovedReportID(action, CONST.REPORT.MOVE_TYPE.TO); + const parentReportID = report?.parentReportID ?? undefined; + const taskReportID = originalMessage && 'taskReportID' in originalMessage ? originalMessage.taskReportID : undefined; + const linkedReportID = originalMessage && 'linkedReportID' in originalMessage ? originalMessage.linkedReportID : undefined; + + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(originalReportID)}`, {canBeMissing: true}); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(iouReportID)}`, {canBeMissing: true}); + const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(movedFromReportID)}`, {canBeMissing: true}); + const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(movedToReportID)}`, {canBeMissing: true}); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReportID)}`, {canBeMissing: true}); + const [taskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(taskReportID)}`, {canBeMissing: true}); + const [linkedReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(linkedReportID)}`, {canBeMissing: true}); + const [iouReportOfLinkedReport] = useOnyx( + `${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(linkedReport && 'iouReportID' in linkedReport ? linkedReport.iouReportID : undefined)}`, + {canBeMissing: true}, + ); + const [policyFromOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`, {canBeMissing: true}); + + const allReports = useMemo((): OnyxCollection => { + if (allReportsProp) { + return allReportsProp; + } + const key = (id: string | undefined) => (id ? `${ONYXKEYS.COLLECTION.REPORT}${id}` : ''); + const map: OnyxCollection = {}; + if (originalReportID && originalReport) { + map[key(originalReportID)] = originalReport; + } + if (iouReportID && iouReport) { + map[key(iouReportID)] = iouReport; + } + if (movedFromReportID && movedFromReport) { + map[key(movedFromReportID)] = movedFromReport; + } + if (movedToReportID && movedToReport) { + map[key(movedToReportID)] = movedToReport; + } + if (parentReportID && parentReport) { + map[key(parentReportID)] = parentReport; + } + if (taskReportID && taskReport) { + map[key(taskReportID)] = taskReport; + } + if (linkedReportID && linkedReport) { + map[key(linkedReportID)] = linkedReport; + } + if (linkedReport && 'iouReportID' in linkedReport && iouReportOfLinkedReport) { + map[key(linkedReport.iouReportID)] = iouReportOfLinkedReport; + } + if (reportID && report) { + map[key(reportID)] = report; + } + return map; + }, [ + allReportsProp, + originalReportID, + originalReport, + iouReportID, + iouReport, + movedFromReportID, + movedFromReport, + movedToReportID, + movedToReport, + parentReportID, + parentReport, + taskReportID, + taskReport, + linkedReportID, + linkedReport, + iouReportOfLinkedReport, + reportID, + report, + ]); + + // Narrow deps (report?.policyID) intentional; React Compiler infers broader `report` + // eslint-disable-next-line react-hooks/preserve-manual-memoization + const policies = useMemo((): OnyxCollection => { + if (policiesProp) { + return policiesProp; + } + if (!report?.policyID || !policyFromOnyx) { + return {}; + } + return {[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]: policyFromOnyx}; + }, [policiesProp, report?.policyID, policyFromOnyx]); + + const originalReportResolved = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; + const iouReportResolved = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; + const movedFromReportResolved = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${movedFromReportID}`]; + const movedToReportResolved = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${movedToReportID}`]; + const parentReportResolved = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; + const taskReportResolved = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`]; + const linkedReportResolved = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${linkedReportID}`]; + const iouReportOfLinkedReportResolved = + linkedReportResolved && 'iouReportID' in linkedReportResolved ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${linkedReportResolved.iouReportID}`] : undefined; const isOriginalReportArchived = useReportIsArchived(originalReportID); const {accountID: currentUserAccountID, email: currentUserEmail} = useCurrentUserPersonalDetails(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); @@ -103,14 +199,11 @@ function ReportActionItem({ const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {canBeMissing: true}); - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getIOUReportIDFromReportActionPreview(action)}`]; - const movedFromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.FROM)}`]; - const movedToReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.TO)}`]; const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); - const transactionsOnIOUReport = useReportTransactions(iouReport?.reportID); + const transactionsOnIOUReport = useReportTransactions(iouReportResolved?.reportID); const transactionID = isMoneyRequestAction(action) && getOriginalMessage(action)?.IOUTransactionID; const getLinkedTransactionRouteError = useCallback( @@ -122,17 +215,10 @@ function ReportActionItem({ const [linkedTransactionRouteError] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {canBeMissing: true, selector: getLinkedTransactionRouteError}); - // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID || undefined}`]; const blockedFromConcierge = useBlockedFromConcierge(); - const targetReport = isChatThread(report) ? parentReport : report; + const targetReport = isChatThread(report) ? parentReportResolved : report; const missingPaymentMethod = getIndicatedMissingPaymentMethod(userWalletTierName, targetReport?.reportID, action, bankAccountList); - const taskReport = originalMessage && 'taskReportID' in originalMessage ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalMessage.taskReportID}`] : undefined; - const linkedReport = originalMessage && 'linkedReportID' in originalMessage ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalMessage.linkedReportID}`] : undefined; - const iouReportOfLinkedReport = linkedReport && 'iouReportID' in linkedReport ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${linkedReport.iouReportID}`] : undefined; - return (