Skip to content

Commit

Permalink
Merge pull request Expensify#44559 from Expensify/Rory-ReportScreenWr…
Browse files Browse the repository at this point in the history
…apper

[CP Stg] Replace ReportScreenIDSetter with useLastAccessedReportID
roryabraham authored Jun 27, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 52859df + d178103 commit 359dbe6
Showing 8 changed files with 189 additions and 129 deletions.
2 changes: 1 addition & 1 deletion src/components/ScreenWrapper.tsx
Original file line number Diff line number Diff line change
@@ -131,7 +131,7 @@ function ScreenWrapper(
) {
/**
* We are only passing navigation as prop from
* ReportScreenWrapper -> ReportScreen -> ScreenWrapper
* ReportScreen -> ScreenWrapper
*
* so in other places where ScreenWrapper is used, we need to
* fallback to useNavigation.
148 changes: 148 additions & 0 deletions src/hooks/useLastAccessedReportID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {useCallback, useSyncExternalStore} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportMetadata} from '@src/types/onyx';
import useActiveWorkspace from './useActiveWorkspace';
import usePermissions from './usePermissions';

/*
* This hook is used to get the lastAccessedReportID.
* This is a piece of data that's derived from a lot of frequently-changing Onyx values: (reports, reportMetadata, policies, etc...)
* We don't want any component that needs access to the lastAccessedReportID to have to re-render any time any of those values change, just when the lastAccessedReportID changes.
* So we have a custom implementation in this file that leverages useSyncExternalStore to connect to a "store" of multiple Onyx values, and re-render only when the one derived value changes.
*/

const subscribers: Array<() => void> = [];

let reports: OnyxCollection<Report> = {};
let reportMetadata: OnyxCollection<ReportMetadata> = {};
let policies: OnyxCollection<Policy> = {};
let accountID: number | undefined;
let isFirstTimeNewExpensifyUser = false;

let reportsConnection: number;
let reportMetadataConnection: number;
let policiesConnection: number;
let accountIDConnection: number;
let isFirstTimeNewExpensifyUserConnection: number;

function notifySubscribers() {
subscribers.forEach((subscriber) => subscriber());
}

function subscribeToOnyxData() {
// eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
reportsConnection = Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
reports = value;
notifySubscribers();
},
});
// eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
reportMetadataConnection = Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
waitForCollectionCallback: true,
callback: (value) => {
reportMetadata = value;
notifySubscribers();
},
});
// eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
policiesConnection = Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => {
policies = value;
notifySubscribers();
},
});
// eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
accountIDConnection = Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (value) => {
accountID = value?.accountID;
notifySubscribers();
},
});
// eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
isFirstTimeNewExpensifyUserConnection = Onyx.connect({
key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
callback: (value) => {
isFirstTimeNewExpensifyUser = !!value;
notifySubscribers();
},
});
}

function unsubscribeFromOnyxData() {
if (reportsConnection) {
Onyx.disconnect(reportsConnection);
reportsConnection = 0;
}
if (reportMetadataConnection) {
Onyx.disconnect(reportMetadataConnection);
reportMetadataConnection = 0;
}
if (policiesConnection) {
Onyx.disconnect(policiesConnection);
policiesConnection = 0;
}
if (accountIDConnection) {
Onyx.disconnect(accountIDConnection);
accountIDConnection = 0;
}
if (isFirstTimeNewExpensifyUserConnection) {
Onyx.disconnect(isFirstTimeNewExpensifyUserConnection);
isFirstTimeNewExpensifyUserConnection = 0;
}
}

function removeSubscriber(subscriber: () => void) {
const subscriberIndex = subscribers.indexOf(subscriber);
if (subscriberIndex < 0) {
return;
}
subscribers.splice(subscriberIndex, 1);
if (subscribers.length === 0) {
unsubscribeFromOnyxData();
}
}

function addSubscriber(subscriber: () => void) {
subscribers.push(subscriber);
if (!reportsConnection) {
subscribeToOnyxData();
}
return () => removeSubscriber(subscriber);
}

/**
* Get the last accessed reportID.
*/
export default function useLastAccessedReportID(shouldOpenOnAdminRoom: boolean) {
const {canUseDefaultRooms} = usePermissions();
const {activeWorkspaceID} = useActiveWorkspace();

const getSnapshot = useCallback(() => {
const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID);
return ReportUtils.findLastAccessedReport(
reports,
!canUseDefaultRooms,
policies,
isFirstTimeNewExpensifyUser,
shouldOpenOnAdminRoom,
reportMetadata,
activeWorkspaceID,
policyMemberAccountIDs,
)?.reportID;
}, [activeWorkspaceID, canUseDefaultRooms, shouldOpenOnAdminRoom]);

// We need access to all the data from these Onyx.connect calls, but we don't want to re-render the consuming component
// unless the derived value (lastAccessedReportID) changes. To address these, we'll wrap everything with useSyncExternalStore
return useSyncExternalStore(addSubscriber, getSnapshot);
}
17 changes: 12 additions & 5 deletions src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import Onyx, {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import OptionsListContextProvider from '@components/OptionListContextProvider';
import useLastAccessedReportID from '@hooks/useLastAccessedReportID';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -77,16 +78,21 @@ const loadReportAvatar = () => require<ReactComponentModule>('../../../pages/Rep
const loadReceiptView = () => require<ReactComponentModule>('../../../pages/TransactionReceiptPage').default;
const loadWorkspaceJoinUser = () => require<ReactComponentModule>('@pages/workspace/WorkspaceJoinUserPage').default;

function getCentralPaneScreenInitialParams(screenName: CentralPaneName): Partial<ValueOf<CentralPaneScreensParamList>> {
function shouldOpenOnAdminRoom() {
const url = getCurrentUrl();
const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined;
return url ? new URL(url).searchParams.get('openOnAdminRoom') === 'true' : false;
}

function getCentralPaneScreenInitialParams(screenName: CentralPaneName, lastAccessedReportID?: string): Partial<ValueOf<CentralPaneScreensParamList>> {
if (screenName === SCREENS.SEARCH.CENTRAL_PANE) {
return {sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC};
}

if (screenName === SCREENS.REPORT && openOnAdminRoom === 'true') {
return {openOnAdminRoom: true};
if (screenName === SCREENS.REPORT) {
return {
openOnAdminRoom: shouldOpenOnAdminRoom() ? true : undefined,
reportID: lastAccessedReportID,
};
}

return undefined;
@@ -192,6 +198,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
const StyleUtils = useStyleUtils();
const {isSmallScreenWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useOnboardingLayout();
const lastAccessedReportID = useLastAccessedReportID(shouldOpenOnAdminRoom());
const screenOptions = getRootNavigatorScreenOptions(isSmallScreenWidth, styles, StyleUtils);
const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(shouldUseNarrowLayout), [screenOptions, shouldUseNarrowLayout]);
const onboardingScreenOptions = useMemo(
@@ -467,7 +474,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
<RootStack.Screen
key={centralPaneName}
name={centralPaneName}
initialParams={getCentralPaneScreenInitialParams(centralPaneName)}
initialParams={getCentralPaneScreenInitialParams(centralPaneName, lastAccessedReportID)}
getComponent={componentGetter}
options={CentralPaneScreenOptions}
/>
2 changes: 1 addition & 1 deletion src/libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS.tsx
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ const CENTRAL_PANE_SCREENS = {
[SCREENS.SETTINGS.SAVE_THE_WORLD]: withPrepareCentralPaneScreen(() => require<ReactComponentModule>('../../../pages/TeachersUnite/SaveTheWorldPage').default),
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: withPrepareCentralPaneScreen(() => require<ReactComponentModule>('../../../pages/settings/Subscription/SubscriptionSettingsPage').default),
[SCREENS.SEARCH.CENTRAL_PANE]: withPrepareCentralPaneScreen(() => require<ReactComponentModule>('../../../pages/Search/SearchPage').default),
[SCREENS.REPORT]: withPrepareCentralPaneScreen(() => require<ReactComponentModule>('./ReportScreenWrapper').default),
[SCREENS.REPORT]: withPrepareCentralPaneScreen(() => require<ReactComponentModule>('../../../pages/home/ReportScreen').default),
} satisfies Screens;

export default CENTRAL_PANE_SCREENS;
91 changes: 0 additions & 91 deletions src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts

This file was deleted.

30 changes: 0 additions & 30 deletions src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
@@ -407,7 +407,7 @@ function isNumeric(value: string): boolean {
if (typeof value !== 'string') {
return false;
}
return /^\d*$/.test(value);
return CONST.REGEX.NUMBER.test(value);
}

/**
26 changes: 26 additions & 0 deletions src/pages/home/ReportScreen.tsx
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import withCurrentReportID from '@components/withCurrentReportID';
import useAppFocusEvent from '@hooks/useAppFocusEvent';
import useDeepCompareRef from '@hooks/useDeepCompareRef';
import useIsReportOpenInRHP from '@hooks/useIsReportOpenInRHP';
import useLastAccessedReportID from '@hooks/useLastAccessedReportID';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
@@ -31,13 +32,15 @@ import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {getCurrentUserAccountID} from '@libs/actions/Report';
import Timing from '@libs/actions/Timing';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
import Performance from '@libs/Performance';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import shouldFetchReport from '@libs/shouldFetchReport';
import * as ValidationUtils from '@libs/ValidationUtils';
import type {AuthScreensParamList} from '@navigation/types';
import variables from '@styles/variables';
import * as ComposerActions from '@userActions/Composer';
@@ -161,6 +164,29 @@ function ReportScreen({
const isLoadingReportOnyx = isLoadingOnyxValue(reportResult);
const permissions = useDeepCompareRef(reportOnyx?.permissions);

// Check if there's a reportID in the route. If not, set it to the last accessed reportID
const lastAccessedReportID = useLastAccessedReportID(!!route.params.openOnAdminRoom);
useEffect(() => {
// Don't update if there is a reportID in the params already
if (route.params.reportID) {
const reportActionID = route?.params?.reportActionID;
const isValidReportActionID = ValidationUtils.isNumeric(reportActionID);
if (reportActionID && !isValidReportActionID) {
navigation.setParams({reportActionID: ''});
}
return;
}

// It's possible that reports aren't fully loaded yet
// in that case the reportID is undefined
if (!lastAccessedReportID) {
return;
}

Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`);
navigation.setParams({reportID: lastAccessedReportID});
}, [lastAccessedReportID, navigation, route]);

/**
* Create a lightweight Report so as to keep the re-rendering as light as possible by
* passing in only the required props.

0 comments on commit 359dbe6

Please sign in to comment.