diff --git a/src/hooks/useLastAccessedReportID.ts b/src/hooks/useLastAccessedReportID.ts deleted file mode 100644 index c9dd4af6e5d8..000000000000 --- a/src/hooks/useLastAccessedReportID.ts +++ /dev/null @@ -1,150 +0,0 @@ -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 = {}; -let reportMetadata: OnyxCollection = {}; -let policies: OnyxCollection = {}; -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. - * @param skip - Whether to skip the calculation of last accessed report ID - */ -export default function useLastAccessedReportID(shouldOpenOnAdminRoom: boolean, skip: boolean) { - const {canUseDefaultRooms} = usePermissions(); - const {activeWorkspaceID} = useActiveWorkspace(); - const getSnapshot = useCallback(() => { - if (skip) { - return undefined; - } - const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); - return ReportUtils.findLastAccessedReport( - reports, - !canUseDefaultRooms, - policies, - isFirstTimeNewExpensifyUser, - shouldOpenOnAdminRoom, - reportMetadata, - activeWorkspaceID, - policyMemberAccountIDs, - )?.reportID; - }, [activeWorkspaceID, canUseDefaultRooms, shouldOpenOnAdminRoom, skip]); - // 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); -} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ee8e221e5aba..791719a211da 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -550,6 +550,26 @@ Onyx.connect({ }, }); +let allReportMetadata: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + return; + } + allReportMetadata = value; + }, +}); + +let isFirstTimeNewExpensifyUser = false; +Onyx.connect({ + key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, + callback: (value) => { + isFirstTimeNewExpensifyUser = value ?? false; + }, +}); + function getCurrentUserAvatar(): AvatarSource | undefined { return currentUserPersonalDetails?.avatar; } @@ -1150,30 +1170,23 @@ function hasExpensifyGuidesEmails(accountIDs: number[]): boolean { return accountIDs.some((accountID) => Str.extractEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); } -function findLastAccessedReport( - reports: OnyxCollection, - ignoreDomainRooms: boolean, - policies: OnyxCollection, - isFirstTimeNewExpensifyUser: boolean, - openOnAdminRoom = false, - reportMetadata: OnyxCollection = {}, - policyID?: string, - policyMemberAccountIDs: number[] = [], - excludeReportID?: string, -): OnyxEntry { +function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = false, policyID?: string, excludeReportID?: string): OnyxEntry { // If it's the user's first time using New Expensify, then they could either have: // - just a Concierge report, if so we'll return that // - their Concierge report, and a separate report that must have deeplinked them to the app before they created their account. // If it's the latter, we'll use the deeplinked report over the Concierge report, // since the Concierge report would be incorrectly selected over the deep-linked report in the logic below. - let reportsValues = Object.values(reports ?? {}); + const policyMemberAccountIDs = PolicyUtils.getPolicyEmployeeListByIdWithoutCurrentUser(allPolicies, policyID, currentUserAccountID); + + const allReports = ReportConnection.getAllReports(); + let reportsValues = Object.values(allReports ?? {}); if (!!policyID || policyMemberAccountIDs.length > 0) { reportsValues = filterReportsByPolicyIDAndMemberAccountIDs(reportsValues, policyMemberAccountIDs, policyID); } - let sortedReports = sortReportsByLastRead(reportsValues, reportMetadata); + let sortedReports = sortReportsByLastRead(reportsValues, allReportMetadata); let adminReport: OnyxEntry; if (openOnAdminRoom) { @@ -1197,7 +1210,7 @@ function findLastAccessedReport( if ( ignoreDomainRooms && isDomainRoom(report) && - getPolicyType(report, policies) !== CONST.POLICY.TYPE.FREE && + getPolicyType(report, allPolicies) !== CONST.POLICY.TYPE.FREE && !hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)) ) { return false; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4d674726540a..f3db60e25a7f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -222,13 +222,6 @@ Onyx.connect({ }, }); -let reportMetadata: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_METADATA, - waitForCollectionCallback: true, - callback: (value) => (reportMetadata = value), -}); - const typingWatchTimers: Record = {}; let reportIDDeeplinkedFromOldDot: string | undefined; @@ -2583,17 +2576,7 @@ function getCurrentUserAccountID(): number { } function navigateToMostRecentReport(currentReport: OnyxEntry) { - const lastAccessedReportID = ReportUtils.findLastAccessedReport( - ReportConnection.getAllReports(), - false, - undefined, - false, - false, - reportMetadata, - undefined, - [], - currentReport?.reportID, - )?.reportID; + const lastAccessedReportID = ReportUtils.findLastAccessedReport(false, false, undefined, currentReport?.reportID)?.reportID; if (lastAccessedReportID) { const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '-1'); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 41611c396907..da53ef56321c 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -20,12 +20,13 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import withCurrentReportID from '@components/withCurrentReportID'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; 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 usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; @@ -146,10 +147,12 @@ function ReportScreen({ const prevIsFocused = usePrevious(isFocused); const firstRenderRef = useRef(true); const flatListRef = useRef(null); + const {canUseDefaultRooms} = usePermissions(); const reactionListRef = useRef(null); const {isOffline} = useNetwork(); const isReportOpenInRHP = useIsReportOpenInRHP(); const {isSmallScreenWidth} = useWindowDimensions(); + const {activeWorkspaceID} = useActiveWorkspace(); const shouldUseNarrowLayout = isSmallScreenWidth || isReportOpenInRHP; const [modal] = useOnyx(ONYXKEYS.MODAL); @@ -169,9 +172,6 @@ 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, !!route.params.reportID); - useEffect(() => { // Don't update if there is a reportID in the params already if (route.params.reportID) { @@ -183,6 +183,8 @@ function ReportScreen({ return; } + const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; + // It's possible that reports aren't fully loaded yet // in that case the reportID is undefined if (!lastAccessedReportID) { @@ -191,7 +193,7 @@ function ReportScreen({ Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`); navigation.setParams({reportID: lastAccessedReportID}); - }, [lastAccessedReportID, navigation, route]); + }, [activeWorkspaceID, canUseDefaultRooms, navigation, route]); /** * Create a lightweight Report so as to keep the re-rendering as light as possible by diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 4130b6788894..34b9ece2994c 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -43,9 +43,11 @@ describe('ReportUtils', () => { safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], }); - Onyx.multiSet({ - ...mockedPoliciesMap, - ...mockedReportsMap, + beforeEach(async () => { + await Onyx.multiSet({ + ...mockedPoliciesMap, + ...mockedReportsMap, + }); }); }); @@ -55,13 +57,17 @@ describe('ReportUtils', () => { test('[ReportUtils] findLastAccessedReport on 2k reports and policies', async () => { const ignoreDomainRooms = true; - const isFirstTimeNewExpensifyUser = true; const reports = getMockedReports(2000); const policies = getMockedPolicies(2000); const openOnAdminRoom = true; + await Onyx.multiSet({ + [ONYXKEYS.COLLECTION.REPORT]: reports, + [ONYXKEYS.COLLECTION.POLICY]: policies, + }); + await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom)); + await measureFunction(() => ReportUtils.findLastAccessedReport(ignoreDomainRooms, openOnAdminRoom)); }); test('[ReportUtils] canDeleteReportAction on 1k reports and policies', async () => {