Skip to content

Commit 1465400

Browse files
authored
Merge pull request Expensify#89525 from software-mansion-labs/borys3kk-fix-only-first-expense-is-highlighted
Reports - Expense created while on "Reports", gets highlighted again when opened.
2 parents af304c0 + df8cc2c commit 1465400

3 files changed

Lines changed: 31 additions & 35 deletions

File tree

src/components/Search/index.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ function Search({
450450
// eslint-disable-next-line react-hooks/exhaustive-deps
451451
}, [isSmallScreenWidth]);
452452

453-
const {newSearchResultKeys, handleSelectionListScroll, newTransactions} = useSearchHighlightAndScroll({
453+
const {newSearchResultKeys, handleSelectionListScroll, newTransactions, hasQueuedHighlights} = useSearchHighlightAndScroll({
454454
searchResults,
455455
transactions,
456456
previousTransactions,
@@ -463,6 +463,16 @@ function Search({
463463
shouldUseLiveData,
464464
});
465465

466+
// Mirror `hasQueuedHighlights` into a ref so the post-create-flow `useFocusEffect`
467+
// (which has empty deps) can read the latest value without re-creating its callback.
468+
// Used to skip the deferral that would otherwise hide the freshly-added row from
469+
// FlashList during the RHP dismiss transition, which would prevent the highlight
470+
// animation from ever firing on it.
471+
const hasQueuedHighlightsRef = useRef(hasQueuedHighlights);
472+
useEffect(() => {
473+
hasQueuedHighlightsRef.current = hasQueuedHighlights;
474+
}, [hasQueuedHighlights]);
475+
466476
// 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
467477
// we also need to check that the searchResults matches the type and status of the current search
468478
const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON);
@@ -518,6 +528,14 @@ function Search({
518528
return;
519529
}
520530

531+
// If the highlight hook already queued rows for the post-create animation,
532+
// skip the skeleton-during-transition defer. Otherwise FlashList stays empty
533+
// for ~1s while the RHP dismiss transition runs, the row never mounts inside
534+
// the 300ms highlight window, and `useAnimatedHighlightStyle` never fires.
535+
if (hasQueuedHighlightsRef.current) {
536+
return;
537+
}
538+
521539
// Show skeleton while the RHP dismiss animation plays. The transition
522540
// hasn't started yet when useFocusEffect fires (it begins after paint),
523541
// so waitForUpcomingTransition defers until the animation actually ends.

src/hooks/useSearchHighlightAndScroll.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {SearchKey} from '@libs/SearchUIUtils';
1414
import CONST from '@src/CONST';
1515
import ONYXKEYS from '@src/ONYXKEYS';
1616
import type {ReportActions, SearchResults, Transaction} from '@src/types/onyx';
17-
import {isEmptyObject} from '@src/types/utils/EmptyObject';
1817
import useNetwork from './useNetwork';
1918
import useOnyx from './useOnyx';
2019
import usePrevious from './usePrevious';
@@ -227,40 +226,23 @@ function useSearchHighlightAndScroll({
227226
}
228227

229228
const newKeys = new Set<string>();
229+
const consumedManualIDs: string[] = [];
230230
for (const id of newTransactionIDs) {
231231
const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${id}`;
232232
highlightedIDs.current.add(newTransactionKey);
233233
newKeys.add(newTransactionKey);
234+
if (manualHighlightTransactionIDs.has(id)) {
235+
consumedManualIDs.push(id);
236+
}
234237
}
235238
setNewSearchResultKeys(newKeys);
236-
}
237-
}, [searchResults?.data, previousSearchResults, isChat, transactionIDsToHighlight]);
238239

239-
// Reset transactionIDsToHighlight after they have been highlighted
240-
useEffect(() => {
241-
if (isEmptyObject(transactionIDsToHighlight) || newSearchResultKeys === null) {
242-
return;
240+
// Clear consumed manual highlight flags so subsequent detect runs don't re-highlight the same IDs.
241+
if (consumedManualIDs.length > 0) {
242+
mergeTransactionIdsHighlightOnSearchRoute(queryJSON.type, Object.fromEntries(consumedManualIDs.map((id) => [id, false])));
243+
}
243244
}
244-
245-
const highlightedTransactionIDs = Object.keys(transactionIDsToHighlight).filter(
246-
(id) => transactionIDsToHighlight[id] && newSearchResultKeys?.has(`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`),
247-
);
248-
249-
// We need to use requestAnimationFrame here to ensure that setTimeout actually starts
250-
// only after the user has navigated to the "Reports > Expenses" page.
251-
// Otherwise, there is still a chance we might miss the timing because setTimeout runs too early,
252-
// causing the highlight not to appear.
253-
let timer: NodeJS.Timeout;
254-
const animation = requestAnimationFrame(() => {
255-
timer = setTimeout(() => {
256-
mergeTransactionIdsHighlightOnSearchRoute(queryJSON.type, Object.fromEntries(highlightedTransactionIDs.map((id) => [id, false])));
257-
}, CONST.ANIMATED_HIGHLIGHT_START_DURATION);
258-
});
259-
return () => {
260-
clearTimeout(timer);
261-
cancelAnimationFrame(animation);
262-
};
263-
}, [transactionIDsToHighlight, queryJSON.type, newSearchResultKeys]);
245+
}, [searchResults?.data, previousSearchResults, isChat, transactionIDsToHighlight, queryJSON.type]);
264246

265247
// Remove transactionIDsToHighlight when the user leaves the current search type
266248
useEffect(
@@ -329,7 +311,9 @@ function useSearchHighlightAndScroll({
329311
triggeredByHookRef.current = false;
330312
};
331313

332-
return {newSearchResultKeys, handleSelectionListScroll, newTransactions};
314+
const hasQueuedHighlights = newSearchResultKeys !== null && newSearchResultKeys.size > 0;
315+
316+
return {newSearchResultKeys, handleSelectionListScroll, newTransactions, hasQueuedHighlights};
333317
}
334318

335319
/**

tests/unit/useSearchHighlightAndScrollTest.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,6 @@ describe('useSearchHighlightAndScroll', () => {
332332
expect(result.current.newSearchResultKeys?.size).toBe(1);
333333
expect([...(result.current.newSearchResultKeys ?? new Set())]).toContain('transactions_3');
334334

335-
// Wait 1s for the timer in useSearchHighlightAndScroll to complete.
336-
await new Promise((resolve) => {
337-
setTimeout(resolve, 1000);
338-
});
339-
340-
expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledTimes(1);
341335
expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledWith(baseProps.queryJSON.type, {'3': false});
342336
});
343337

0 commit comments

Comments
 (0)