diff --git a/src/hooks/useLastAccessedReportID.ts b/src/hooks/useLastAccessedReportID.ts index 16a4a6bc2a31..d3e96905fdda 100644 --- a/src/hooks/useLastAccessedReportID.ts +++ b/src/hooks/useLastAccessedReportID.ts @@ -1,148 +1,17 @@ -import {useCallback, useSyncExternalStore} from 'react'; -import type {OnyxCollection} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import {useMemo} from 'react'; 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. */ -export default function useLastAccessedReportID(shouldOpenOnAdminRoom: boolean) { +export default function useLastAccessedReportID(shouldOpenOnAdminRoom: boolean): string | undefined { 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); + return useMemo( + () => ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom, activeWorkspaceID)?.reportID, + [activeWorkspaceID, canUseDefaultRooms, shouldOpenOnAdminRoom], + ); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b17fe7266079..e132f7f43b41 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -552,7 +552,6 @@ Onyx.connect({ }); let lastUpdatedReport: OnyxEntry; - Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (value) => { @@ -564,6 +563,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 getLastUpdatedReport(): OnyxEntry { return lastUpdatedReport; } @@ -1157,29 +1176,22 @@ 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[] = [], -): OnyxEntry { +function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = false, policyID?: 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 ?? {}) as Report[]; + const policyMemberAccountIDs = PolicyUtils.getPolicyEmployeeListByIdWithoutCurrentUser(allPolicies, policyID, currentUserAccountID); + + let reportsValues = Object.values(allReports ?? {}) as Report[]; 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) { @@ -1194,7 +1206,8 @@ function findLastAccessedReport( // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. // Domain rooms are now the only type of default room that are on the defaultRooms beta. sortedReports = sortedReports.filter( - (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)), + (report) => + !isDomainRoom(report) || getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)), ); } diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 4130b6788894..11c6d3772799 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -42,8 +42,10 @@ describe('ReportUtils', () => { keys: ONYXKEYS, safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], }); + }); - Onyx.multiSet({ + 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 () => {