From dd90569329760cea02f8b98066ae8b85241fe2cc Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 13 Jun 2025 01:02:57 +0300 Subject: [PATCH 1/9] revert change back to calling handle scroll on ref --- src/components/Search/index.tsx | 6 ++---- src/hooks/useSearchHighlightAndScroll.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a080d50fe8fe..fc4672d6f046 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -6,7 +6,7 @@ import {useOnyx} from 'react-native-onyx'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; -import type {ReportActionListItemType, ReportListItemType, SearchListItem, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types'; +import type {ReportActionListItemType, ReportListItemType, SearchListItem, TransactionListItemType} from '@components/SelectionList/types'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -171,7 +171,6 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS [reportActions], ); const {translate} = useLocalize(); - const searchListRef = useRef(null); const shouldGroupByReports = groupBy === CONST.SEARCH.GROUP_BY.REPORTS; @@ -582,7 +581,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS return ( handleSelectionListScroll(sortedSelectedData, searchListRef.current)} /> ); diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index be20d3af461c..0d0cbc94a838 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -165,7 +165,7 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans * Callback to handle scrolling to the new search result. */ const handleSelectionListScroll = useCallback( - (data: SearchListItem[], ref: SelectionListHandle | null) => { + (data: SearchListItem[]) => (ref: SelectionListHandle | null) => { // Early return if there's no ref, new transaction wasn't brought in by this hook // or there's no new search result key if (!ref || !triggeredByHookRef.current || newSearchResultKey === null) { From c8d3323d576df9fc1cf32e8f8b33b54d78385165 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 20 Jun 2025 19:11:35 +0300 Subject: [PATCH 2/9] added data as a dependency --- src/components/Search/SearchList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Search/SearchList.tsx b/src/components/Search/SearchList.tsx index d346682d6eb0..9d72f2b0cbf5 100644 --- a/src/components/Search/SearchList.tsx +++ b/src/components/Search/SearchList.tsx @@ -220,8 +220,7 @@ function SearchList( listRef.current.scrollToIndex({index, animated, viewOffset: variables.contentHeaderHeight}); }, - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [], + [data], ); const setHasKeyBeenPressed = useCallback(() => { if (hasKeyBeenPressed.current) { From 88f16662a57432a31791ab91626b48b08ce6f57c Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 20 Jun 2025 19:17:06 +0300 Subject: [PATCH 3/9] gave a bit delay to scroll to last item --- src/hooks/useSearchHighlightAndScroll.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index 618792fbae7e..4af682df2881 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -202,7 +202,14 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans } // Perform the scrolling action - ref.scrollToIndex(indexOfNewItem); + if (indexOfNewItem === data.length - 1) { + // Scrolling to last item needs a delay to work due to FlatList internal bug. + setTimeout(() => { + ref.scrollToIndex(indexOfNewItem); + }, 100); + } else { + ref.scrollToIndex(indexOfNewItem); + } // Reset the trigger flag to prevent unintended future scrolls and highlights triggeredByHookRef.current = false; }, From 70b80ddc0f8399b08fb2a1eda54485da00e7a034 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 20 Jun 2025 19:24:09 +0300 Subject: [PATCH 4/9] increase delay --- src/hooks/useSearchHighlightAndScroll.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index 4af682df2881..d0e4a6b17d1b 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -206,7 +206,7 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans // Scrolling to last item needs a delay to work due to FlatList internal bug. setTimeout(() => { ref.scrollToIndex(indexOfNewItem); - }, 100); + }, 200); } else { ref.scrollToIndex(indexOfNewItem); } From 13bf5642eda5fe81809e0fcfb0675d5469ffcdd8 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 16 Jul 2025 17:47:28 +0300 Subject: [PATCH 5/9] minor fixes --- src/components/Search/index.tsx | 2 -- src/hooks/useSearchHighlightAndScroll.ts | 9 +-------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 2f30031782a9..09ac7124aef3 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -579,8 +579,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS ); }, [clearSelectedTransactions, data, groupBy, reportActionsArray, selectedTransactions, setSelectedTransactions]); - const onLayout = useCallback(() => handleSelectionListScroll(sortedSelectedData, searchListRef.current), [handleSelectionListScroll, sortedSelectedData]); - if (shouldShowLoadingState) { return ( { - ref.scrollToIndex(indexOfNewItem); - }, 200); - } else { - ref.scrollToIndex(indexOfNewItem); - } + ref.scrollToIndex(indexOfNewItem); // Reset the trigger flag to prevent unintended future scrolls and highlights triggeredByHookRef.current = false; }, From 162eeafb999dee28965fb1d3c204baa429132ae2 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 27 Aug 2025 22:47:21 +0300 Subject: [PATCH 6/9] fix hasNewItemsRef resetting logic --- src/hooks/useSearchHighlightAndScroll.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index 30ae2e116b30..43c21c0acc3d 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -106,7 +106,9 @@ function useSearchHighlightAndScroll({ // We only want to highlight new items if the addition of transactions or report actions triggered the search. // This is because, on deletion of items, the backend sometimes returns old items in place of the deleted ones. // We don't want to highlight these old items, even if they appear new in the current search results. - hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionsIDs.length; + if (!hasNewItemsRef.current) { + hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionsIDs.length; + } // Set the flag indicating the search is triggered by the hook triggeredByHookRef.current = true; @@ -164,6 +166,7 @@ function useSearchHighlightAndScroll({ setNewSearchResultKey(newReportActionKey); highlightedIDs.current.add(newReportActionID); + hasNewItemsRef.current = false; } else { const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults); const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); @@ -180,6 +183,7 @@ function useSearchHighlightAndScroll({ setNewSearchResultKey(newTransactionKey); highlightedIDs.current.add(newTransactionID); + hasNewItemsRef.current = false; } }, [searchResults?.data, previousSearchResults, isChat]); From 23bd44b6f1c652b8e4c0dae86ab6fd206d89405f Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 5 Sep 2025 22:13:41 +0300 Subject: [PATCH 7/9] fix scrollToIndex viewOffset --- src/components/Search/SearchList/index.tsx | 2 +- src/components/Search/index.tsx | 16 +++++++++++++--- src/hooks/useSearchHighlightAndScroll.ts | 6 ++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index 4d81138def04..ed7abcf4b900 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -235,7 +235,7 @@ function SearchList( return; } - listRef.current.scrollToIndex({index, animated, viewOffset: variables.contentHeaderHeight}); + listRef.current.scrollToIndex({index, animated, viewOffset: -variables.contentHeaderHeight}); }, [data], ); diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index d3563e17d520..897930131d94 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,12 +1,12 @@ import {findFocusedRoute, useFocusEffect, useIsFocused, useNavigation} from '@react-navigation/native'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import SearchTableHeader, {getExpenseHeaders} from '@components/SelectionList/SearchTableHeader'; -import type {ReportActionListItemType, SearchListItem, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import type {ReportActionListItemType, SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay'; import useLocalize from '@hooks/useLocalize'; @@ -709,6 +709,16 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS [type, status, data, sortBy, sortOrder, groupBy, isChat, newSearchResultKey, selectedTransactions, canSelectMultiple, localeCompare, hash], ); + const searchListRef = useRef(null); + + useLayoutEffect(() => { + handleSelectionListScroll(sortedSelectedData, searchListRef.current); + + // We only need to run the effect on change of newSearchResultKey to scroll to the new item. + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newSearchResultKey]); + const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; useEffect(() => { @@ -833,7 +843,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS previousReportActionsIDs.length : transactionsIDs.length > previousTransactionsIDs.length; } @@ -166,7 +166,6 @@ function useSearchHighlightAndScroll({ setNewSearchResultKey(newReportActionKey); highlightedIDs.current.add(newReportActionID); - hasNewItemsRef.current = false; } else { const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults); const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); @@ -183,7 +182,6 @@ function useSearchHighlightAndScroll({ setNewSearchResultKey(newTransactionKey); highlightedIDs.current.add(newTransactionID); - hasNewItemsRef.current = false; } }, [searchResults?.data, previousSearchResults, isChat]); @@ -204,7 +202,7 @@ function useSearchHighlightAndScroll({ * Callback to handle scrolling to the new search result. */ const handleSelectionListScroll = useCallback( - (data: SearchListItem[]) => (ref: SelectionListHandle | null) => { + (data: SearchListItem[], ref: SelectionListHandle | null) => { // Early return if there's no ref, new transaction wasn't brought in by this hook // or there's no new search result key if (!ref || !triggeredByHookRef.current || newSearchResultKey === null) { From ee40b97410a64050a405a989d32d398075f2b95d Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 12 Sep 2025 23:40:38 +0300 Subject: [PATCH 8/9] minor update --- src/components/Search/SearchList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index 1d8d6f06da2c..f8092299c7d6 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -251,7 +251,7 @@ function SearchList( return; } - listRef.current.scrollToIndex({index, animated, viewOffset: -variables.contentHeaderHeight}); + listRef.current.scrollToIndex({index, animated, viewOffset: variables.contentHeaderHeight}); }, [data], ); From 41c3a89da006bc05e780a39710591b16a89a3e23 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 24 Sep 2025 22:00:04 +0300 Subject: [PATCH 9/9] fix bug of old items being highlighted --- src/components/Search/SearchList/index.tsx | 3 +-- src/hooks/useSearchHighlightAndScroll.ts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx index 450aeecdc780..527d94e455f4 100644 --- a/src/components/Search/SearchList/index.tsx +++ b/src/components/Search/SearchList/index.tsx @@ -38,7 +38,6 @@ import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import navigationRef from '@libs/Navigation/navigationRef'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction, TransactionViolations} from '@src/types/onyx'; @@ -267,7 +266,7 @@ function SearchList({ return; } - listRef.current.scrollToIndex({index, animated, viewOffset: variables.contentHeaderHeight}); + listRef.current.scrollToIndex({index, animated, viewPosition: 0}); }, [data], ); diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index 5a44125b9b9d..7b4ad5436ca1 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -44,7 +44,7 @@ function useSearchHighlightAndScroll({ // Ref to track if the search was triggered by this hook const triggeredByHookRef = useRef(false); const searchTriggeredRef = useRef(false); - const hasNewItemsRef = useRef(false); + const newTransactionIDsRef = useRef([]); const previousSearchResults = usePrevious(searchResults?.data); const [newSearchResultKey, setNewSearchResultKey] = useState(null); const highlightedIDs = useRef>(new Set()); @@ -126,8 +126,8 @@ function useSearchHighlightAndScroll({ // We only want to highlight new items if the addition of transactions or report actions triggered the search. // This is because, on deletion of items, the backend sometimes returns old items in place of the deleted ones. // We don't want to highlight these old items, even if they appear new in the current search results. - if ((!isChat && hasTransactionsIDsChange) || (isChat && hasReportActionsIDsChange)) { - hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionsIDs.length; + if (!isChat && hasTransactionsIDsChange) { + newTransactionIDsRef.current.push(...transactionsIDs.filter((id) => !previousTransactionsIDs.includes(id)).map((id) => id.replace(ONYXKEYS.COLLECTION.TRANSACTION, ''))); } // Set the flag indicating the search is triggered by the hook @@ -179,7 +179,7 @@ function useSearchHighlightAndScroll({ // 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)); - if (!triggeredByHookRef.current || newReportActionIDs.length === 0 || !hasNewItemsRef.current) { + if (!triggeredByHookRef.current || newReportActionIDs.length === 0) { return; } @@ -193,12 +193,16 @@ function useSearchHighlightAndScroll({ const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data); // 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)); + const newTransactionIDs = currentTransactionIDs.filter( + (id) => !previousTransactionIDs.includes(id) && !highlightedIDs.current.has(id) && newTransactionIDsRef.current.includes(id), + ); - if (!triggeredByHookRef.current || newTransactionIDs.length === 0 || !hasNewItemsRef.current) { + if (!triggeredByHookRef.current || newTransactionIDs.length === 0) { return; } + // As the new transactions are going to be highlighted below, which would make them obsolete, we will remove them from newTransactionIDsRef. + newTransactionIDsRef.current = newTransactionIDsRef.current.filter((id) => !newTransactionIDs.includes(id)); const newTransactionID = newTransactionIDs.at(0) ?? ''; const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${newTransactionID}`; @@ -256,7 +260,7 @@ function useSearchHighlightAndScroll({ }); // Early return if the new item is not found in the data array - if (indexOfNewItem <= 0) { + if (indexOfNewItem < 0) { return; }