Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Expand Down Expand Up @@ -95,6 +96,7 @@ const reportAttributesSelector = (c: OnyxEntry<ReportAttributesDerivedValue>) =>

function MoneyRequestReportPreviewContent({
iouReportID,
newTransactionIDs,
chatReportID,
action,
containerStyles,
Expand Down Expand Up @@ -414,7 +416,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(() => {
Expand All @@ -440,6 +442,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 > 4) {
return;
}

// 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});
}, 100);
numberOfScrollToIndexFailed.current++;
};

const carouselTransactionsRef = useRef(carouselTransactions);

useEffect(() => {
carouselTransactionsRef.current = carouselTransactions;
}, [carouselTransactions]);

useFocusEffect(
useCallback(() => {
const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.includes(transaction.transactionID));

if (index < 0) {
return;
}
const newTransaction = carouselTransactions.at(index);
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-compiler/react-compiler, react-hooks/exhaustive-deps
}, [newTransactionIDs]),
);

// eslint-disable-next-line react-compiler/react-compiler
const onViewableItemsChanged = useRef(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
Expand Down Expand Up @@ -807,6 +850,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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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';
Expand Down Expand Up @@ -112,6 +114,9 @@ function MoneyRequestReportPreview({
});
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
}, [iouReportID]);
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`, {canBeMissing: true});
const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, transactions);
const newTransactionIDs = newTransactions.map((transaction) => transaction.transactionID);

const renderItem: ListRenderItem<Transaction> = ({item}) => (
<TransactionPreview
Expand All @@ -133,11 +138,13 @@ function MoneyRequestReportPreview({
reportPreviewAction={action}
onPreviewPressed={openReportFromPreview}
shouldShowPayerAndReceiver={shouldShowPayerAndReceiver}
shouldHighlight={newTransactionIDs.includes(item.transactionID)}
/>
);

return (
<MoneyRequestReportPreviewContent
newTransactionIDs={newTransactionIDs}
iouReportID={iouReportID}
chatReportID={chatReportID}
iouReport={iouReport}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ type MoneyRequestReportPreviewContentProps = MoneyRequestReportPreviewContentOny

/** Callback called when the whole preview is pressed */
onPress: () => void;

/** IDs of newly added transactions */
newTransactionIDs?: string[];
};

export type {MoneyRequestReportPreviewContentProps, MoneyRequestReportPreviewProps, MoneyRequestReportPreviewStyleType};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import truncate from 'lodash/truncate';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import Animated from 'react-native-reanimated';
import Button from '@components/Button';
import Icon from '@components/Icon';
// eslint-disable-next-line no-restricted-imports
Expand All @@ -11,6 +12,7 @@ import ReportActionItemImages from '@components/ReportActionItem/ReportActionIte
import UserInfoCellsWithArrow from '@components/SelectionListWithSections/Search/UserInfoCellsWithArrow';
import Text from '@components/Text';
import TransactionPreviewSkeletonView from '@components/TransactionPreviewSkeletonView';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
Expand Down Expand Up @@ -62,6 +64,7 @@ function TransactionPreviewContent({
shouldShowPayerAndReceiver,
navigateToReviewFields,
isReviewDuplicateTransactionPage = false,
shouldHighlight = false,
}: TransactionPreviewContentProps) {
const icons = useMemoizedLazyExpensifyIcons(['Folder', 'Tag']);
const theme = useTheme();
Expand Down Expand Up @@ -218,10 +221,17 @@ function TransactionPreviewContent({
const previewTextViewGap = (shouldShowCategoryOrTag || !shouldWrapDisplayAmount) && styles.gap2;
const previewTextMargin = shouldShowIOUHeader && shouldShowMerchantOrDescription && !isBillSplit && !shouldShowCategoryOrTag && styles.mbn1;

const animatedHighlightStyle = useAnimatedHighlightStyle({
shouldHighlight,
highlightColor: theme.messageHighlightBG,
backgroundColor: theme.cardBG,
shouldApplyOtherStyles: false,
});

const transactionWrapperStyles = [styles.border, styles.moneyRequestPreviewBox, (isIOUSettled || isApproved) && isSettlementOrApprovalPartial && styles.offlineFeedbackPending];

return (
<View style={[transactionWrapperStyles, containerStyles]}>
<Animated.View style={[transactionWrapperStyles, containerStyles, animatedHighlightStyle]}>
<OfflineWithFeedback
errors={walletTermsErrors}
onClose={() => offlineWithFeedbackOnClose}
Expand All @@ -232,7 +242,7 @@ function TransactionPreviewContent({
shouldDisableOpacity={isDeleted}
shouldHideOnDelete={shouldHideOnDelete}
>
<View style={[(isTransactionScanning || isWhisper) && [styles.reportPreviewBoxHoverBorder, styles.reportContainerBorderRadius]]}>
<View style={[(isTransactionScanning || isWhisper) && [styles.reportPreviewBoxHoverBorderColor, styles.reportContainerBorderRadius]]}>
<ReportActionItemImages
images={receiptImages}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
Expand Down Expand Up @@ -397,7 +407,7 @@ function TransactionPreviewContent({
)}
</View>
</OfflineWithFeedback>
</View>
</Animated.View>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function TransactionPreview(props: TransactionPreviewProps) {
iouReportID,
transactionID: transactionIDFromProps,
onPreviewPressed,
shouldHighlight,
reportPreviewAction,
contextAction,
} = props;
Expand Down Expand Up @@ -130,6 +131,7 @@ function TransactionPreview(props: TransactionPreviewProps) {
walletTermsErrors={walletTerms?.errors}
routeName={route.name}
isReviewDuplicateTransactionPage={isReviewDuplicateTransactionPage}
shouldHighlight={shouldHighlight}
/>
</PressableWithoutFeedback>
);
Expand All @@ -154,6 +156,7 @@ function TransactionPreview(props: TransactionPreviewProps) {
walletTermsErrors={walletTerms?.errors}
routeName={route.name}
reportPreviewAction={reportPreviewAction}
shouldHighlight={shouldHighlight}
isReviewDuplicateTransactionPage={isReviewDuplicateTransactionPage}
/>
);
Expand Down
6 changes: 6 additions & 0 deletions src/components/ReportActionItem/TransactionPreview/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ type TransactionPreviewProps = {

/** In case we want to override context menu action */
contextAction?: OnyxEntry<ReportAction>;

/** Whether the item should be highlighted */
shouldHighlight?: boolean;
};

type TransactionPreviewContentProps = {
Expand Down Expand Up @@ -141,6 +144,9 @@ type TransactionPreviewContentProps = {

/** Is this component used during duplicate review flow */
isReviewDuplicateTransactionPage?: boolean;

/** Whether the item should be highlighted */
shouldHighlight?: boolean;
};

export type {TransactionPreviewContentProps, TransactionPreviewProps, TransactionPreviewStyleType};
7 changes: 5 additions & 2 deletions src/hooks/useAnimatedHighlightStyle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type Props = {
/** Whether the item should be highlighted */
shouldHighlight: boolean;

/** Whether it should return height and border radius styles */
shouldApplyOtherStyles?: boolean;

/** The base backgroundColor used for the highlight animation, defaults to theme.appBG
* @default theme.appBG
*/
Expand Down Expand Up @@ -63,6 +66,7 @@ export default function useAnimatedHighlightStyle({
height,
highlightColor,
backgroundColor,
shouldApplyOtherStyles = true,
skipInitialFade = false,
}: Props) {
const [startHighlight, setStartHighlight] = useState(false);
Expand All @@ -80,9 +84,8 @@ export default function useAnimatedHighlightStyle({

return {
backgroundColor: interpolateColor(repeatableValue, [0, 1], [backgroundColor ?? theme.appBG, highlightColor ?? theme.border]),
height: height ? interpolate(nonRepeatableValue, [0, 1], [0, height]) : 'auto',
opacity: interpolate(nonRepeatableValue, [0, 1], [0, 1]),
borderRadius,
...(shouldApplyOtherStyles && {height: height ? interpolate(nonRepeatableValue, [0, 1], [0, height]) : 'auto', borderRadius}),
};
}, [borderRadius, height, backgroundColor, highlightColor, theme.appBG, theme.border]);

Expand Down
4 changes: 4 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4246,6 +4246,10 @@ const staticStyles = (theme: ThemeColors) =>
backgroundColor: theme.cardBG,
},

reportPreviewBoxHoverBorderColor: {
borderColor: theme.cardBG,
},

reportContainerBorderRadius: {
borderRadius: variables.componentBorderRadiusLarge,
},
Expand Down
Loading