-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Fix - Make it more apparent in the expense chat carousel view when expenses are added to a report #81446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix - Make it more apparent in the expense chat carousel view when expenses are added to a report #81446
Changes from all commits
965451f
679b291
361ddf8
940adc5
bd59c42
8708854
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import {useFocusEffect} from '@react-navigation/native'; | ||
| import React, {useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import {FlatList, View} from 'react-native'; | ||
| import type {ListRenderItemInfo, ViewToken} from 'react-native'; | ||
|
|
@@ -93,9 +94,12 @@ import EmptyMoneyRequestReportPreview from './EmptyMoneyRequestReportPreview'; | |
| import type {MoneyRequestReportPreviewContentProps} from './types'; | ||
|
|
||
| const reportAttributesSelector = (c: OnyxEntry<ReportAttributesDerivedValue>) => c?.reports; | ||
| const MAX_SCROLL_TO_INDEX_RETRIES = 5; | ||
| const SCROLL_TO_INDEX_RETRY_DELAY = 100; | ||
|
|
||
| function MoneyRequestReportPreviewContent({ | ||
| iouReportID, | ||
| newTransactionIDs, | ||
| chatReportID, | ||
| action, | ||
| containerStyles, | ||
|
|
@@ -432,7 +436,7 @@ function MoneyRequestReportPreviewContent({ | |
| thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBS_UP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBS_UP_DURATION})) : 1); | ||
| }, [isApproved, isApprovedAnimationRunning, thumbsUpScale]); | ||
|
|
||
| const carouselTransactions = shouldShowAccessPlaceHolder ? [] : transactions.slice(0, 11); | ||
| const carouselTransactions = useMemo(() => (shouldShowAccessPlaceHolder ? [] : transactions.slice(0, 11)), [shouldShowAccessPlaceHolder, transactions]); | ||
| const prevCarouselTransactionLength = useRef(0); | ||
|
|
||
| useEffect(() => { | ||
|
|
@@ -458,6 +462,47 @@ function MoneyRequestReportPreviewContent({ | |
| const viewabilityConfig = useMemo(() => { | ||
| return {itemVisiblePercentThreshold: 100}; | ||
| }, []); | ||
| const numberOfScrollToIndexFailed = useRef(0); | ||
| const onScrollToIndexFailed: (info: {index: number; highestMeasuredFrameIndex: number; averageItemLength: number}) => void = ({index}) => { | ||
| // There is a probability of infinite loop so we want to make sure that it is not called more than 5 times. | ||
| if (numberOfScrollToIndexFailed.current >= MAX_SCROLL_TO_INDEX_RETRIES) { | ||
| return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ CONSISTENCY-2 (docs)The hardcoded value Fix: Define a named constant: const MAX_SCROLL_TO_INDEX_RETRIES = 5; // Maximum number of retries before stopping
const onScrollToIndexFailed = ({index}: {index: number; highestMeasuredFrameIndex: number; averageItemLength: number}) => {
if (numberOfScrollToIndexFailed.current >= MAX_SCROLL_TO_INDEX_RETRIES) {
return;
}
// ... rest of the code
};Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| } | ||
|
|
||
| // Sometimes scrollToIndex might be called before the item is rendered so we will re-call scrollToIndex after a small delay. | ||
| setTimeout(() => { | ||
| carouselRef.current?.scrollToIndex({index, animated: true, viewOffset: 2 * styles.gap2.gap}); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ CONSISTENCY-2 (docs)The hardcoded value Fix: Define a named constant or use an existing constant from CONST: const SCROLL_TO_INDEX_RETRY_DELAY = 100; // milliseconds to wait before retrying scrollToIndex
const onScrollToIndexFailed = ({index}: {index: number; highestMeasuredFrameIndex: number; averageItemLength: number}) => {
if (numberOfScrollToIndexFailed.current > 4) {
return;
}
setTimeout(() => {
carouselRef.current?.scrollToIndex({index, animated: true, viewOffset: 2 * styles.gap2.gap});
}, SCROLL_TO_INDEX_RETRY_DELAY);
numberOfScrollToIndexFailed.current++;
};Or check if theres an appropriate constant in Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| }, SCROLL_TO_INDEX_RETRY_DELAY); | ||
| numberOfScrollToIndexFailed.current++; | ||
| }; | ||
|
|
||
| const carouselTransactionsRef = useRef(carouselTransactions); | ||
|
|
||
| useEffect(() => { | ||
| carouselTransactionsRef.current = carouselTransactions; | ||
| }, [carouselTransactions]); | ||
|
|
||
| useFocusEffect( | ||
| useCallback(() => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ PERF-2 (docs)The Fix: Convert to a Set for faster lookups: useFocusEffect(
useCallback(() => {
const newTransactionIDsSet = new Set(newTransactionIDs);
const index = carouselTransactions.findIndex((transaction) => newTransactionIDsSet.has(transaction.transactionID));
if (index < 0) {
return;
}
// ... rest of the code
}, [newTransactionIDs]),
);Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.has(transaction.transactionID)); | ||
|
|
||
| if (index < 0) { | ||
| return; | ||
| } | ||
| const newTransaction = carouselTransactions.at(index); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ PERFORMANCE-12 (docs)The Fix: Return a cleanup function from the callback that clears the timeout: useFocusEffect(
useCallback(() => {
const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.includes(transaction.transactionID));
if (index < 0) {
return;
}
const newTransaction = carouselTransactions.at(index);
const timeoutId = setTimeout(() => {
if (newTransaction?.transactionID !== carouselTransactionsRef.current.at(index)?.transactionID) {
return;
}
numberOfScrollToIndexFailed.current = 0;
carouselRef.current?.scrollToIndex({index, viewOffset: 2 * styles.gap2.gap, animated: true});
}, CONST.ANIMATED_TRANSITION);
return () => {
clearTimeout(timeoutId);
};
}, [newTransactionIDs]),
);Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| setTimeout(() => { | ||
| // If the new transaction is not available at the index it was on before the delay, avoid the scrolling | ||
| // because we are scrolling to either a wrong or unavailable transaction (which can cause crash). | ||
| if (newTransaction?.transactionID !== carouselTransactionsRef.current.at(index)?.transactionID) { | ||
| return; | ||
| } | ||
| numberOfScrollToIndexFailed.current = 0; | ||
| carouselRef.current?.scrollToIndex({index, viewOffset: 2 * styles.gap2.gap, animated: true}); | ||
| }, CONST.ANIMATED_TRANSITION); | ||
|
|
||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [newTransactionIDs]), | ||
| ); | ||
|
|
||
| const onViewableItemsChanged = useRef(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { | ||
| const newIndex = viewableItems.at(0)?.index; | ||
|
|
@@ -840,6 +885,7 @@ function MoneyRequestReportPreviewContent({ | |
| ) : ( | ||
| <View style={[styles.flex1, styles.flexColumn, styles.overflowVisible]}> | ||
| <FlatList | ||
| onScrollToIndexFailed={onScrollToIndexFailed} | ||
| snapToAlignment="start" | ||
| decelerationRate="fast" | ||
| snapToInterval={reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.gap2.gap} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| import {useIsFocused} from '@react-navigation/native'; | ||
| import React, {useCallback, useMemo, useRef, useState} from 'react'; | ||
| import type {LayoutChangeEvent, ListRenderItem} from 'react-native'; | ||
| import {usePersonalDetails} from '@components/OnyxListItemProvider'; | ||
| import TransactionPreview from '@components/ReportActionItem/TransactionPreview'; | ||
| import useNewTransactions from '@hooks/useNewTransactions'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import usePolicy from '@hooks/usePolicy'; | ||
| import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
|
|
@@ -119,6 +122,11 @@ function MoneyRequestReportPreview({ | |
| Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()})); | ||
| } | ||
| }, [iouReportID, isSmallScreenWidth]); | ||
| const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`, {canBeMissing: true}); | ||
| const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, transactions); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ PERF-13 (docs)The Fix: Wrap in const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, transactions);
const newTransactionIDs = useMemo(() => newTransactions.map((transaction) => transaction.transactionID), [newTransactions]);Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| const isFocused = useIsFocused(); | ||
| // We only want to highlight the new expenses if the screen is focused. | ||
| const newTransactionIDs = isFocused ? new Set(newTransactions.map((transaction) => transaction.transactionID)) : undefined; | ||
|
|
||
| const renderItem: ListRenderItem<Transaction> = ({item}) => ( | ||
| <TransactionPreview | ||
|
|
@@ -140,11 +148,13 @@ function MoneyRequestReportPreview({ | |
| reportPreviewAction={action} | ||
| onPreviewPressed={openReportFromPreview} | ||
| shouldShowPayerAndReceiver={shouldShowPayerAndReceiver} | ||
| shouldHighlight={newTransactionIDs?.has(item.transactionID)} | ||
| /> | ||
| ); | ||
|
|
||
| return ( | ||
| <MoneyRequestReportPreviewContent | ||
| newTransactionIDs={newTransactionIDs} | ||
| iouReportID={iouReportID} | ||
| chatReportID={chatReportID} | ||
| iouReport={iouReport} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ PERFORMANCE-12 (docs)
The
setTimeoutcall inonScrollToIndexFailedis not cleared when the component unmounts, which can cause memory leaks if the component is unmounted before the timeout fires.Fix: Store the timeout ID and clear it in a cleanup function:
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.