From 9276965d5dfb8c18db2e4527ae37165ae3aadd91 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 29 May 2026 11:58:31 -0700 Subject: [PATCH 1/2] Revert "Merge pull request #88538 from software-mansion-labs/collectioneur/transition-tracker-batch-2" This reverts commit 6768020f6fa1f4c47bfbfd7af147f439b0615d9f, reversing changes made to ff4cf19498f06ba60196fe0a65640ff6cb8ccd53. --- config/eslint/eslint.seatbelt.tsv | 8 +- src/components/MoneyRequestHeader.tsx | 2 +- src/components/MoneyRequestHeaderActions.tsx | 2 +- .../MoneyRequestHeaderSecondaryActions.tsx | 15 ++-- .../MoneyRequestReportView.tsx | 13 ++-- .../SidePanel/SidePanelContextProvider.tsx | 14 +--- .../SidePanel/SidePanelModal/types.ts | 2 +- src/hooks/useExpenseActions.ts | 25 +++--- src/libs/Navigation/Navigation.ts | 8 +- src/libs/Navigation/TransitionTracker.ts | 11 +-- src/libs/actions/Report/index.ts | 77 ++++++------------- src/libs/actions/Session/index.ts | 18 ++--- src/libs/actions/Task.ts | 9 ++- .../Search/SearchMoneyRequestReportPage.tsx | 12 +-- .../DeleteTransactionNavigateBackHandler.tsx | 12 +-- src/pages/inbox/ReportHeader.tsx | 14 ++-- .../BaseReportActionContextMenu.tsx | 7 +- src/pages/inbox/report/ReportActionsList.tsx | 38 ++++----- .../assignCard/ConfirmationStep.tsx | 14 ++-- tests/actions/ReportTest.ts | 4 +- 20 files changed, 124 insertions(+), 181 deletions(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 4538ef061efa..c3c9a3110770 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -394,7 +394,7 @@ "../../src/libs/actions/ReportLayout.ts" "no-restricted-syntax" 1 "../../src/libs/actions/ScheduleCall.ts" "no-restricted-syntax" 1 "../../src/libs/actions/Search.ts" "no-restricted-syntax" 1 -"../../src/libs/actions/Session/index.ts" "no-restricted-imports" 1 +"../../src/libs/actions/Session/index.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/libs/actions/Session/index.ts" "no-restricted-syntax" 9 "../../src/libs/actions/Session/index.ts" "rulesdir/no-onyx-connect" 2 "../../src/libs/actions/StatsCounter.ts" "no-restricted-syntax" 2 @@ -464,7 +464,7 @@ "../../src/pages/RoomMembersPage.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/ScheduleCall/ScheduleCallPage.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx" "react-hooks/set-state-in-effect" 1 -"../../src/pages/Search/SearchMoneyRequestReportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchMoneyRequestReportPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/Search/SearchPage.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/Search/SearchTransactionsChangeReport.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/Share/ShareRootPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 @@ -473,7 +473,7 @@ "../../src/pages/ValidateLoginPage/index.web.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/domain/Groups/PreferredWorkspaceToggle.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 "../../src/pages/domain/Saml/SamlLoginSectionContent.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 -"../../src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx" "no-restricted-imports" 1 +"../../src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/inbox/ReportFetchHandler.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2 "../../src/pages/inbox/ReportNavigateAwayHandler.tsx" "react-hooks/exhaustive-deps" 1 "../../src/pages/inbox/hooks/useDeferNonEssentials.ts" "no-restricted-imports" 2 @@ -495,7 +495,7 @@ "../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/refs" 5 "../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/set-state-in-effect" 1 -"../../src/pages/inbox/report/ReportActionsList.tsx" "no-restricted-imports" 1 +"../../src/pages/inbox/report/ReportActionsList.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 4 "../../src/pages/inbox/report/ReportActionsList.tsx" "react-hooks/refs" 5 "../../src/pages/inbox/report/ReportActionsList.tsx" "react-hooks/set-state-in-effect" 3 "../../src/pages/inbox/report/TripSummary.tsx" "rulesdir/no-default-id-values" 1 diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 5b1f63c17214..65d29db40798 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -48,7 +48,7 @@ type MoneyRequestHeaderProps = { reportID: string | undefined; /** Method to trigger when pressing close button of the header */ - onBackButtonPress: (prioritizeBackTo?: boolean, options?: {afterTransition?: () => void}) => void; + onBackButtonPress: (prioritizeBackTo?: boolean) => void; }; function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRequestHeaderProps) { diff --git a/src/components/MoneyRequestHeaderActions.tsx b/src/components/MoneyRequestHeaderActions.tsx index bdf2909d8011..4dd5a2f42231 100644 --- a/src/components/MoneyRequestHeaderActions.tsx +++ b/src/components/MoneyRequestHeaderActions.tsx @@ -16,7 +16,7 @@ type MoneyRequestHeaderActionsProps = { reportID: string | undefined; /** Method to trigger when pressing close button of the header */ - onBackButtonPress: (prioritizeBackTo?: boolean, options?: {afterTransition?: () => void}) => void; + onBackButtonPress: (prioritizeBackTo?: boolean) => void; }; function MoneyRequestHeaderActions({reportID, onBackButtonPress}: MoneyRequestHeaderActionsProps) { diff --git a/src/components/MoneyRequestHeaderSecondaryActions.tsx b/src/components/MoneyRequestHeaderSecondaryActions.tsx index 597f790edb21..cc38af6bb872 100644 --- a/src/components/MoneyRequestHeaderSecondaryActions.tsx +++ b/src/components/MoneyRequestHeaderSecondaryActions.tsx @@ -3,6 +3,8 @@ import {shouldFailAllRequestsSelector} from '@selectors/Network'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import {validTransactionDraftsSelector} from '@selectors/TransactionDraft'; import React, {useRef, useState} from 'react'; +// eslint-disable-next-line no-restricted-imports +import {InteractionManager} from 'react-native'; import type {ValueOf} from 'type-fest'; import useConfirmModal from '@hooks/useConfirmModal'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; @@ -82,7 +84,7 @@ type MoneyRequestHeaderSecondaryActionsProps = { reportID: string | undefined; /** Method to trigger when pressing close button of the header */ - onBackButtonPress: (prioritizeBackTo?: boolean, options?: {afterTransition?: () => void}) => void; + onBackButtonPress: (prioritizeBackTo?: boolean) => void; }; function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: MoneyRequestHeaderSecondaryActionsProps) { @@ -443,8 +445,6 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money } const backToRoute = route.params?.backTo ?? Navigation.getActiveRoute(); setDeleteTransactionNavigateBackUrl(backToRoute); - - let afterDelete: (() => void) | undefined; if (isTrackExpenseAction(parentReportAction) && !isExpenseSplit) { deleteTrackExpense({ chatReportID: report?.parentReportID, @@ -467,7 +467,8 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true); return; } - afterDelete = () => { + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { const deleteResult = deleteTransactions( [transaction.transactionID], duplicateTransactions, @@ -481,14 +482,14 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money } removeTransaction(transaction.transactionID); - }; + }); } if (isInNarrowPaneModal) { - Navigation.navigateBackToLastSuperWideRHPScreen({afterTransition: afterDelete}); + Navigation.navigateBackToLastSuperWideRHPScreen(); return; } - onBackButtonPress(false, {afterTransition: afterDelete}); + onBackButtonPress(); }); }, }, diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 87b80abbb82f..9f6b32fb3e84 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useMemo} from 'react'; // We use Animated for all functionality related to wide RHP to make it easier // to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated. // eslint-disable-next-line no-restricted-imports -import {Animated, ScrollView, View} from 'react-native'; +import {Animated, InteractionManager, ScrollView, View} from 'react-native'; import type {LayoutChangeEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import CollapsibleHeaderOnKeyboard from '@components/CollapsibleHeaderOnKeyboard'; @@ -61,7 +61,7 @@ type MoneyRequestReportViewProps = { onLayout?: (event: LayoutChangeEvent) => void; }; -function goBackFromSearchMoneyRequest(options?: {afterTransition?: () => void}) { +function goBackFromSearchMoneyRequest() { const rootState = navigationRef.getRootState(); const lastRoute = rootState.routes.at(-1); @@ -71,7 +71,7 @@ function goBackFromSearchMoneyRequest(options?: {afterTransition?: () => void}) } if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { - Navigation.goBack(undefined, options); + Navigation.goBack(); return; } @@ -81,11 +81,11 @@ function goBackFromSearchMoneyRequest(options?: {afterTransition?: () => void}) } if (rootState.routes.length > 1) { - Navigation.goBack(undefined, options); + Navigation.goBack(); return; } - Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()}), options); + Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); } function InitialLoadingSkeleton({styles, onLayout, reasonAttributes}: {styles: ThemeStyles; onLayout?: (event: LayoutChangeEvent) => void; reasonAttributes: SkeletonSpanReasonAttributes}) { @@ -144,7 +144,8 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport const isLoadingInitialReportActions = reportLoadingState?.isLoadingInitialReportActions; const dismissReportCreationError = useCallback(() => { - goBackFromSearchMoneyRequest({afterTransition: () => removeFailedReport(reportID)}); + goBackFromSearchMoneyRequest(); + InteractionManager.runAfterInteractions(() => removeFailedReport(reportID)); }, [reportID]); // Special case handling a report that is a transaction thread diff --git a/src/components/SidePanel/SidePanelContextProvider.tsx b/src/components/SidePanel/SidePanelContextProvider.tsx index 1f23aeb28f2f..c1dee29c6d69 100644 --- a/src/components/SidePanel/SidePanelContextProvider.tsx +++ b/src/components/SidePanel/SidePanelContextProvider.tsx @@ -33,7 +33,7 @@ type SidePanelStateContextProps = { type SidePanelActionsContextProps = { openSidePanel: () => void; - closeSidePanel: (options?: {afterTransition?: () => void}) => void; + closeSidePanel: () => void; }; const SidePanelStateContext = createContext({ @@ -88,7 +88,6 @@ function SidePanelContextProvider({children}: PropsWithChildren) { const reportID = (isRHPAdminsRoom || isRHPHomePage) && isUserAdmin && isPolicyActive && adminsChatReportID ? adminsChatReportID : conciergeReportID; - const onCloseCompleteRef = useRef<(() => void) | undefined>(undefined); const [sessionStartTime, setSessionStartTime] = useState(null); const [prevShouldHideSidePanel, setPrevShouldHideSidePanel] = useState(shouldHideSidePanel); @@ -122,22 +121,17 @@ function SidePanelContextProvider({children}: PropsWithChildren) { duration: CONST.SIDE_PANEL_ANIMATED_TRANSITION, useNativeDriver: true, }), - ]).start(() => { - setIsSidePanelTransitionEnded(true); - onCloseCompleteRef.current?.(); - onCloseCompleteRef.current = undefined; - }); + ]).start(() => setIsSidePanelTransitionEnded(true)); }, [shouldHideSidePanel, shouldApplySidePanelOffset]); - const closeSidePanel = (options?: {afterTransition?: () => void}) => { + const closeSidePanel = (shouldUpdateNarrow = false) => { // User shouldn't be able to close side panel if side panel NVP is undefined if (!sidePanelNVP) { return; } - onCloseCompleteRef.current = options?.afterTransition; setIsSidePanelTransitionEnded(false); - SidePanelActions.closeSidePanel(!isExtraLargeScreenWidth); + SidePanelActions.closeSidePanel(!isExtraLargeScreenWidth || shouldUpdateNarrow); // Focus the composer after closing the Side Panel focusComposerWithDelay(ReportActionComposeFocusManager.composerRef.current, CONST.SIDE_PANEL_ANIMATED_TRANSITION + CONST.COMPOSER_FOCUS_DELAY)(true); diff --git a/src/components/SidePanel/SidePanelModal/types.ts b/src/components/SidePanel/SidePanelModal/types.ts index d07c618e3cd9..1aa245864fce 100644 --- a/src/components/SidePanel/SidePanelModal/types.ts +++ b/src/components/SidePanel/SidePanelModal/types.ts @@ -7,7 +7,7 @@ type SidePanelModalProps = { shouldHideSidePanel: boolean; sidePanelTranslateX: RefObject; shouldHideSidePanelBackdrop: boolean; - closeSidePanel: (options?: {afterTransition?: () => void}) => void; + closeSidePanel: (shouldUpdateNarrow?: boolean) => void; }; export default SidePanelModalProps; diff --git a/src/hooks/useExpenseActions.ts b/src/hooks/useExpenseActions.ts index bdc9cc6a06e6..619030f9b489 100644 --- a/src/hooks/useExpenseActions.ts +++ b/src/hooks/useExpenseActions.ts @@ -550,19 +550,18 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic setDeleteTransactionNavigateBackUrl(deleteNavigateBackUrl); Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.goBack(backToRoute, { - afterTransition: () => { - deleteAppReport({ - report: moneyRequestReport, - selfDMReport, - currentUserEmailParam: email ?? '', - currentUserAccountIDParam: accountID, - reportTransactions, - allTransactionViolations, - bankAccountList, - hash: currentSearchHash, - }); - }, + Navigation.goBack(backToRoute); + InteractionManager.runAfterInteractions(() => { + deleteAppReport({ + report: moneyRequestReport, + selfDMReport, + currentUserEmailParam: email ?? '', + currentUserAccountIDParam: accountID, + reportTransactions, + allTransactionViolations, + bankAccountList, + hash: currentSearchHash, + }); }); }); }, diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index c5b7b947ecee..63e8fc8ecb43 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -825,7 +825,7 @@ function dismissModal({ref = navigationRef, afterTransition, waitForTransition}: const dismissModalWithReport = ( {reportID, reportActionID, referrer, backTo}: ReportsSplitNavigatorParamList[typeof SCREENS.REPORT], ref = navigationRef, - options?: {onBeforeNavigate?: (willOpenReport: boolean) => void; afterTransition?: () => void}, + options?: {onBeforeNavigate?: (willOpenReport: boolean) => void}, ) => { const dismissAndOpenReport = () => { const topmostSuperWideRHPReportID = getTopmostSuperWideRHPReportID(); @@ -833,7 +833,7 @@ const dismissModalWithReport = ( if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { options?.onBeforeNavigate?.(false); - dismissToSuperWideRHP({afterTransition: options?.afterTransition}); + dismissToSuperWideRHP(); return; } @@ -842,14 +842,14 @@ const dismissModalWithReport = ( const isReportsSplitTopmostFullScreen = isReportTopmostSplitNavigator(); if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) { options?.onBeforeNavigate?.(false); - dismissModal({afterTransition: options?.afterTransition}); + dismissModal(); return; } options?.onBeforeNavigate?.(true); const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID, referrer, backTo); dismissModal({ afterTransition: () => { - navigate(reportRoute, {afterTransition: options?.afterTransition}); + navigate(reportRoute); }, }); }; diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts index 978a1fdf36fb..e5bd76bc746e 100644 --- a/src/libs/Navigation/TransitionTracker.ts +++ b/src/libs/Navigation/TransitionTracker.ts @@ -7,7 +7,7 @@ type CancelHandle = {cancel: () => void}; type RunAfterTransitionsOptions = { /** The function to invoke once all active transitions have completed. */ - callback: () => void | Promise; + callback: () => void; /** If true, the callback fires synchronously regardless of any active transitions. Defaults to false. */ runImmediately?: boolean; @@ -20,7 +20,7 @@ type RunAfterTransitionsOptions = { const activeTransitions = new Map>(); -let pendingCallbacks: Array<() => void | Promise> = []; +let pendingCallbacks: Array<() => void> = []; let nextTransitionStartResolve: (() => void) | null = null; let promiseForNextTransitionStart = new Promise((resolve) => { @@ -36,12 +36,7 @@ function flushCallbacks(): void { pendingCallbacks = []; for (const callback of callbacks) { try { - const result = callback(); - if (result instanceof Promise) { - result.catch((error) => { - Log.warn('[TransitionTracker] A pending async callback threw an error', {error}); - }); - } + callback(); } catch (error) { Log.warn('[TransitionTracker] A pending callback threw an error', {error}); } diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index be27c9f963fa..37e8c244ff9b 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2067,6 +2067,11 @@ function createGroupChat( resourceID: reportID, }; + // Clear group chat data after navigation dismissed so we don't see stale data + InteractionManager.runAfterInteractions(() => { + clearGroupChat(); + }); + API.paginate(CONST.API_REQUEST_TYPE.WRITE, WRITE_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}, paginationConfig, { checkAndFixConflictingRequest: (persistedRequests) => resolveOpenReportDuplicationConflictAction(persistedRequests, parameters), }); @@ -2221,33 +2226,18 @@ function createTransactionThreadReport(params: CreateTransactionThreadReportPara * Navigates to a report, handling modal dismissal and delayed navigation for composer focus. * * @param reportID The ID of the report to navigate to - * @param options.shouldDismissModal Whether to dismiss the modal before navigating (defaults to true) - * @param options.afterTransition Callback to run after the navigate transition completes + * @param shouldDismissModal Whether to dismiss the modal before navigating */ -function navigateToReport(reportID: string | undefined, options?: {shouldDismissModal?: boolean; afterTransition?: () => void}) { - const shouldDismissModal = options?.shouldDismissModal ?? true; - +function navigateToReport(reportID: string | undefined, shouldDismissModal = true) { if (shouldDismissModal) { - if (!reportID) { - Navigation.dismissModal({afterTransition: options?.afterTransition}); - return; - } Navigation.dismissModal(); } - if (!reportID) { return; } // In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks, wrapping navigation in setTimeout fixes this setTimeout(() => { - Navigation.isNavigationReady().then(() => { - const route = ROUTES.REPORT_WITH_ID.getRoute(reportID); - if (options?.afterTransition) { - Navigation.navigate(route, {afterTransition: options.afterTransition}); - } else { - Navigation.navigate(route); - } - }); + Navigation.isNavigationReady().then(() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID))); }, 0); } @@ -2267,7 +2257,6 @@ function navigateToAndOpenReport( betas: OnyxEntry, shouldDismissModal = true, shouldRevalidateExistingChat = false, - linkToOptions?: LinkToOptions, ) { const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins); const chat = getChatByParticipants([...participantAccountIDs, currentUserAccountID]); @@ -2293,7 +2282,7 @@ function navigateToAndOpenReport( betas, }); - navigateToReport(fallbackChat.reportID, {shouldDismissModal, ...linkToOptions}); + navigateToReport(fallbackChat.reportID, shouldDismissModal); }; if (isEmptyObject(chat) || isReportNotFound(chat)) { @@ -2302,7 +2291,7 @@ function navigateToAndOpenReport( } if (!shouldRevalidateExistingChat) { - navigateToReport(chat.reportID, {shouldDismissModal, ...linkToOptions}); + navigateToReport(chat.reportID, shouldDismissModal); return; } @@ -2328,7 +2317,7 @@ function navigateToAndOpenReport( // Re-open existing chats to re-validate server-side access and refresh stale local state. openReport({reportID: chat.reportID, introSelected, isSelfTourViewed, betas}); - navigateToReport(chat.reportID, {shouldDismissModal, ...linkToOptions}); + navigateToReport(chat.reportID, shouldDismissModal); } function navigateToAndCreateGroupChat( @@ -2349,7 +2338,7 @@ function navigateToAndCreateGroupChat( const newChat = buildOptimisticGroupChatReport(participantAccountIDs, reportName, avatarUri ?? '', currentUserAccountID, optimisticReportID, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); createGroupChat(newChat.reportID, userLogins, newChat, currentUserLogin, introSelected, isSelfTourViewed, betas, avatarFile); - navigateToReport(newChat.reportID, {afterTransition: clearGroupChat}); + navigateToReport(newChat.reportID); } /** @@ -2394,7 +2383,7 @@ function navigateToAndOpenReportWithAccountIDs( betas, }); - navigateToReport(fallbackChat.reportID, {shouldDismissModal: false}); + navigateToReport(fallbackChat.reportID, false); }; if (!chat || isReportNotFound(chat)) { @@ -2403,7 +2392,7 @@ function navigateToAndOpenReportWithAccountIDs( } if (!shouldRevalidateExistingChat) { - navigateToReport(chat.reportID, {shouldDismissModal: false}); + navigateToReport(chat.reportID, false); return; } @@ -2429,7 +2418,7 @@ function navigateToAndOpenReportWithAccountIDs( // Re-open existing chats to re-validate server-side access and refresh stale local state. openReport({reportID: chat.reportID, introSelected, isSelfTourViewed, betas}); - navigateToReport(chat.reportID, {shouldDismissModal: false}); + navigateToReport(chat.reportID, false); } /** @@ -3920,25 +3909,10 @@ function navigateToConciergeChat( return; } // TODO: allPersonalDetails fallback should be removed in follow-up PRs https://github.com/Expensify/App/issues/73656 - navigateToAndOpenReport( - [CONST.EMAIL.CONCIERGE], - personalDetails ?? allPersonalDetails, - currentUserAccountID, - introSelected, - isSelfTourViewed, - betas, - shouldDismissModal, - false, - linkToOptions, - ); + navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], personalDetails ?? allPersonalDetails, currentUserAccountID, introSelected, isSelfTourViewed, betas, shouldDismissModal); }); } else if (shouldDismissModal) { - const reportParams = {reportID: conciergeReportID, reportActionID}; - if (linkToOptions?.afterTransition) { - Navigation.dismissModalWithReport(reportParams, undefined, {afterTransition: linkToOptions.afterTransition}); - } else { - Navigation.dismissModalWithReport(reportParams); - } + Navigation.dismissModalWithReport({reportID: conciergeReportID, reportActionID}); } else { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeReportID), linkToOptions); } @@ -4420,18 +4394,11 @@ function navigateToConciergeChatAndDeleteReport( Navigation.goBack(); } const personalDetails = buildPersonalDetailsList([reportOwnerPersonalDetail, currentUserPersonalDetail, conciergePersonalDetail]); - navigateToConciergeChat( - conciergeReportID, - introSelected, - currentUserAccountID, - isSelfTourViewed, - betas, - false, - undefined, - {afterTransition: () => deleteReport(reportID, shouldDeleteChildReports)}, - undefined, - personalDetails, - ); + navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas, false, undefined, undefined, undefined, personalDetails); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + deleteReport(reportID, shouldDeleteChildReports); + }); } function clearCreateChatError( diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index ca509f5ac2ed..1e13afd7785f 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -3,6 +3,8 @@ import {openAuthSessionAsync} from 'expo-web-browser'; import throttle from 'lodash/throttle'; import type {ChannelAuthorizationData} from 'pusher-js/types/src/core/auth/options'; import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; +// eslint-disable-next-line no-restricted-imports +import {InteractionManager} from 'react-native'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {buildOldDotURL, openExternalLink} from '@libs/actions/Link'; @@ -36,7 +38,6 @@ import Log from '@libs/Log'; import {findMatchingDynamicSuffix} from '@libs/Navigation/helpers/dynamicRoutesUtils/findAllMatchingDynamicSuffixes'; import Navigation from '@libs/Navigation/Navigation'; import navigationRef from '@libs/Navigation/navigationRef'; -import TransitionTracker from '@libs/Navigation/TransitionTracker'; import * as MainQueue from '@libs/Network/MainQueue'; import * as NetworkStore from '@libs/Network/NetworkStore'; import {getCurrentUserEmail} from '@libs/Network/NetworkStore'; @@ -1413,14 +1414,13 @@ function waitForUserSignIn(): Promise { } function handleExitToNavigation(exitTo: Route) { - TransitionTracker.runAfterTransitions({ - callback: async () => { - await waitForUserSignIn(); - await Navigation.waitForProtectedRoutes(); - Navigation.goBack(ROUTES.HOME); - Navigation.navigate(exitTo); - }, - waitForUpcomingTransition: true, + InteractionManager.runAfterInteractions(() => { + waitForUserSignIn().then(() => { + Navigation.waitForProtectedRoutes().then(() => { + Navigation.goBack(ROUTES.HOME); + Navigation.navigate(exitTo); + }); + }); }); } diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 7c3eba5a62b6..c3ae5ed78e0d 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line no-restricted-imports +import {InteractionManager} from 'react-native'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import FallbackAvatar from '@assets/images/avatars/fallback-avatar.svg'; @@ -384,11 +386,10 @@ function createTaskAndNavigate(params: CreateTaskAndNavigateParams) { API.write(WRITE_COMMANDS.CREATE_TASK, parameters, {optimisticData, successData, failureData}); if (!isCreatedUsingMarkdown) { - Navigation.dismissModalWithReport({reportID: parentReportID}, undefined, { - afterTransition: () => { - clearOutTaskInfo(); - }, + InteractionManager.runAfterInteractions(() => { + clearOutTaskInfo(); }); + Navigation.dismissModalWithReport({reportID: parentReportID}); } notifyNewAction(parentReportID, optimisticAddCommentReport.reportAction, true); } diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 82b7149aa4ef..92104d1d8c1f 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -1,6 +1,8 @@ import {PortalHost} from '@gorhom/portal'; import {useIsFocused} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +// eslint-disable-next-line no-restricted-imports +import {InteractionManager} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -28,7 +30,6 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Log from '@libs/Log'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; import {getIOUActionForTransactionID, getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; @@ -101,11 +102,10 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { return; } // Clear the URL only after we navigate away to avoid a brief Not Found flash. - TransitionTracker.runAfterTransitions({ - callback: () => { - requestAnimationFrame(clearDeleteTransactionNavigateBackUrl); - }, - waitForUpcomingTransition: true, + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + clearDeleteTransactionNavigateBackUrl(); + }); }); }, [isFocused, deleteTransactionNavigateBackUrl]); diff --git a/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx b/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx index 71d1262ba951..204931377050 100644 --- a/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx +++ b/src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx @@ -1,7 +1,8 @@ import {useIsFocused} from '@react-navigation/native'; import {useEffect} from 'react'; +// eslint-disable-next-line no-restricted-imports +import {InteractionManager} from 'react-native'; import useOnyx from '@hooks/useOnyx'; -import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {doesDeleteNavigateBackUrlIncludeDuplicatesReview} from '@libs/TransactionNavigationUtils'; import {clearDeleteTransactionNavigateBackUrl} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -23,11 +24,10 @@ function DeleteTransactionNavigateBackHandler() { return; } // Clear the URL only after we navigate away to avoid a brief Not Found flash. - TransitionTracker.runAfterTransitions({ - callback: () => { - requestAnimationFrame(clearDeleteTransactionNavigateBackUrl); - }, - waitForUpcomingTransition: true, + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + clearDeleteTransactionNavigateBackUrl(); + }); }); }, [isFocused, deleteTransactionNavigateBackUrl]); diff --git a/src/pages/inbox/ReportHeader.tsx b/src/pages/inbox/ReportHeader.tsx index ffbba7c772d9..76d67178ab85 100644 --- a/src/pages/inbox/ReportHeader.tsx +++ b/src/pages/inbox/ReportHeader.tsx @@ -46,28 +46,28 @@ function ReportHeader() { const {reportPendingAction, reportErrors} = getReportOfflinePendingActionAndErrors(report); const pendingAction = reportPendingAction ?? report?.pendingFields?.reimbursed; - const onBackButtonPress = (prioritizeBackTo = false, options?: {afterTransition?: () => void}) => { + const onBackButtonPress = (prioritizeBackTo = false) => { if (isInSidePanel) { - closeSidePanel({afterTransition: options?.afterTransition}); + closeSidePanel(); return; } if (backTo === SCREENS.RIGHT_MODAL.SEARCH_REPORT) { - Navigation.goBack(undefined, options); + Navigation.goBack(); return; } if (prioritizeBackTo && backTo) { - Navigation.goBack(backTo as Route, options); + Navigation.goBack(backTo as Route); return; } if (isInNarrowPaneModal) { - Navigation.goBack(undefined, options); + Navigation.goBack(); return; } if (backTo) { - Navigation.goBack(backTo as Route, options); + Navigation.goBack(backTo as Route); return; } - Navigation.goBack(undefined, options); + Navigation.goBack(); }; if (isTransactionThreadView) { diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 824b0773b8c2..c1a24463e532 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -2,7 +2,8 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import {deepEqual} from 'fast-equals'; import type {RefObject} from 'react'; import React, {memo, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import {InteractionManager, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -327,7 +328,9 @@ function BaseReportActionContextMenu({ */ const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { if (isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false, () => { + hideContextMenu(false); + + InteractionManager.runAfterInteractions(() => { signOutAndRedirectToSignIn(); }); } else { diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 2ba50dbc0130..47bc9829077c 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -2,7 +2,8 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import type {ListRenderItemInfo} from '@shopify/flash-list'; import React, {memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; -import {DeviceEventEmitter, View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import {DeviceEventEmitter, InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {renderScrollComponent as renderActionSheetAwareScrollView} from '@components/ActionSheetAwareScrollView'; import InvertedFlashList from '@components/FlashList/InvertedFlashList'; @@ -27,7 +28,6 @@ import durationHighlightItem from '@libs/Navigation/helpers/getDurationHighlight import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import TransitionTracker from '@libs/Navigation/TransitionTracker'; import { getFirstVisibleReportActionID, getReportActionMessage, @@ -620,17 +620,13 @@ function ReportActionsList({ return; } - const handle = TransitionTracker.runAfterTransitions({ - callback: () => { - if (shouldFocusToTopOnMount) { - return; - } - setIsFloatingMessageCounterVisible(false); - reportScrollManager.scrollToBottom(); - }, - waitForUpcomingTransition: true, + InteractionManager.runAfterInteractions(() => { + if (shouldFocusToTopOnMount) { + return; + } + setIsFloatingMessageCounterVisible(false); + reportScrollManager.scrollToBottom(); }); - return () => handle.cancel(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -642,10 +638,8 @@ function ReportActionsList({ } const prevSorted = lastAction?.reportActionID ? prevSortedVisibleReportActionsObjects[lastAction?.reportActionID] : null; if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_TRACK_EXPENSE_WHISPER && !prevSorted) { - TransitionTracker.runAfterTransitions({ - callback: () => { - reportScrollManager.scrollToBottom(); - }, + InteractionManager.runAfterInteractions(() => { + reportScrollManager.scrollToBottom(); }); } }, [lastAction?.reportActionID, lastAction?.actionName, prevSortedVisibleReportActionsObjects, reportScrollManager]); @@ -669,10 +663,8 @@ function ReportActionsList({ if (lastIOUActionWithError?.reportActionID === prevLastIOUActionWithError?.reportActionID) { return; } - TransitionTracker.runAfterTransitions({ - callback: () => { - reportScrollManager.scrollToBottom(); - }, + InteractionManager.runAfterInteractions(() => { + reportScrollManager.scrollToBottom(); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastAction]); @@ -850,11 +842,7 @@ function ReportActionsList({ return; } - TransitionTracker.runAfterTransitions({ - callback: () => { - requestAnimationFrame(() => loadNewerChats(false)); - }, - }); + InteractionManager.runAfterInteractions(() => requestAnimationFrame(() => loadNewerChats(false))); }, [loadNewerChats]); const onEndReached = useCallback(() => { diff --git a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx index 7cdf71fe12e2..94aea6e9811f 100644 --- a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx @@ -1,6 +1,7 @@ import {Str} from 'expensify-common'; import React, {useEffect, useState} from 'react'; -import {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import {InteractionManager, View} from 'react-native'; import Button from '@components/Button'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import MenuItem from '@components/MenuItem'; @@ -78,16 +79,11 @@ function ConfirmationStep({route}: ConfirmationStepProps) { return; } + Navigation.dismissModal(); if (backTo) { - Navigation.dismissModal(); - Navigation.navigate(backTo, { - afterTransition: () => clearAssignCardStepAndData(), - }); - } else { - Navigation.dismissModal({ - afterTransition: () => clearAssignCardStepAndData(), - }); + Navigation.navigate(backTo); } + InteractionManager.runAfterInteractions(() => clearAssignCardStepAndData()); }, [assignCard?.isAssignmentFinished, backTo]); const submit = () => { diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index cbf6b28f31ec..da2040a7e640 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -117,9 +117,7 @@ jest.mock('@libs/Sound', () => ({ jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); jest.mock('@libs/Navigation/Navigation', () => ({ - navigate: jest.fn((_route: string, options?: {afterTransition?: () => void}) => { - options?.afterTransition?.(); - }), + navigate: jest.fn(), getActiveRoute: jest.fn().mockReturnValue(''), getActiveRouteWithoutParams: jest.fn(() => ''), dismissModalWithReport: jest.fn(), From b52fcf976a9bf63b8a3c78ceebde0eada33648d3 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 29 May 2026 12:50:58 -0700 Subject: [PATCH 2/2] Update eslint-seatbelt baseline for reverted runAfterInteractions usages --- config/eslint/eslint.seatbelt.tsv | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index c3c9a3110770..72141fca4bd2 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -123,6 +123,7 @@ "../../src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 3 "../../src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx" "react-hooks/refs" 6 "../../src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx" "react-hooks/set-state-in-effect" 3 +"../../src/components/MoneyRequestReportView/MoneyRequestReportView.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/components/MultiGestureCanvas/index.tsx" "no-restricted-syntax" 1 "../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/refs" 2 @@ -208,7 +209,7 @@ "../../src/hooks/useDialogContainerFocus/index.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/hooks/useDomainGroupFilter.ts" "react-hooks/set-state-in-effect" 1 "../../src/hooks/useDragAndDrop/types.ts" "@typescript-eslint/no-deprecated/React.MutableRefObject" 1 -"../../src/hooks/useExpenseActions.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2 +"../../src/hooks/useExpenseActions.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 3 "../../src/hooks/useFilesValidation.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 "../../src/hooks/useInitial.ts" "react-hooks/refs" 4 "../../src/hooks/useIsBlockedToAddFeed.ts" "react-hooks/set-state-in-effect" 1 @@ -386,7 +387,7 @@ "../../src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts" "no-restricted-syntax" 1 "../../src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts" "no-restricted-syntax" 1 "../../src/libs/actions/Report/MarkAllMessageAsRead.tsx" "no-restricted-syntax" 1 -"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 +"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2 "../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 3 "../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/reportAction.originalMessage" 1 "../../src/libs/actions/Report/index.ts" "no-restricted-syntax" 10 @@ -399,6 +400,7 @@ "../../src/libs/actions/Session/index.ts" "rulesdir/no-onyx-connect" 2 "../../src/libs/actions/StatsCounter.ts" "no-restricted-syntax" 2 "../../src/libs/actions/Subscription.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Task.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/libs/actions/Task.ts" "no-restricted-syntax" 7 "../../src/libs/actions/TaxRate.ts" "no-restricted-syntax" 1 "../../src/libs/actions/TeachersUnite.ts" "no-restricted-syntax" 1 @@ -479,6 +481,7 @@ "../../src/pages/inbox/hooks/useDeferNonEssentials.ts" "no-restricted-imports" 2 "../../src/pages/inbox/hooks/useFlushDeferredWriteOnFocus.ts" "no-restricted-imports" 1 "../../src/pages/inbox/hooks/useReportWasDeleted.ts" "react-hooks/set-state-in-effect" 1 +"../../src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx" "react-hooks/refs" 1 "../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 @@ -624,6 +627,7 @@ "../../src/pages/workspace/companyCards/WorkspaceVerifyWorkAccountPage.tsx" "react-hooks/set-state-in-effect" 1 "../../src/pages/workspace/companyCards/addNew/DynamicAddNewCardPage.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 "../../src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 +"../../src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1 "../../src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx" "react-hooks/preserve-manual-memoization" 1 "../../src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 "../../src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2