Skip to content

Commit 7e10758

Browse files
authored
Merge pull request Expensify#86718 from callstack-internal/decompose/report-screen-6a
decompose ReportScreen 6a: extract route param + lifecycle handlers
2 parents 42eb490 + f518d68 commit 7e10758

3 files changed

Lines changed: 146 additions & 87 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {useIsFocused} from '@react-navigation/native';
2+
import {useEffect} from 'react';
3+
import useAppFocusEvent from '@hooks/useAppFocusEvent';
4+
import useBankAccountUnlockEffect from '@hooks/useBankAccountUnlockEffect';
5+
import {useCurrentReportIDState} from '@hooks/useCurrentReportID';
6+
import useOnyx from '@hooks/useOnyx';
7+
import usePrevious from '@hooks/usePrevious';
8+
import {hideEmojiPicker} from '@libs/actions/EmojiPickerAction';
9+
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
10+
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
11+
import {cancelSpan, cancelSpansByPrefix} from '@libs/telemetry/activeSpans';
12+
import CONST from '@src/CONST';
13+
import ONYXKEYS from '@src/ONYXKEYS';
14+
15+
type ReportLifecycleHandlerProps = {
16+
reportID: string | undefined;
17+
};
18+
19+
/**
20+
* Component that does not render anything. Handles screen lifecycle side effects:
21+
* - Hide emoji picker when screen loses focus
22+
* - Clear notifications when report is opened/re-focused
23+
* - Telemetry span cancellation on unmount
24+
* - Bank account unlock effect
25+
*/
26+
function ReportLifecycleHandler({reportID}: ReportLifecycleHandlerProps) {
27+
const onyxReportID = getNonEmptyStringOnyxID(reportID);
28+
const isFocused = useIsFocused();
29+
const prevIsFocused = usePrevious(isFocused);
30+
const {currentReportID: currentReportIDValue} = useCurrentReportIDState();
31+
const isTopMostReportId = currentReportIDValue === reportID;
32+
33+
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${onyxReportID}`);
34+
useBankAccountUnlockEffect(report);
35+
36+
// Hide emoji picker when screen loses focus
37+
useEffect(() => {
38+
if (!prevIsFocused || isFocused) {
39+
return;
40+
}
41+
hideEmojiPicker(true);
42+
}, [prevIsFocused, isFocused]);
43+
44+
// Telemetry cleanup
45+
useEffect(() => {
46+
return () => {
47+
// Cancel telemetry span when user leaves the screen before full report data is loaded
48+
cancelSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${onyxReportID}`);
49+
50+
// Cancel any pending send-message spans to prevent orphaned spans when navigating away
51+
cancelSpansByPrefix(CONST.TELEMETRY.SPAN_SEND_MESSAGE);
52+
};
53+
}, [onyxReportID]);
54+
55+
// Clear notifications for the current report when it's opened and re-focused
56+
const clearNotifications = () => {
57+
// Check if this is the top-most ReportScreen since the Navigator preserves multiple at a time
58+
if (!isTopMostReportId) {
59+
return;
60+
}
61+
62+
clearReportNotifications(onyxReportID);
63+
};
64+
65+
useEffect(clearNotifications, [clearNotifications]);
66+
useAppFocusEvent(clearNotifications);
67+
68+
return null;
69+
}
70+
71+
ReportLifecycleHandler.displayName = 'ReportLifecycleHandler';
72+
73+
export default ReportLifecycleHandler;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {useFocusEffect} from '@react-navigation/native';
2+
import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
3+
import usePermissions from '@hooks/usePermissions';
4+
import Log from '@libs/Log';
5+
import Navigation from '@libs/Navigation/Navigation';
6+
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
7+
import {findLastAccessedReport} from '@libs/ReportUtils';
8+
import {isNumeric} from '@libs/ValidationUtils';
9+
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types';
10+
import CONST from '@src/CONST';
11+
import type SCREENS from '@src/SCREENS';
12+
13+
type ReportRouteParamHandlerProps = {
14+
route:
15+
| PlatformStackScreenProps<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>['route']
16+
| PlatformStackScreenProps<RightModalNavigatorParamList, typeof SCREENS.RIGHT_MODAL.SEARCH_REPORT>['route'];
17+
navigation:
18+
| PlatformStackScreenProps<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>['navigation']
19+
| PlatformStackScreenProps<RightModalNavigatorParamList, typeof SCREENS.RIGHT_MODAL.SEARCH_REPORT>['navigation'];
20+
};
21+
22+
/**
23+
* Component that does not render anything. Resolves the reportID route param when missing,
24+
* and validates the reportActionID param.
25+
*/
26+
function ReportRouteParamHandler({route, navigation}: ReportRouteParamHandlerProps) {
27+
const {isBetaEnabled} = usePermissions();
28+
const archivedReportsIdSet = useArchivedReportsIdSet();
29+
30+
useFocusEffect(() => {
31+
// Don't update if there is a reportID in the params already
32+
if (route.params.reportID) {
33+
const reportActionID = route?.params?.reportActionID;
34+
const isValidReportActionID = reportActionID && isNumeric(reportActionID);
35+
if (reportActionID && !isValidReportActionID) {
36+
Navigation.isNavigationReady().then(() => navigation.setParams({reportActionID: ''}));
37+
}
38+
return;
39+
}
40+
41+
const lastAccessedReportID = findLastAccessedReport(
42+
!isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS),
43+
'openOnAdminRoom' in route.params && !!route.params.openOnAdminRoom,
44+
undefined,
45+
archivedReportsIdSet,
46+
)?.reportID;
47+
48+
// It's possible that reports aren't fully loaded yet
49+
// in that case the reportID is undefined
50+
if (!lastAccessedReportID) {
51+
return;
52+
}
53+
Navigation.isNavigationReady().then(() => {
54+
Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`);
55+
navigation.setParams({reportID: lastAccessedReportID});
56+
});
57+
});
58+
59+
return null;
60+
}
61+
62+
ReportRouteParamHandler.displayName = 'ReportRouteParamHandler';
63+
64+
export default ReportRouteParamHandler;

src/pages/inbox/ReportScreen.tsx

Lines changed: 9 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {PortalHost} from '@gorhom/portal';
2-
import {useFocusEffect, useIsFocused} from '@react-navigation/native';
2+
import {useIsFocused} from '@react-navigation/native';
33
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
44
import type {ViewStyle} from 'react-native';
55
// We use Animated for all functionality related to wide RHP to make it easier
66
// to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated.
77
// eslint-disable-next-line no-restricted-imports
8-
import {Animated, DeviceEventEmitter, InteractionManager, View} from 'react-native';
8+
import {Animated, InteractionManager, View} from 'react-native';
99
import type {OnyxEntry} from 'react-native-onyx';
1010
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
1111
import DragAndDropProvider from '@components/DragAndDrop/Provider';
@@ -20,9 +20,6 @@ import ScrollView from '@components/ScrollView';
2020
import useShowWideRHPVersion from '@components/WideRHPContextProvider/useShowWideRHPVersion';
2121
import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper';
2222
import useActionListContextValue from '@hooks/useActionListContextValue';
23-
import useAppFocusEvent from '@hooks/useAppFocusEvent';
24-
import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
25-
import useBankAccountUnlockEffect from '@hooks/useBankAccountUnlockEffect';
2623
import {useCurrentReportIDState} from '@hooks/useCurrentReportID';
2724
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
2825
import useDocumentTitle from '@hooks/useDocumentTitle';
@@ -34,7 +31,6 @@ import useNewTransactions from '@hooks/useNewTransactions';
3431
import useOnyx from '@hooks/useOnyx';
3532
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
3633
import useParentReportAction from '@hooks/useParentReportAction';
37-
import usePermissions from '@hooks/usePermissions';
3834
import usePrevious from '@hooks/usePrevious';
3935
import useReportIsArchived from '@hooks/useReportIsArchived';
4036
import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
@@ -43,13 +39,11 @@ import useSidePanelActions from '@hooks/useSidePanelActions';
4339
import useSubmitToDestinationVisible from '@hooks/useSubmitToDestinationVisible';
4440
import useThemeStyles from '@hooks/useThemeStyles';
4541
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
46-
import {hideEmojiPicker} from '@libs/actions/EmojiPickerAction';
4742
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
4843
import Log from '@libs/Log';
4944
import {getAllNonDeletedTransactions, shouldDisplayReportTableView, shouldWaitForTransactions as shouldWaitForTransactionsUtil} from '@libs/MoneyRequestReportUtils';
5045
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
5146
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
52-
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
5347
import {
5448
getFilteredReportActionsForReportView,
5549
getIOUActionForReportID,
@@ -63,7 +57,6 @@ import {
6357
import {getReportName} from '@libs/ReportNameUtils';
6458
import {
6559
canUserPerformWriteAction,
66-
findLastAccessedReport,
6760
getReportOfflinePendingActionAndErrors,
6861
getReportTransactions,
6962
isAdminRoom,
@@ -81,9 +74,7 @@ import {
8174
isTaskReport,
8275
isValidReportIDFromPath,
8376
} from '@libs/ReportUtils';
84-
import {cancelSpan, cancelSpansByPrefix} from '@libs/telemetry/activeSpans';
8577
import {getParentReportActionDeletionStatus} from '@libs/TransactionNavigationUtils';
86-
import {isNumeric} from '@libs/ValidationUtils';
8778
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types';
8879
import {setShouldShowComposeInput} from '@userActions/Composer';
8980
import {
@@ -111,6 +102,8 @@ import useReportWasDeleted from './hooks/useReportWasDeleted';
111102
import ReactionListWrapper from './ReactionListWrapper';
112103
import ReportActionsView from './report/ReportActionsView';
113104
import ReportFooter from './report/ReportFooter';
105+
import ReportLifecycleHandler from './ReportLifecycleHandler';
106+
import ReportRouteParamHandler from './ReportRouteParamHandler';
114107
import {ActionListContext} from './ReportScreenContext';
115108

116109
type ReportScreenNavigationProps =
@@ -157,9 +150,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
157150
const isFocused = useIsFocused();
158151
const prevIsFocused = usePrevious(isFocused);
159152
const [firstRender, setFirstRender] = useState(true);
160-
const isSkippingOpenReport = useRef(false);
161153
const hasCreatedLegacyThreadRef = useRef(false);
162-
const {isBetaEnabled} = usePermissions();
163154
const {isOffline} = useNetwork();
164155
const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout();
165156
const isInSidePanel = useIsInSidePanel();
@@ -178,8 +169,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
178169
const isSelfTourViewed = onboarding?.selfTourViewed;
179170
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
180171

181-
const archivedReportsIdSet = useArchivedReportsIdSet();
182-
183172
const parentReportAction = useParentReportAction(reportOnyx);
184173

185174
const deletedParentAction = isDeletedParentAction(parentReportAction);
@@ -190,37 +179,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
190179
const prevIsLoadingReportData = usePrevious(isLoadingReportData);
191180
const prevIsAnonymousUser = useRef(false);
192181

193-
useFocusEffect(
194-
useCallback(() => {
195-
// Don't update if there is a reportID in the params already
196-
if (route.params.reportID) {
197-
const reportActionID = route?.params?.reportActionID;
198-
const isValidReportActionID = reportActionID && isNumeric(reportActionID);
199-
if (reportActionID && !isValidReportActionID) {
200-
Navigation.isNavigationReady().then(() => navigation.setParams({reportActionID: ''}));
201-
}
202-
return;
203-
}
204-
205-
const lastAccessedReportID = findLastAccessedReport(
206-
!isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS),
207-
'openOnAdminRoom' in route.params && !!route.params.openOnAdminRoom,
208-
undefined,
209-
archivedReportsIdSet,
210-
)?.reportID;
211-
212-
// It's possible that reports aren't fully loaded yet
213-
// in that case the reportID is undefined
214-
if (!lastAccessedReportID) {
215-
return;
216-
}
217-
Navigation.isNavigationReady().then(() => {
218-
Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`);
219-
navigation.setParams({reportID: lastAccessedReportID});
220-
});
221-
}, [archivedReportsIdSet, isBetaEnabled, navigation, route.params]),
222-
);
223-
224182
/**
225183
* Create a lightweight Report so as to keep the re-rendering as light as possible by
226184
* passing in only the required props.
@@ -343,15 +301,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
343301

344302
const {closeSidePanel} = useSidePanelActions();
345303

346-
useBankAccountUnlockEffect(report);
347-
348-
useEffect(() => {
349-
if (!prevIsFocused || isFocused) {
350-
return;
351-
}
352-
hideEmojiPicker(true);
353-
}, [prevIsFocused, isFocused]);
354-
355304
const backTo = route?.params?.backTo as string;
356305
const onBackButtonPress = useCallback(
357306
(prioritizeBackTo = false) => {
@@ -648,38 +597,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
648597
updateLastVisitTime(reportID);
649598
}, [reportID, isFocused, isInSidePanel]);
650599

651-
useEffect(() => {
652-
const skipOpenReportListener = DeviceEventEmitter.addListener(`switchToPreExistingReport_${reportID}`, ({preexistingReportID}: {preexistingReportID: string}) => {
653-
if (!preexistingReportID) {
654-
return;
655-
}
656-
isSkippingOpenReport.current = true;
657-
});
658-
659-
return () => {
660-
skipOpenReportListener.remove();
661-
662-
// We need to cancel telemetry span when user leaves the screen before full report data is loaded
663-
cancelSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${reportID}`);
664-
665-
// Cancel any pending send-message spans to prevent orphaned spans when navigating away
666-
cancelSpansByPrefix(CONST.TELEMETRY.SPAN_SEND_MESSAGE);
667-
};
668-
}, [reportID]);
669-
670-
// Clear notifications for the current report when it's opened and re-focused
671-
const clearNotifications = useCallback(() => {
672-
// Check if this is the top-most ReportScreen since the Navigator preserves multiple at a time
673-
if (!isTopMostReportId) {
674-
return;
675-
}
676-
677-
clearReportNotifications(reportID);
678-
}, [reportID, isTopMostReportId]);
679-
680-
useEffect(clearNotifications, [clearNotifications]);
681-
useAppFocusEvent(clearNotifications);
682-
683600
useEffect(() => {
684601
// eslint-disable-next-line @typescript-eslint/no-deprecated
685602
const interactionTask = InteractionManager.runAfterInteractions(() => {
@@ -1043,6 +960,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
1043960
testID={`report-screen-${reportID}`}
1044961
>
1045962
<DeleteTransactionNavigateBackHandler />
963+
<ReportRouteParamHandler
964+
route={route}
965+
navigation={navigation}
966+
/>
1046967
<FullPageNotFoundView
1047968
shouldShow={shouldShowNotFoundPage}
1048969
subtitleKey={shouldShowNotFoundLinkedAction ? 'notFound.commentYouLookingForCannotBeFound' : 'notFound.noAccess'}
@@ -1056,6 +977,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
1056977
shouldDisplaySearchRouter
1057978
>
1058979
<DragAndDropProvider isDisabled={isEditingDisabled}>
980+
<ReportLifecycleHandler reportID={reportIDFromRoute} />
1059981
<OfflineWithFeedback
1060982
pendingAction={reportPendingAction ?? report?.pendingFields?.reimbursed}
1061983
errors={reportErrors}

0 commit comments

Comments
 (0)