|
| 1 | +import React, {useImperativeHandle} from 'react'; |
| 2 | +import type {ForwardedRef} from 'react'; |
| 3 | +import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; |
| 4 | +import type {ExtendedTargetedEvent} from '@components/SelectionList/ListItem/types'; |
| 5 | +import type {TransactionPreviewData} from '@libs/actions/Search'; |
| 6 | +import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab'; |
| 7 | +import CONST from '@src/CONST'; |
| 8 | +import type {Transaction} from '@src/types/onyx'; |
| 9 | +import useSearchListViewState from './hooks/useSearchListViewState'; |
| 10 | +import AnimatedExitRow from './primitives/AnimatedExitRow'; |
| 11 | +import SelectionTopBar from './primitives/SelectionTopBar'; |
| 12 | +import BaseSearchList from './SearchList/BaseSearchList'; |
| 13 | +import ChatListItem from './SearchList/ListItem/ChatListItem'; |
| 14 | +import type {SearchListItem} from './SearchList/ListItem/types'; |
| 15 | +import SearchListViewLayout from './SearchListViewLayout'; |
| 16 | +import type {SearchColumnType, SearchQueryJSON} from './types'; |
| 17 | + |
| 18 | +/** Imperative handle the router uses for highlight-driven scrolling (mirrors SearchList's handle). */ |
| 19 | +type SearchListHandle = { |
| 20 | + scrollToIndex: (index: number, animated?: boolean) => void; |
| 21 | +}; |
| 22 | + |
| 23 | +type ChatSearchViewProps = { |
| 24 | + /** The chat search query. */ |
| 25 | + queryJSON: SearchQueryJSON; |
| 26 | + |
| 27 | + /** The sorted rows to render (from the router's useSearchSnapshot). */ |
| 28 | + data: SearchListItem[]; |
| 29 | + |
| 30 | + /** The columns to render in the list (drives the table min-width and header). */ |
| 31 | + columns: SearchColumnType[]; |
| 32 | + |
| 33 | + /** Whether the list supports multi-select. */ |
| 34 | + canSelectMultiple: boolean; |
| 35 | + |
| 36 | + /** Whether the action column uses its wider variant. */ |
| 37 | + isActionColumnWide: boolean; |
| 38 | + |
| 39 | + /** Whether mobile selection mode is on. */ |
| 40 | + isMobileSelectionModeEnabled: boolean; |
| 41 | + |
| 42 | + /** The column header element (undefined on narrow layouts). */ |
| 43 | + SearchTableHeader?: React.JSX.Element; |
| 44 | + |
| 45 | + /** Whether a table header bar is shown above the list. */ |
| 46 | + tableHeaderVisible: boolean; |
| 47 | + |
| 48 | + /** Whether everything has been loaded (gates the fully-checked select-all state). */ |
| 49 | + hasLoadedAllTransactions?: boolean; |
| 50 | + |
| 51 | + /** Rows flagged for the post-create highlight animation (feeds BaseSearchList extraData). */ |
| 52 | + newTransactions: Transaction[]; |
| 53 | + |
| 54 | + /** The navigation handler for a row tap (owned by the router). */ |
| 55 | + onSelectRow: (item: SearchListItem, transactionPreviewData?: TransactionPreviewData, event?: ModifiedMouseEvent) => void; |
| 56 | + |
| 57 | + /** The list footer (pagination / pending skeleton). */ |
| 58 | + ListFooterComponent?: React.JSX.Element; |
| 59 | + |
| 60 | + /** Fires when the list scrolls near its end (router's fetchMoreResults). */ |
| 61 | + onEndReached: () => void; |
| 62 | + |
| 63 | + /** Fires on the list's first layout and on layout changes. */ |
| 64 | + onLayout: () => void; |
| 65 | + |
| 66 | + /** Scroll handler forwarded to the list. */ |
| 67 | + onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void; |
| 68 | + |
| 69 | + /** Content container style for the list. */ |
| 70 | + contentContainerStyle: StyleProp<ViewStyle>; |
| 71 | + |
| 72 | + /** Outer container style for the list wrapper. */ |
| 73 | + containerStyle: StyleProp<ViewStyle>; |
| 74 | + |
| 75 | + /** Imperative handle for highlight-driven scrolling, set by the router. */ |
| 76 | + ref?: ForwardedRef<SearchListHandle>; |
| 77 | +}; |
| 78 | + |
| 79 | +const keyExtractor = (item: SearchListItem, index: number) => item.keyForList ?? `${index}`; |
| 80 | + |
| 81 | +const isRowDeleted = (item: SearchListItem) => item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; |
| 82 | + |
| 83 | +/** |
| 84 | + * The chat Search list. |
| 85 | + * |
| 86 | + * Rendered by `<Search>` as a child of the selection providers. The shared list state and interactions come |
| 87 | + * from `useSearchListViewState`, and the surrounding chrome from `SearchListViewLayout`; this view only owns |
| 88 | + * the chat specifics: the `ChatListItem` renderer and single-pass selection counts. Chat rows have no |
| 89 | + * column data, no checkbox, and no exit animation (only grouped expenses animate), and long press is |
| 90 | + * suppressed, so this is the leanest of the Search views. |
| 91 | + */ |
| 92 | +function ChatSearchView({ |
| 93 | + queryJSON, |
| 94 | + data, |
| 95 | + columns, |
| 96 | + canSelectMultiple, |
| 97 | + isActionColumnWide, |
| 98 | + isMobileSelectionModeEnabled, |
| 99 | + SearchTableHeader: searchTableHeader, |
| 100 | + tableHeaderVisible, |
| 101 | + hasLoadedAllTransactions, |
| 102 | + newTransactions, |
| 103 | + onSelectRow, |
| 104 | + ListFooterComponent, |
| 105 | + onEndReached, |
| 106 | + onLayout, |
| 107 | + onScroll, |
| 108 | + contentContainerStyle, |
| 109 | + containerStyle, |
| 110 | + ref, |
| 111 | +}: ChatSearchViewProps) { |
| 112 | + const {type} = queryJSON; |
| 113 | + |
| 114 | + const {isKeyboardShown, safeAreaPaddingBottomStyle, isLargeScreenWidth, toggleAll, selectedTransactions, listRef, onLongPressRow, modal, handleSelectRow, scrollToListIndex} = |
| 115 | + useSearchListViewState({ |
| 116 | + data, |
| 117 | + isMobileSelectionModeEnabled, |
| 118 | + onSelectRow, |
| 119 | + shouldPreventLongPressRow: true, |
| 120 | + }); |
| 121 | + |
| 122 | + // Chat is a flat list: no group children, no empty groups, so selection counts are a single pass. |
| 123 | + const selectedItemsLength = data.reduce((acc, item) => acc + (item.keyForList && selectedTransactions[item.keyForList]?.isSelected ? 1 : 0), 0); |
| 124 | + const totalItems = data.filter((item) => !isRowDeleted(item)).length; |
| 125 | + |
| 126 | + // Flat data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index. |
| 127 | + useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]); |
| 128 | + |
| 129 | + const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => ( |
| 130 | + // Chat rows never animate their exit (only grouped expenses do), so the wrapper just preserves the overflow clip. |
| 131 | + <AnimatedExitRow |
| 132 | + shouldApplyAnimation={false} |
| 133 | + hasItemsBeingRemoved={false} |
| 134 | + > |
| 135 | + <ChatListItem |
| 136 | + showTooltip |
| 137 | + isFocused={isItemFocused} |
| 138 | + onSelectRow={handleSelectRow} |
| 139 | + onLongPressRow={onLongPressRow} |
| 140 | + canSelectMultiple={canSelectMultiple} |
| 141 | + item={item} |
| 142 | + isDisabled={isRowDeleted(item)} |
| 143 | + onFocus={onFocus} |
| 144 | + keyForList={item.keyForList} |
| 145 | + /> |
| 146 | + </AnimatedExitRow> |
| 147 | + ); |
| 148 | + |
| 149 | + const isSelectAllChecked = selectedItemsLength > 0 && selectedItemsLength === totalItems && hasLoadedAllTransactions; |
| 150 | + const selectAllButtonVisible = canSelectMultiple && !searchTableHeader; |
| 151 | + |
| 152 | + return ( |
| 153 | + <SearchListViewLayout |
| 154 | + columns={columns} |
| 155 | + type={type} |
| 156 | + isActionColumnWide={isActionColumnWide} |
| 157 | + isHeaderVisible={!!searchTableHeader} |
| 158 | + dataKey={data} |
| 159 | + isKeyboardShown={isKeyboardShown} |
| 160 | + safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} |
| 161 | + containerStyle={containerStyle} |
| 162 | + > |
| 163 | + {tableHeaderVisible && ( |
| 164 | + <SelectionTopBar |
| 165 | + isLargeScreenWidth={isLargeScreenWidth} |
| 166 | + shouldSplitGroups={false} |
| 167 | + canSelectMultiple={canSelectMultiple} |
| 168 | + isSelectAllChecked={isSelectAllChecked} |
| 169 | + selectedItemsLength={selectedItemsLength} |
| 170 | + totalItems={totalItems} |
| 171 | + hasLoadedAllTransactions={hasLoadedAllTransactions} |
| 172 | + selectAllButtonVisible={selectAllButtonVisible} |
| 173 | + onAllCheckboxPress={toggleAll} |
| 174 | + SearchTableHeader={searchTableHeader} |
| 175 | + /> |
| 176 | + )} |
| 177 | + <BaseSearchList |
| 178 | + data={data} |
| 179 | + renderItem={renderItem} |
| 180 | + onSelectRow={handleSelectRow} |
| 181 | + keyExtractor={keyExtractor} |
| 182 | + onScroll={onScroll} |
| 183 | + showsVerticalScrollIndicator={false} |
| 184 | + ref={listRef} |
| 185 | + columns={columns} |
| 186 | + scrollToIndex={scrollToListIndex} |
| 187 | + flattenedItemsLength={data.length} |
| 188 | + onEndReached={onEndReached} |
| 189 | + onEndReachedThreshold={0.75} |
| 190 | + ListFooterComponent={ListFooterComponent} |
| 191 | + onLayout={onLayout} |
| 192 | + contentContainerStyle={contentContainerStyle} |
| 193 | + newTransactions={newTransactions} |
| 194 | + /> |
| 195 | + {modal} |
| 196 | + </SearchListViewLayout> |
| 197 | + ); |
| 198 | +} |
| 199 | + |
| 200 | +ChatSearchView.displayName = 'ChatSearchView'; |
| 201 | + |
| 202 | +export default ChatSearchView; |
0 commit comments