diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 1eb916acd2df..20d39d899894 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -78,7 +78,10 @@ function mapToItemWithSelectionInfo( shouldAnimateInHighlight: boolean, ) { if (SearchUIUtils.isReportActionListItemType(item)) { - return item; + return { + ...item, + shouldAnimateInHighlight, + }; } return SearchUIUtils.isTransactionListItemType(item) @@ -134,6 +137,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const previousTransactions = usePrevious(transactions); + const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); + const previousReportActions = usePrevious(reportActions); useEffect(() => { if (!currentSearchResults?.search?.type) { @@ -211,6 +216,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo previousTransactions, queryJSON, offset, + reportActions, + previousReportActions, }); // There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded @@ -323,15 +330,20 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo const ListItem = SearchUIUtils.getListItem(type, status); const sortedData = SearchUIUtils.getSortedSections(type, status, data, sortBy, sortOrder); + const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const sortedSelectedData = sortedData.map((item) => { - const baseKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`; + const baseKey = isChat + ? `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(item as ReportActionListItemType).reportActionID}` + : `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`; // Check if the base key matches the newSearchResultKey (TransactionListItemType) const isBaseKeyMatch = baseKey === newSearchResultKey; // Check if any transaction within the transactions array (ReportListItemType) matches the newSearchResultKey - const isAnyTransactionMatch = (item as ReportListItemType)?.transactions?.some((transaction) => { - const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`; - return transactionKey === newSearchResultKey; - }); + const isAnyTransactionMatch = + !isChat && + (item as ReportListItemType)?.transactions?.some((transaction) => { + const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`; + return transactionKey === newSearchResultKey; + }); // Determine if either the base key or any transaction key matches const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch; diff --git a/src/components/SelectionList/ChatListItem.tsx b/src/components/SelectionList/ChatListItem.tsx index a3e04c9088f1..4807aa7760c8 100644 --- a/src/components/SelectionList/ChatListItem.tsx +++ b/src/components/SelectionList/ChatListItem.tsx @@ -5,11 +5,13 @@ import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/M import MultipleAvatars from '@components/MultipleAvatars'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import TextWithTooltip from '@components/TextWithTooltip'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ReportActionItemDate from '@pages/home/report/ReportActionItemDate'; import ReportActionItemFragment from '@pages/home/report/ReportActionItemFragment'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; import type {ChatListItemProps, ListItem, ReportActionListItemType} from './types'; @@ -56,11 +58,24 @@ function ChatListItem({ const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; const mentionReportContextValue = useMemo(() => ({currentReportID: item?.reportID ?? '-1'}), [item.reportID]); - + const animatedHighlightStyle = useAnimatedHighlightStyle({ + borderRadius: variables.componentBorderRadius, + shouldHighlight: item?.shouldAnimateInHighlight ?? false, + highlightColor: theme.messageHighlightBG, + backgroundColor: theme.highlightBG, + }); + const pressableStyle = [ + styles.selectionListPressableItemWrapper, + styles.textAlignLeft, + // Removing background style because they are added to the parent OpacityView via animatedHighlightStyle + styles.bgTransparent, + item.isSelected && styles.activeComponentBG, + item.cursorStyle, + ]; return ( ({ keyForList={item.keyForList} onFocus={onFocus} shouldSyncFocus={shouldSyncFocus} + pressableWrapperStyle={[styles.mh5, animatedHighlightStyle]} hoverStyle={item.isSelected && styles.activeComponentBG} > {(hovered) => ( diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index 95a953139ebe..a5937559fd2f 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -3,43 +3,51 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SearchQueryJSON} from '@components/Search/types'; import type {ReportActionListItemType, ReportListItemType, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types'; import * as SearchActions from '@libs/actions/Search'; +import {isReportActionEntry} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {SearchResults, Transaction} from '@src/types/onyx'; +import type {ReportActions, SearchResults, Transaction} from '@src/types/onyx'; import usePrevious from './usePrevious'; type UseSearchHighlightAndScroll = { searchResults: OnyxEntry; transactions: OnyxCollection; previousTransactions: OnyxCollection; + reportActions: OnyxCollection; + previousReportActions: OnyxCollection; queryJSON: SearchQueryJSON; offset: number; }; /** - * Hook used to trigger a search when a new transaction is added and handle highlighting and scrolling. + * Hook used to trigger a search when a new transaction or report action is added and handle highlighting and scrolling. */ -function useSearchHighlightAndScroll({searchResults, transactions, previousTransactions, queryJSON, offset}: UseSearchHighlightAndScroll) { +function useSearchHighlightAndScroll({searchResults, transactions, previousTransactions, reportActions, previousReportActions, queryJSON, offset}: UseSearchHighlightAndScroll) { // Ref to track if the search was triggered by this hook const triggeredByHookRef = useRef(false); const searchTriggeredRef = useRef(false); const previousSearchResults = usePrevious(searchResults?.data); const [newSearchResultKey, setNewSearchResultKey] = useState(null); - const highlightedTransactionIDs = useRef>(new Set()); + const highlightedIDs = useRef>(new Set()); const initializedRef = useRef(false); + const isChat = queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT; - // Trigger search when a new transaction is added + // Trigger search when a new report action is added while on chat or when a new transaction is added for the other search types. useEffect(() => { const previousTransactionsLength = previousTransactions && Object.keys(previousTransactions).length; const transactionsLength = transactions && Object.keys(transactions).length; - // Return early if search was already triggered or there's no change in transactions length - if (searchTriggeredRef.current || previousTransactionsLength === transactionsLength) { + const reportActionsLength = reportActions && Object.values(reportActions).reduce((sum, curr) => sum + Object.keys(curr ?? {}).length, 0); + const prevReportActionsLength = previousReportActions && Object.values(previousReportActions).reduce((sum, curr) => sum + Object.keys(curr ?? {}).length, 0); + // Return early if search was already triggered or there's no change in current and previous data length + if (searchTriggeredRef.current || (!isChat && previousTransactionsLength === transactionsLength) || (isChat && reportActionsLength === prevReportActionsLength)) { return; } + const newTransactionAdded = transactionsLength && typeof previousTransactionsLength === 'number' && transactionsLength > previousTransactionsLength; + const newReportActionAdded = reportActionsLength && typeof prevReportActionsLength === 'number' && reportActionsLength > prevReportActionsLength; - // Check if a new transaction was added - if (transactionsLength && typeof previousTransactionsLength === 'number' && transactionsLength > previousTransactionsLength) { + // Check if a new transaction or report action was added + if ((!isChat && !!newTransactionAdded) || (isChat && !!newReportActionAdded)) { // Set the flag indicating the search is triggered by the hook triggeredByHookRef.current = true; @@ -50,45 +58,62 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans searchTriggeredRef.current = true; } - // Reset the ref when transactions are updated + // Reset the ref when transactions or report actions in chat search type are updated return () => { searchTriggeredRef.current = false; }; - }, [transactions, previousTransactions, queryJSON, offset]); + }, [transactions, previousTransactions, queryJSON, offset, reportActions, previousReportActions, isChat]); - // Initialize the set with existing transaction IDs only once + // Initialize the set with existing IDs only once useEffect(() => { if (initializedRef.current || !searchResults?.data) { return; } - const existingTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); - highlightedTransactionIDs.current = new Set(existingTransactionIDs); + const existingIDs = isChat ? extractReportActionIDsFromSearchResults(searchResults.data) : extractTransactionIDsFromSearchResults(searchResults.data); + highlightedIDs.current = new Set(existingIDs); initializedRef.current = true; - }, [searchResults?.data]); + }, [searchResults?.data, isChat]); - // Detect new transactions + // Detect new items (transactions or report actions) useEffect(() => { if (!previousSearchResults || !searchResults?.data) { return; } + if (isChat) { + const previousReportActionIDs = extractReportActionIDsFromSearchResults(previousSearchResults); + const currentReportActionIDs = extractReportActionIDsFromSearchResults(searchResults.data); - const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults); - const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); + // Find new report action IDs that are not in the previousReportActionIDs and not already highlighted + const newReportActionIDs = currentReportActionIDs.filter((id) => !previousReportActionIDs.includes(id) && !highlightedIDs.current.has(id)); - // Find new transaction IDs that are not in the previousTransactionIDs and not already highlighted - const newTransactionIDs = currentTransactionIDs.filter((id) => !previousTransactionIDs.includes(id) && !highlightedTransactionIDs.current.has(id)); + if (!triggeredByHookRef.current || newReportActionIDs.length === 0) { + return; + } - if (!triggeredByHookRef.current || newTransactionIDs.length === 0) { - return; - } + const newReportActionID = newReportActionIDs.at(0) ?? ''; + const newReportActionKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newReportActionID}`; - const newTransactionID = newTransactionIDs.at(0) ?? ''; - const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${newTransactionID}`; + setNewSearchResultKey(newReportActionKey); + highlightedIDs.current.add(newReportActionID); + } else { + const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults); + const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); - setNewSearchResultKey(newTransactionKey); - highlightedTransactionIDs.current.add(newTransactionID); - }, [searchResults, previousSearchResults]); + // Find new transaction IDs that are not in the previousTransactionIDs and not already highlighted + const newTransactionIDs = currentTransactionIDs.filter((id) => !previousTransactionIDs.includes(id) && !highlightedIDs.current.has(id)); + + if (!triggeredByHookRef.current || newTransactionIDs.length === 0) { + return; + } + + const newTransactionID = newTransactionIDs.at(0) ?? ''; + const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${newTransactionID}`; + + setNewSearchResultKey(newTransactionKey); + highlightedIDs.current.add(newTransactionID); + } + }, [searchResults?.data, previousSearchResults, isChat]); // Reset newSearchResultKey after it's been used useEffect(() => { @@ -114,35 +139,41 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans return; } - // Extract the transaction ID from the newSearchResultKey - const newTransactionID = newSearchResultKey.replace(ONYXKEYS.COLLECTION.TRANSACTION, ''); - - // Find the index of the new transaction in the data array - const indexOfNewTransaction = data.findIndex((item) => { - // Handle TransactionListItemType - if ('transactionID' in item && item.transactionID === newTransactionID) { - return true; - } - - // Handle ReportListItemType with transactions array - if ('transactions' in item && Array.isArray(item.transactions)) { - return item.transactions.some((transaction) => transaction?.transactionID === newTransactionID); + // Extract the transaction/report action ID from the newSearchResultKey + const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, ''); + + // Find the index of the new transaction/report action in the data array + const indexOfNewItem = data.findIndex((item) => { + if (isChat) { + if ('reportActionID' in item && item.reportActionID === newID) { + return true; + } + } else { + // Handle TransactionListItemType + if ('transactionID' in item && item.transactionID === newID) { + return true; + } + + // Handle ReportListItemType with transactions array + if ('transactions' in item && Array.isArray(item.transactions)) { + return item.transactions.some((transaction) => transaction?.transactionID === newID); + } } return false; }); - // Early return if the transaction is not found in the data array - if (indexOfNewTransaction <= 0) { + // Early return if the new item is not found in the data array + if (indexOfNewItem <= 0) { return; } // Perform the scrolling action - ref.scrollToIndex(indexOfNewTransaction); + ref.scrollToIndex(indexOfNewItem); // Reset the trigger flag to prevent unintended future scrolls and highlights triggeredByHookRef.current = false; }, - [newSearchResultKey], + [newSearchResultKey, isChat], ); return {newSearchResultKey, handleSelectionListScroll}; @@ -174,4 +205,14 @@ function extractTransactionIDsFromSearchResults(searchResultsData: Partial): string[] { + return Object.keys(searchResultsData ?? {}) + .filter(isReportActionEntry) + .map((key) => Object.keys(searchResultsData[key] ?? {})) + .flat(); +} + export default useSearchHighlightAndScroll; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e100fb885fff..df38ecaacc61 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -585,4 +585,5 @@ export { getExpenseTypeTranslationKey, getOverflowMenu, isCorrectSearchUserName, + isReportActionEntry, };