diff --git a/src/libs/NavigationFocusReturn.ts b/src/libs/NavigationFocusReturn.ts index 9c461fac87be..d90198ca756a 100644 --- a/src/libs/NavigationFocusReturn.ts +++ b/src/libs/NavigationFocusReturn.ts @@ -1,13 +1,12 @@ import {findFocusedRoute} from '@react-navigation/core'; import type {NavigationState, PartialState} from '@react-navigation/native'; -// eslint-disable-next-line no-restricted-imports -- idiomatic defer primitive past navigation transitions. -import {InteractionManager} from 'react-native'; import compoundParamsKey, {COMPOUND_KEY_DELIMITER} from './compoundParamsKey'; import FOCUSABLE_SELECTOR from './focusableSelector'; import hasFocusableAttributes from './focusGuards'; import getHadTabNavigation from './hadTabNavigation'; import {consumeLauncher, pickLauncher, resetLauncherStackForTests} from './LauncherStack'; import navigationRef from './Navigation/navigationRef'; +import TransitionTracker from './Navigation/TransitionTracker'; import {isCycleIdle, Priorities, resetCycle, tryClaim} from './ScreenFocusArbiter'; /** focusin tracks the last keyboard-focused element; a nav state listener captures it against the outgoing route and restores it on backward nav. */ @@ -318,28 +317,29 @@ function scheduleRestore(routeKey: string): void { const attempt = () => { // Defer past the transition so useAutoFocusInput and React Navigation's own focus work settle first. - // eslint-disable-next-line @typescript-eslint/no-deprecated -- idiomatic defer primitive despite type-def deprecation. - imHandle = InteractionManager.runAfterInteractions(() => { - if (cancelled) { - return; - } - frameId = requestAnimationFrame(() => { + imHandle = TransitionTracker.runAfterTransitions({ + callback: () => { if (cancelled) { return; } - attempts += 1; - const restored = restoreTriggerForRoute(routeKey); - if (restored || !triggerMap.has(routeKey)) { - pendingRestore = null; - return; - } - if (attempts >= MAX_RESTORE_ATTEMPTS) { - triggerMap.delete(routeKey); - pendingRestore = null; - return; - } - retryTimerId = setTimeout(attempt, RESTORE_RETRY_MS); - }); + frameId = requestAnimationFrame(() => { + if (cancelled) { + return; + } + attempts += 1; + const restored = restoreTriggerForRoute(routeKey); + if (restored || !triggerMap.has(routeKey)) { + pendingRestore = null; + return; + } + if (attempts >= MAX_RESTORE_ATTEMPTS) { + triggerMap.delete(routeKey); + pendingRestore = null; + return; + } + retryTimerId = setTimeout(attempt, RESTORE_RETRY_MS); + }); + }, }); }; diff --git a/src/libs/actions/replaceOptimisticReportWithActualReport.ts b/src/libs/actions/replaceOptimisticReportWithActualReport.ts index 0bb103ce149f..ce5be7f99c43 100644 --- a/src/libs/actions/replaceOptimisticReportWithActualReport.ts +++ b/src/libs/actions/replaceOptimisticReportWithActualReport.ts @@ -1,8 +1,8 @@ -// eslint-disable-next-line no-restricted-imports -import {DeviceEventEmitter, InteractionManager} from 'react-native'; +import {DeviceEventEmitter} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {isMoneyRequest, isMoneyRequestReport, isOneTransactionReport} from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -83,126 +83,136 @@ function replaceOptimisticReportWithActualReport(report: Report, draftReportComm } } - InteractionManager.runAfterInteractions(() => { - // It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists. - // Or we optimistically created a transaction thread chat report for an IOU report action that already has an associated child chat report. - // Or we optimistically created a thread report under a comment that already has an associated child chat report. - // In this case, the API will let us know by returning a preexistingReportID. - // We should clear out the optimistically created report and re-route the user to the preexisting report. - const existingReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`]; - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; - let callback = () => { - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); + TransitionTracker.runAfterTransitions({ + callback: () => { + // It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists. + // Or we optimistically created a transaction thread chat report for an IOU report action that already has an associated child chat report. + // Or we optimistically created a thread report under a comment that already has an associated child chat report. + // In this case, the API will let us know by returning a preexistingReportID. + // We should clear out the optimistically created report and re-route the user to the preexisting report. + const existingReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; + let callback = () => { + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); - if (!parentReportActionID) { - // DMs and group-DMs don't have parent actions, so we only need to merge report data and preserve existing participants - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, { - ...report, - reportID: preexistingReportID, - preexistingReportID: null, - // Replacing the existing report's participants to avoid duplicates - participants: existingReport?.participants ?? report.participants, - }); - } else { - // Thread reports have parent actions that need their childReportID updated to point to the preexisting thread - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, { - ...report, - reportID: preexistingReportID, - preexistingReportID: null, - }); - // Non-optimistic parent actions already exist, so we update their childReportID; - // optimistic actions were already cleaned up above - const parentReportAction = parentReportID ? allReportActions?.[parentReportID]?.[parentReportActionID] : null; - if (parentReportAction && !parentReportAction.isOptimisticAction) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { - [parentReportActionID]: {childReportID: preexistingReportID}, + if (!parentReportActionID) { + // DMs and group-DMs don't have parent actions, so we only need to merge report data and preserve existing participants + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, { + ...report, + reportID: preexistingReportID, + preexistingReportID: null, + // Replacing the existing report's participants to avoid duplicates + participants: existingReport?.participants ?? report.participants, }); + } else { + // Thread reports have parent actions that need their childReportID updated to point to the preexisting thread + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, { + ...report, + reportID: preexistingReportID, + preexistingReportID: null, + }); + // Non-optimistic parent actions already exist, so we update their childReportID; + // optimistic actions were already cleaned up above + const parentReportAction = parentReportID ? allReportActions?.[parentReportID]?.[parentReportActionID] : null; + if (parentReportAction && !parentReportAction.isOptimisticAction) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { + [parentReportActionID]: {childReportID: preexistingReportID}, + }); + } } - } - }; + }; - const isParentOneTransactionReport = isOneTransactionReport(parentReport); + const isParentOneTransactionReport = isOneTransactionReport(parentReport); - // One-transaction reports display the expense via the parent IOU report screen, not the thread, so the draft belongs there - const reportToCopyDraftTo = !!parentReportID && isParentOneTransactionReport ? parentReportID : preexistingReportID; + // One-transaction reports display the expense via the parent IOU report screen, not the thread, so the draft belongs there + const reportToCopyDraftTo = !!parentReportID && isParentOneTransactionReport ? parentReportID : preexistingReportID; - if (!navigationRef.isReady()) { - callback(); - return; - } + if (!navigationRef.isReady()) { + callback(); + return; + } - // Use Navigation.getActiveRoute() instead of navigationRef.getCurrentRoute()?.path because - // getCurrentRoute().path can be undefined during first navigation. - // We still need getCurrentRoute() for params and screen name as getActiveRoute() only returns the path string. - const activeRoute = Navigation.getActiveRoute(); - const currentRouteInfo = navigationRef.getCurrentRoute(); - const backTo = (currentRouteInfo?.params as {backTo?: Route})?.backTo; - const screenName = currentRouteInfo?.name; + // Use Navigation.getActiveRoute() instead of navigationRef.getCurrentRoute()?.path because + // getCurrentRoute().path can be undefined during first navigation. + // We still need getCurrentRoute() for params and screen name as getActiveRoute() only returns the path string. + const activeRoute = Navigation.getActiveRoute(); + const currentRouteInfo = navigationRef.getCurrentRoute(); + const backTo = (currentRouteInfo?.params as {backTo?: Route})?.backTo; + const screenName = currentRouteInfo?.name; - // Besides the central-pane report route (/r/:reportID), the optimistic report can also be focused - // in the RHP transaction-thread carousel (search/view/:reportID) used by the "Review X expenses" - // flow. Matched via route name + reportID param (not a path substring) since the carousel's nested - // backTo can contain other report IDs. Without this, the report is cleared out from under the user - // and ReportNavigateAwayHandler treats it as removed, dismissing the RHP and navigating to the parent. - const isOptimisticReportFocusedInSearchRHP = - screenName === SCREENS.RIGHT_MODAL.SEARCH_REPORT && - !!currentRouteInfo?.params && - typeof currentRouteInfo.params === 'object' && - 'reportID' in currentRouteInfo.params && - currentRouteInfo.params.reportID === reportID; - const isOptimisticReportFocused = activeRoute.includes(`/r/${reportID}`) || isOptimisticReportFocusedInSearchRHP; + // Besides the central-pane report route (/r/:reportID), the optimistic report can also be focused + // in the RHP transaction-thread carousel (search/view/:reportID) used by the "Review X expenses" + // flow. Matched via route name + reportID param (not a path substring) since the carousel's nested + // backTo can contain other report IDs. Without this, the report is cleared out from under the user + // and ReportNavigateAwayHandler treats it as removed, dismissing the RHP and navigating to the parent. + const isOptimisticReportFocusedInSearchRHP = + screenName === SCREENS.RIGHT_MODAL.SEARCH_REPORT && + !!currentRouteInfo?.params && + typeof currentRouteInfo.params === 'object' && + 'reportID' in currentRouteInfo.params && + currentRouteInfo.params.reportID === reportID; + const isOptimisticReportFocused = activeRoute.includes(`/r/${reportID}`) || isOptimisticReportFocusedInSearchRHP; - // Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730. - // When user is editing a money request report (/e/:reportID route) and has - // an optimistic report in the background that should be replaced with preexisting report - const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`); + // Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730. + // When user is editing a money request report (/e/:reportID route) and has + // an optimistic report in the background that should be replaced with preexisting report + const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`); - // Only re-route them if they are still looking at the optimistically created report - if (isOptimisticReportFocused || isOptimisticReportInBackground) { - const currCallback = callback; - callback = () => { - if (isOptimisticReportFocused) { - if (!parentReportActionID || !isParentOneTransactionReport) { - // We are either in a DM/group-DM that do not have a parent report, - // a thread under any comment, - // or transaction thread report under an IOU report action that its parent IOU report is not a one expense report, - // we need to navigate to the preexisting report chat - // because we will clear the optimistically created report in the currCallback - Navigation.setParams({reportID: preexistingReportID.toString()}); - } else { - // We are in a transaction thread report under an IOU report action where the parent IOU report is a one transaction report - // We need to navigate to the one expense report screen instead of the preexisting report chat - // because we will clear the optimistically created transaction thread report in the currCallback - // and the one transaction should be accessed via the one expense report screen and not the preexisting report chat - Navigation.setParams({reportID: parentReportID}); + // Only re-route them if they are still looking at the optimistically created report + if (isOptimisticReportFocused || isOptimisticReportInBackground) { + const currCallback = callback; + callback = () => { + if (isOptimisticReportFocused) { + if (!parentReportActionID || !isParentOneTransactionReport) { + // We are either in a DM/group-DM that do not have a parent report, + // a thread under any comment, + // or transaction thread report under an IOU report action that its parent IOU report is not a one expense report, + // we need to navigate to the preexisting report chat + // because we will clear the optimistically created report in the currCallback + Navigation.setParams({reportID: preexistingReportID.toString()}); + } else { + // We are in a transaction thread report under an IOU report action where the parent IOU report is a one transaction report + // We need to navigate to the one expense report screen instead of the preexisting report chat + // because we will clear the optimistically created transaction thread report in the currCallback + // and the one transaction should be accessed via the one expense report screen and not the preexisting report chat + Navigation.setParams({reportID: parentReportID}); + } + } else if (isOptimisticReportInBackground) { + // Navigate to the correct backTo route with the preexisting report ID + Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route); } - } else if (isOptimisticReportInBackground) { - // Navigate to the correct backTo route with the preexisting report ID - Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route); - } - currCallback(); - }; + currCallback(); + }; - // The report screen will listen to this event and transfer the draft comment to the existing report - // This will allow the newest draft comment to be transferred to the existing report - DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, { - preexistingReportID, - reportToCopyDraftTo, - callback, - }); + // The report screen will listen to this event and transfer the draft comment to the existing report + // This will allow the newest draft comment to be transferred to the existing report + DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, { + preexistingReportID, + reportToCopyDraftTo, + callback, + }); - return; - } + return; + } - if ( - parentReportID && - isParentOneTransactionReport && - (activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)) || activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID}))) - ) { - if (draftReportComment) { - // Draft must be saved first because the callback will clear the optimistic report and its associated draft - saveReportDraftComment(parentReportID, draftReportComment, () => { + if ( + parentReportID && + isParentOneTransactionReport && + (activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)) || activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID}))) + ) { + if (draftReportComment) { + // Draft must be saved first because the callback will clear the optimistic report and its associated draft + saveReportDraftComment(parentReportID, draftReportComment, () => { + callback(); + + // We are already on the parent one expense report, so just call the API to fetch report data + // betas is safe to pass as undefined because introSelected is undefined, so the code path + // that uses betas is never reached. Passing it explicitly so the compiler flags this when + // betas becomes required. Refactor issue: https://github.com/Expensify/App/issues/66424 + openReport({reportID: parentReportID, introSelected: undefined, betas: undefined}); + }); + } else { callback(); // We are already on the parent one expense report, so just call the API to fetch report data @@ -210,27 +220,19 @@ function replaceOptimisticReportWithActualReport(report: Report, draftReportComm // that uses betas is never reached. Passing it explicitly so the compiler flags this when // betas becomes required. Refactor issue: https://github.com/Expensify/App/issues/66424 openReport({reportID: parentReportID, introSelected: undefined, betas: undefined}); - }); - } else { - callback(); - - // We are already on the parent one expense report, so just call the API to fetch report data - // betas is safe to pass as undefined because introSelected is undefined, so the code path - // that uses betas is never reached. Passing it explicitly so the compiler flags this when - // betas becomes required. Refactor issue: https://github.com/Expensify/App/issues/66424 - openReport({reportID: parentReportID, introSelected: undefined, betas: undefined}); + } + return; } - return; - } - // In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report - // after that clear the optimistically created report - if (!draftReportComment) { - callback(); - return; - } + // In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report + // after that clear the optimistically created report + if (!draftReportComment) { + callback(); + return; + } - saveReportDraftComment(reportToCopyDraftTo, draftReportComment, callback); + saveReportDraftComment(reportToCopyDraftTo, draftReportComment, callback); + }, }); } diff --git a/src/pages/DynamicReportDetailsPage.tsx b/src/pages/DynamicReportDetailsPage.tsx index cde9780044ad..e5802aa5657f 100644 --- a/src/pages/DynamicReportDetailsPage.tsx +++ b/src/pages/DynamicReportDetailsPage.tsx @@ -3,8 +3,7 @@ import {delegateEmailSelector} from '@selectors/Account'; import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; @@ -50,6 +49,7 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {ReportDetailsNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import Parser from '@libs/Parser'; import Permissions from '@libs/Permissions'; @@ -1086,9 +1086,7 @@ function DynamicReportDetailsPage({policy, report, route, reportMetadata, report navigateToTargetUrl(); // Delay deletion until the RHP close animation finishes to prevent a brief // "Not Found" flash inside the animating-out panel on slower devices. - InteractionManager.runAfterInteractions(() => { - deleteTransaction(); - }); + TransitionTracker.runAfterTransitions({callback: deleteTransaction, waitForUpcomingTransition: true}); }); }, [showConfirmModal, translate, caseID, iouTransactionID, shouldOpenSplitExpenseEditFlowOnDelete, navigateToTargetUrl, deleteTransaction]); diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 1dd04647975b..43ce76b90657 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -1,6 +1,4 @@ import React, {useEffect, useMemo} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager} from 'react-native'; import Onyx from 'react-native-onyx'; import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider'; import {useSearchResultsContext, useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; @@ -235,11 +233,7 @@ function SearchTransactionsChangeReport() { allTransactionViolation: transactionViolations, allReports, }); - InteractionManager.runAfterInteractions(() => { - clearSelectedTransactions(); - }); - - Navigation.goBack(); + Navigation.goBack(undefined, {afterTransition: clearSelectedTransactions}); }; const removeFromReport = () => { diff --git a/src/pages/inbox/ReportFetchHandler.tsx b/src/pages/inbox/ReportFetchHandler.tsx index 3095f53868b0..590573b87e5e 100644 --- a/src/pages/inbox/ReportFetchHandler.tsx +++ b/src/pages/inbox/ReportFetchHandler.tsx @@ -1,7 +1,5 @@ import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; import {useEffect, useEffectEvent, useRef} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager} from 'react-native'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useIsAnonymousUser from '@hooks/useIsAnonymousUser'; import useIsInSidePanel from '@hooks/useIsInSidePanel'; @@ -16,6 +14,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; +import type {CancelHandle} from '@libs/Navigation/TransitionTracker'; import {getFilteredReportActionsForReportView, getIOUActionForReportID, getOneTransactionThreadReportID, isCreatedAction} from '@libs/ReportActionsUtils'; import { isChatThread, @@ -335,11 +335,14 @@ function ReportFetchHandler() { // any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null. // Existing reports created will have empty fields for `pendingFields`. const didCreateReportSuccessfully = !report?.pendingFields || (!report?.pendingFields.addWorkspaceRoom && !report?.pendingFields.createChat); - let interactionTask: ReturnType | null = null; + let interactionTask: CancelHandle | null = null; if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { - interactionTask = InteractionManager.runAfterInteractions(() => { - subscribeToReportLeavingEvents(reportIDFromRoute, currentUserAccountID); - didSubscribeToReportLeavingEvents.current = true; + interactionTask = TransitionTracker.runAfterTransitions({ + callback: () => { + subscribeToReportLeavingEvents(reportIDFromRoute, currentUserAccountID); + didSubscribeToReportLeavingEvents.current = true; + }, + waitForUpcomingTransition: true, }); } return () => { diff --git a/src/pages/inbox/report/useReportActionsNewActionLiveTail.ts b/src/pages/inbox/report/useReportActionsNewActionLiveTail.ts index b684c66485e5..da469de4d508 100644 --- a/src/pages/inbox/report/useReportActionsNewActionLiveTail.ts +++ b/src/pages/inbox/report/useReportActionsNewActionLiveTail.ts @@ -1,12 +1,11 @@ import {useCallback, useEffect, useEffectEvent, useRef, useState} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type useReportScrollManager from '@hooks/useReportScrollManager'; import type {OpenReportActionParams} from '@libs/actions/Report'; import {openReport, pruneReportActionPagesToNewestWindow, subscribeToNewActionEvent} from '@libs/actions/Report'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import Navigation from '@libs/Navigation/Navigation'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; @@ -76,56 +75,58 @@ function useReportActionsNewActionLiveTail({ const [isScrollToBottomEnabled, setIsScrollToBottomEnabled] = useState(false); const scrollToBottomForCurrentUserAction = useEffectEvent((isFromCurrentUser: boolean, action?: OnyxTypes.ReportAction) => { - InteractionManager.runAfterInteractions(() => { - // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where - // they are now in the list. - if (!isFromCurrentUser || (!isReportTopmostSplitNavigator() && !Navigation.getReportRHPActiveRoute())) { - return; - } - if (!hasNewestReportAction && !isFromCurrentUser) { - if (Navigation.getReportRHPActiveRoute()) { + TransitionTracker.runAfterTransitions({ + callback: () => { + // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where + // they are now in the list. + if (!isFromCurrentUser || (!isReportTopmostSplitNavigator() && !Navigation.getReportRHPActiveRoute())) { return; } - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - }); - return; - } - - const shouldJumpToLiveTail = - !isOffline && action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && (hasNewerActions || !!linkedReportActionID || unreadMarkerReportActionID); - - if (shouldJumpToLiveTail) { - if (liveTailJumpRef.current.stage === 'idle') { - liveTailJumpRef.current = {stage: 'open_report'}; - openReport({ - reportID, - introSelected, - betas, + if (!hasNewestReportAction && !isFromCurrentUser) { + if (Navigation.getReportRHPActiveRoute()) { + return; + } + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); }); + return; + } + + const shouldJumpToLiveTail = + !isOffline && action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && (hasNewerActions || !!linkedReportActionID || unreadMarkerReportActionID); + + if (shouldJumpToLiveTail) { + if (liveTailJumpRef.current.stage === 'idle') { + liveTailJumpRef.current = {stage: 'open_report'}; + openReport({ + reportID, + introSelected, + betas, + }); + } + return; } - return; - } - const index = sortedVisibleReportActions.findIndex((item) => item.reportActionID === action?.reportActionID); - if (action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) { - if (index > 0) { - setTimeout(() => { - reportScrollManager.scrollToIndex(index); - }, 100); + const index = sortedVisibleReportActions.findIndex((item) => item.reportActionID === action?.reportActionID); + if (action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) { + if (index > 0) { + setTimeout(() => { + reportScrollManager.scrollToIndex(index); + }, 100); + } else { + setIsFloatingMessageCounterVisible(false); + reportScrollManager.scrollToBottom(); + } + if (action?.reportActionID) { + setActionIdToHighlight(action.reportActionID); + } } else { setIsFloatingMessageCounterVisible(false); reportScrollManager.scrollToBottom(); } - if (action?.reportActionID) { - setActionIdToHighlight(action.reportActionID); - } - } else { - setIsFloatingMessageCounterVisible(false); - reportScrollManager.scrollToBottom(); - } - setIsScrollToBottomEnabled(true); + setIsScrollToBottomEnabled(true); + }, }); }); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx index d0aa891f6f2f..5a272362b4ad 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx @@ -1,8 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashIsEmpty from 'lodash/isEmpty'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -513,9 +512,7 @@ function IOURequestStepDistanceOdometer({ }; const restoreLastInputFocus = useCallback(() => { - InteractionManager.runAfterInteractions(() => { - lastFocusedInputRef.current?.focus(); - }); + lastFocusedInputRef.current?.focus(); }, []); useDiscardChangesConfirmation({ diff --git a/src/pages/iou/request/step/IOURequestStepReport/hooks/useReportSelectionActions.ts b/src/pages/iou/request/step/IOURequestStepReport/hooks/useReportSelectionActions.ts index f41265074bf6..5c7e17dc27b1 100644 --- a/src/pages/iou/request/step/IOURequestStepReport/hooks/useReportSelectionActions.ts +++ b/src/pages/iou/request/step/IOURequestStepReport/hooks/useReportSelectionActions.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line no-restricted-imports -import {InteractionManager} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useSearchSelectionActions} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; @@ -9,6 +7,7 @@ import {setCustomUnitID, setCustomUnitRateID} from '@libs/actions/IOU/MoneyReque import {clearSubrates} from '@libs/actions/IOU/PerDiem'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; import {getReportOrDraftReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -177,8 +176,9 @@ function useReportSelectionActions({ } handleGoBack(); - InteractionManager.runAfterInteractions(() => { - Navigation.setNavigationActionToMicrotaskQueue(() => { + TransitionTracker.runAfterTransitions({ + waitForUpcomingTransition: true, + callback: () => { const participants = buildParticipants(report); setTransactionReport( @@ -208,7 +208,7 @@ function useReportSelectionActions({ }); removeTransaction(transaction.transactionID); } - }); + }, }); }; @@ -216,21 +216,22 @@ function useReportSelectionActions({ if (!transaction) { return; } - Navigation.dismissToSuperWideRHP(); - InteractionManager.runAfterInteractions(() => { - const policyTagList = personalPolicyID ? allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${personalPolicyID}`] : {}; - changeTransactionsReport({ - transactionIDs: targetTransactionIDs, - isASAPSubmitBetaEnabled, - accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, - email: session?.email ?? '', - policy: allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${personalPolicyID}`], - policyTagList, - transactions: targetTransactions, - allTransactionViolation: transactionViolations, - allReports, - }); - removeTransaction(transaction.transactionID); + Navigation.dismissToSuperWideRHP({ + afterTransition: () => { + const policyTagList = personalPolicyID ? allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${personalPolicyID}`] : {}; + changeTransactionsReport({ + transactionIDs: targetTransactionIDs, + isASAPSubmitBetaEnabled, + accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + email: session?.email ?? '', + policy: allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${personalPolicyID}`], + policyTagList, + transactions: targetTransactions, + allTransactionViolation: transactionViolations, + allReports, + }); + removeTransaction(transaction.transactionID); + }, }); }; diff --git a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx index 0ee61ed9f870..bab1146f6329 100644 --- a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx +++ b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx @@ -1,8 +1,7 @@ import {useRoute} from '@react-navigation/native'; import {emailSelector} from '@selectors/Session'; import React, {useEffect, useMemo} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import ConfirmationPage from '@components/ConfirmationPage'; import type {ConfirmationPageProps} from '@components/ConfirmationPage'; @@ -18,6 +17,7 @@ import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {shouldHideOldAppRedirect} from '@libs/TryNewDotUtils'; import {closeReactNativeApp} from '@userActions/HybridApp'; @@ -197,9 +197,13 @@ function MergeResultPage() { return; } - InteractionManager.runAfterInteractions(() => { - Navigation.removeScreenFromNavigationState(SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_DETAILS); + const handle = TransitionTracker.runAfterTransitions({ + callback: () => { + Navigation.removeScreenFromNavigationState(SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_DETAILS); + }, + waitForUpcomingTransition: true, }); + return () => handle.cancel(); }, [result]); const { diff --git a/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx b/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx index 67c01ca46684..8e5314555df5 100644 --- a/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/ReplaceDeviceVerifyNewPage.tsx @@ -1,6 +1,5 @@ import React, {useEffect, useRef} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {ScrollView as RNScrollView} from 'react-native'; import Button from '@components/Button'; @@ -57,10 +56,8 @@ function ReplaceDeviceVerifyNewPage() { }, [account, account?.twoFactorAuthSecretKey]); const handleInputFocus = () => { - InteractionManager.runAfterInteractions(() => { - requestAnimationFrame(() => { - scrollViewRef.current?.scrollToEnd({animated: true}); - }); + requestAnimationFrame(() => { + scrollViewRef.current?.scrollToEnd({animated: true}); }); }; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index ec3947ea78bb..b61f7d24480a 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -1,8 +1,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import type {TupleToUnion} from 'type-fest'; import ApprovalWorkflowSection from '@components/ApprovalWorkflowSection'; import Icon from '@components/Icon'; @@ -224,9 +223,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const {showLockedAccountModal} = useLockedAccountActions(); useEffect(() => { - InteractionManager.runAfterInteractions(() => { - fetchData(); - }); + fetchData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []);