Skip to content

Commit 6601eb3

Browse files
authored
Merge pull request Expensify#83925 from FitseTLT/fix-carousel-auto-scroll-problem
Fix carousel auto scroll problem
2 parents 29f76b9 + 5e3edab commit 6601eb3

14 files changed

Lines changed: 188 additions & 14 deletions

File tree

src/CONST/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ const CONST = {
281281
ANIMATION_PAID_BUTTON_HIDE_DELAY: 300,
282282
BACKGROUND_IMAGE_TRANSITION_DURATION: 1000,
283283
SCREEN_TRANSITION_END_TIMEOUT: 1000,
284+
PENDING_TRANSACTION_DELETION_DELAY: 4000,
285+
PENDING_TRANSACTION_SCROLL_DELAY: 1000,
284286

285287
// Delay before pre-inserting the Search fullscreen route under the RHP on the confirmation screen.
286288
// Chosen to be long enough for the RHP entrance animation to complete (~250ms) and avoid jank

src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ function MoneyRequestReportPreviewContent({
436436
viewOffset: -2 * styles.gap2.gap,
437437
animated: true,
438438
});
439-
}, CONST.ANIMATED_TRANSITION);
439+
}, CONST.PENDING_TRANSACTION_SCROLL_DELAY);
440440

441441
// We only want to scroll to a new transaction when the set of new transaction IDs changes.
442442
// eslint-disable-next-line react-hooks/exhaustive-deps

src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {useIsFocused} from '@react-navigation/core';
12
import type {ListRenderItem} from '@shopify/flash-list';
23
import React, {useCallback, useMemo, useRef, useState} from 'react';
34
import type {LayoutChangeEvent} from 'react-native';
@@ -20,7 +21,7 @@ import {contextMenuRef} from '@pages/inbox/report/ContextMenu/ReportActionContex
2021
import CONST from '@src/CONST';
2122
import ONYXKEYS from '@src/ONYXKEYS';
2223
import ROUTES from '@src/ROUTES';
23-
import {hasOnceLoadedReportActionsSelector} from '@src/selectors/ReportMetaData';
24+
import {hasOnceLoadedReportActionsSelector, pendingNewTransactionIDsSelector} from '@src/selectors/ReportMetaData';
2425
import type {Transaction} from '@src/types/onyx';
2526
import MoneyRequestReportPreviewContent from './MoneyRequestReportPreviewContent';
2627
import type {MoneyRequestReportPreviewProps} from './types';
@@ -120,7 +121,11 @@ function MoneyRequestReportPreview({
120121
const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`, {
121122
selector: hasOnceLoadedReportActionsSelector,
122123
});
123-
const newTransactions = useNewTransactions(hasOnceLoadedReportActions, transactions);
124+
const [pendingNewTransactionIDs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`, {
125+
selector: pendingNewTransactionIDsSelector,
126+
});
127+
const isFocused = useIsFocused();
128+
const newTransactions = useNewTransactions(hasOnceLoadedReportActions, transactions, pendingNewTransactionIDs, chatReportID, isFocused);
124129
const newTransactionIDs = new Set(newTransactions.map((transaction) => transaction.transactionID));
125130

126131
const transactionPreviewContainerStyles = [styles.h100, reportPreviewStyles.transactionPreviewCarouselStyle];

src/hooks/useNewTransactions.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,77 @@
11
import {useEffect, useMemo, useRef} from 'react';
2+
import {deletePendingNewTransactionIDs} from '@libs/actions/IOU/PendingNewTransactions';
23
import CONST from '@src/CONST';
34
import type {Transaction} from '@src/types/onyx';
5+
import {isEmptyObject} from '@src/types/utils/EmptyObject';
46
import usePrevious from './usePrevious';
57

68
/**
79
* This hook returns new transactions that have been added since the last transactions update.
810
* This hook should be used only in the context of highlighting the new transactions on the Report table view.
11+
*
12+
* When `pendingNewTransactionIDs` is provided, those transactions will be treated as new even on the
13+
* first load. This handles the case where a transaction was created before the component mounts
14+
* (e.g., submitting a tracked expense from Self DM to a workspace on Web).
915
*/
10-
function useNewTransactions(hasOnceLoadedReportActions: boolean | undefined, transactions: Transaction[] | undefined) {
11-
// If we haven't loaded report yet we set previous transactions to undefined.
16+
function useNewTransactions(
17+
hasOnceLoadedReportActions: boolean | undefined,
18+
transactions: Transaction[] | undefined,
19+
pendingNewTransactionIDs?: Record<string, true | null>,
20+
reportID?: string,
21+
isFocused?: boolean,
22+
) {
23+
// If we haven't loaded report yet we set previous transaction ids to undefined.
1224
const prevTransactions = usePrevious(hasOnceLoadedReportActions ? transactions : undefined);
1325

1426
// We need to skip the first transactions change, to avoid highlighting transactions on the first load.
1527
const skipFirstTransactionsChange = useRef(!hasOnceLoadedReportActions);
1628

1729
const newTransactions = useMemo(() => {
30+
// If isFocused is not passed (=undefined) we will not return empty.
31+
if (isFocused === false) {
32+
return CONST.EMPTY_ARRAY as unknown as Transaction[];
33+
}
34+
1835
if (transactions === undefined || prevTransactions === undefined || transactions.length <= prevTransactions.length) {
36+
// When a transaction is submitted from another report (e.g., Self DM → workspace), it is
37+
// already in the transactions list by the time this component mounts.
38+
// So we use pendingNewTransactionIDs from report metadata to identify these transactions on first load.
39+
if (isFocused && reportID && !isEmptyObject(pendingNewTransactionIDs) && transactions?.length) {
40+
const pendingSet = new Set(Object.keys(pendingNewTransactionIDs));
41+
const pendingTransactions = transactions.filter(({transactionID}) => pendingSet.has(transactionID) && pendingNewTransactionIDs[transactionID]);
42+
return pendingTransactions;
43+
}
1944
return CONST.EMPTY_ARRAY as unknown as Transaction[];
2045
}
2146
if (skipFirstTransactionsChange.current) {
2247
skipFirstTransactionsChange.current = false;
2348
return CONST.EMPTY_ARRAY as unknown as Transaction[];
2449
}
2550
return transactions.filter((transaction) => !prevTransactions?.some((prevTransaction) => prevTransaction.transactionID === transaction.transactionID));
26-
}, [transactions, prevTransactions]);
51+
52+
// We don't need to recalculate on change of prevTransactions or pendingNewTransactionIDs as it will make the value
53+
// disappear quickly which will break the scroll and highlight on slower devices like mobile app.
54+
// eslint-disable-next-line react-hooks/exhaustive-deps
55+
}, [transactions, reportID, isFocused]);
56+
57+
useEffect(() => {
58+
if (!pendingNewTransactionIDs) {
59+
return;
60+
}
61+
const pendingSet = new Set(Object.keys(pendingNewTransactionIDs));
62+
const pendingTransactions = newTransactions.filter(({transactionID}) => pendingSet.has(transactionID) && pendingNewTransactionIDs[transactionID]);
63+
if (!pendingTransactions.length) {
64+
return;
65+
}
66+
67+
// We deletePendingNewTransactionIDs after the scroll and highlight has occurred.
68+
setTimeout(() => {
69+
deletePendingNewTransactionIDs(
70+
reportID,
71+
pendingTransactions.map((transaction) => transaction.transactionID),
72+
);
73+
}, CONST.PENDING_TRANSACTION_DELETION_DELAY);
74+
}, [pendingNewTransactionIDs, newTransactions, reportID]);
2775

2876
// In case when we have loaded the report, but there were no transactions in it, then we need to explicitly set skipFirstTransactionsChange to false, as it will be not set in the useMemo above.
2977
useEffect(() => {

src/libs/Navigation/helpers/navigateAfterExpenseCreate.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {addPendingNewTransactionIDs} from '@libs/actions/IOU/PendingNewTransactions';
12
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
23
import Log from '@libs/Log';
34
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
@@ -17,6 +18,7 @@ type NavigateAfterExpenseCreateParams = {
1718
isFromGlobalCreate?: boolean;
1819
isInvoice?: boolean;
1920
hasMultipleTransactions: boolean;
21+
shouldAddPendingNewTransactionIDs?: boolean;
2022
};
2123

2224
/**
@@ -26,14 +28,24 @@ type NavigateAfterExpenseCreateParams = {
2628
* - If it is created on the inbox tab, it will open the chat report containing that expense.
2729
* - If it is created elsewhere, it will navigate to Reports > Expense and highlight the newly created expense.
2830
*/
29-
function navigateAfterExpenseCreate({activeReportID, transactionID, isFromGlobalCreate, isInvoice, hasMultipleTransactions}: NavigateAfterExpenseCreateParams) {
31+
function navigateAfterExpenseCreate({
32+
activeReportID,
33+
transactionID,
34+
isFromGlobalCreate,
35+
isInvoice,
36+
hasMultipleTransactions,
37+
shouldAddPendingNewTransactionIDs = false,
38+
}: NavigateAfterExpenseCreateParams) {
3039
const isUserOnInbox = isReportTopmostSplitNavigator();
3140

3241
// If the expense is not created from global create or is currently on the inbox tab,
3342
// we just need to dismiss the money request flow screens
3443
// and open the report chat containing the IOU report
3544
if (!isFromGlobalCreate || isUserOnInbox || !transactionID) {
3645
dismissModalAndOpenReportInInboxTab(activeReportID, isInvoice, hasMultipleTransactions);
46+
if (shouldAddPendingNewTransactionIDs) {
47+
addPendingNewTransactionIDs(activeReportID, transactionID);
48+
}
3749
return;
3850
}
3951

src/libs/actions/IOU/NavigationHelpers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,16 @@ function handleNavigateAfterExpenseCreate({
3939
transactionID,
4040
isFromGlobalCreate,
4141
isInvoice,
42+
shouldAddPendingNewTransactionIDs = false,
4243
}: {
4344
activeReportID?: string;
4445
transactionID?: string;
4546
isFromGlobalCreate?: boolean;
4647
isInvoice?: boolean;
48+
shouldAddPendingNewTransactionIDs?: boolean;
4749
}) {
4850
const hasMultipleTransactions = Object.values(getAllTransactions()).filter((transaction) => transaction?.reportID === activeReportID).length > 0;
49-
navigateAfterExpenseCreate({activeReportID, transactionID, isFromGlobalCreate, isInvoice, hasMultipleTransactions});
51+
navigateAfterExpenseCreate({activeReportID, transactionID, isFromGlobalCreate, isInvoice, hasMultipleTransactions, shouldAddPendingNewTransactionIDs});
5052
}
5153

5254
export {dismissModalAndOpenReportInInboxTab, handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Onyx from 'react-native-onyx';
2+
import ONYXKEYS from '@src/ONYXKEYS';
3+
4+
function addPendingNewTransactionIDs(reportID: string | undefined, transactionID: string | undefined) {
5+
if (!reportID || !transactionID) {
6+
return;
7+
}
8+
9+
// We are saving in object form so that consecutive onyx merge will not reset previous value.
10+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {pendingNewTransactionIDs: {[transactionID]: true}});
11+
}
12+
13+
function deletePendingNewTransactionIDs(reportID: string | undefined, transactionIDs: string[]) {
14+
if (!reportID) {
15+
return;
16+
}
17+
18+
const pendingNewTransactionIDs: Record<string, null> = {};
19+
for (const transactionID of transactionIDs) {
20+
Object.assign(pendingNewTransactionIDs, {[transactionID]: null});
21+
}
22+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {pendingNewTransactionIDs});
23+
}
24+
25+
export {addPendingNewTransactionIDs, deletePendingNewTransactionIDs};

src/libs/actions/IOU/PerDiem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf
10151015
notifyNewAction(activeReportID, undefined, participantParams.payeeAccountID === currentUserAccountIDParam);
10161016
}
10171017

1018-
return {iouReport};
1018+
return {iouReport, transactionID: transaction.transactionID};
10191019
}
10201020

10211021
/**

src/libs/actions/IOU/Split.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2154,8 +2154,14 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest
21542154
const activeReportID = isMoneyRequestReport && report?.reportID ? report.reportID : parameters.chatReportID;
21552155

21562156
if (shouldHandleNavigation) {
2157+
const navigationActiveReportID = backToReport ?? activeReportID;
21572158
highlightTransactionOnSearchRouteIfNeeded(isFromGlobalCreate, parameters.transactionID, CONST.SEARCH.DATA_TYPES.EXPENSE);
2158-
handleNavigateAfterExpenseCreate({activeReportID: backToReport ?? activeReportID, isFromGlobalCreate, transactionID: parameters.transactionID});
2159+
handleNavigateAfterExpenseCreate({
2160+
activeReportID: navigationActiveReportID,
2161+
isFromGlobalCreate,
2162+
transactionID: parameters.transactionID,
2163+
shouldAddPendingNewTransactionIDs: navigationActiveReportID === parameters.chatReportID,
2164+
});
21592165
}
21602166

21612167
if (!isMoneyRequestReport) {

src/libs/actions/IOU/TrackExpense.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,12 +1883,13 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep
18831883

18841884
if (!requestMoneyInformation.isRetry) {
18851885
highlightTransactionOnSearchRouteIfNeeded(isFromGlobalCreate, transaction.transactionID, CONST.SEARCH.DATA_TYPES.EXPENSE);
1886-
18871886
if (shouldHandleNavigation) {
1887+
const navigationReportID = backToReport ?? activeReportID;
18881888
handleNavigateAfterExpenseCreate({
1889-
activeReportID: backToReport ?? activeReportID,
1889+
activeReportID: navigationReportID,
18901890
transactionID: transaction.transactionID,
18911891
isFromGlobalCreate,
1892+
shouldAddPendingNewTransactionIDs: navigationReportID === chatReport.reportID,
18921893
});
18931894
}
18941895
}
@@ -2689,6 +2690,7 @@ function trackExpense(params: CreateTrackExpenseParams) {
26892690
activeReportID,
26902691
transactionID: transaction?.transactionID,
26912692
isFromGlobalCreate,
2693+
shouldAddPendingNewTransactionIDs: action === CONST.IOU.ACTION.CATEGORIZE || action === CONST.IOU.ACTION.SHARE,
26922694
});
26932695
}
26942696
}

0 commit comments

Comments
 (0)