diff --git a/src/App.tsx b/src/App.tsx index a3a9f7a3f3b6..6316fa80fba1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; +import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; @@ -78,6 +79,7 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, + ReportIDsContextProvider, PlaybackContextProvider, FullScreenContextProvider, VolumeContextProvider, diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index a452e7565b4e..a72063913283 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,7 +39,17 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); + const reportID = Navigation.getTopmostReportId(state) ?? ''; + + /* + * Make sure we don't make the reportID undefined when switching between the chat list and settings tab. + * This helps prevent unnecessary re-renders. + */ + const params = state?.routes?.[state.index]?.params; + if (params && 'screen' in params && typeof params.screen === 'string' && params.screen.indexOf('Settings_') !== -1) { + return; + } + setCurrentReportID(reportID); }, [setCurrentReportID], ); diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx new file mode 100644 index 000000000000..58d4e42cd83b --- /dev/null +++ b/src/hooks/useReportIDs.tsx @@ -0,0 +1,174 @@ +import React, {createContext, useCallback, useContext, useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import SidebarUtils from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Message} from '@src/types/onyx/ReportAction'; +import useActiveWorkspace from './useActiveWorkspace'; +import useCurrentReportID from './useCurrentReportID'; +import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; + +type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; +type PolicySelector = Pick<OnyxTypes.Policy, 'type' | 'name' | 'avatar' | 'employeeList'>; +type ReportActionsSelector = Array<Pick<OnyxTypes.ReportAction, 'reportActionID' | 'actionName' | 'errors' | 'message' | 'originalMessage'>>; + +type ReportIDsContextProviderProps = { + children: React.ReactNode; + currentReportIDForTests?: string; +}; + +type ReportIDsContextValue = { + orderedReportIDs: string[]; + currentReportID: string; +}; + +const ReportIDsContext = createContext<ReportIDsContextValue>({ + orderedReportIDs: [], + currentReportID: '', +}); + +/** + * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering + * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. + */ +const chatReportSelector = (report: OnyxEntry<OnyxTypes.Report>): ChatReportSelector => + (report && { + reportID: report.reportID, + participantAccountIDs: report.participantAccountIDs, + isPinned: report.isPinned, + isHidden: report.isHidden, + notificationPreference: report.notificationPreference, + errorFields: { + addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, + }, + lastMessageText: report.lastMessageText, + lastVisibleActionCreated: report.lastVisibleActionCreated, + iouReportID: report.iouReportID, + total: report.total, + nonReimbursableTotal: report.nonReimbursableTotal, + hasOutstandingChildRequest: report.hasOutstandingChildRequest, + isWaitingOnBankAccount: report.isWaitingOnBankAccount, + statusNum: report.statusNum, + stateNum: report.stateNum, + chatType: report.chatType, + type: report.type, + policyID: report.policyID, + visibility: report.visibility, + lastReadTime: report.lastReadTime, + // Needed for name sorting: + reportName: report.reportName, + policyName: report.policyName, + oldPolicyName: report.oldPolicyName, + // Other less obvious properites considered for sorting: + ownerAccountID: report.ownerAccountID, + currency: report.currency, + managerID: report.managerID, + // Other important less obivous properties for filtering: + parentReportActionID: report.parentReportActionID, + parentReportID: report.parentReportID, + isDeletedParentAction: report.isDeletedParentAction, + isUnreadWithMention: ReportUtils.isUnreadWithMention(report), + }) as ChatReportSelector; + +const reportActionsSelector = (reportActions: OnyxEntry<OnyxTypes.ReportActions>): ReportActionsSelector => + (reportActions && + Object.values(reportActions).map((reportAction) => { + const {reportActionID, actionName, errors = [], originalMessage} = reportAction; + const decision = reportAction.message?.[0]?.moderationDecision?.decision; + + return { + reportActionID, + actionName, + errors, + message: [ + { + moderationDecision: {decision}, + }, + ] as Message[], + originalMessage, + }; + })) as ReportActionsSelector; + +const policySelector = (policy: OnyxEntry<OnyxTypes.Policy>): PolicySelector => + (policy && { + type: policy.type, + name: policy.name, + avatar: policy.avatar, + employeeList: policy.employeeList, + }) as PolicySelector; + +function ReportIDsContextProvider({ + children, + /** + * Only required to make unit tests work, since we + * explicitly pass the currentReportID in LHNTestUtils + * to SidebarLinksData, so this context doesn't have + * access to currentReportID in that case. + * + * This is a workaround to have currentReportID available in testing environment. + */ + currentReportIDForTests, +}: ReportIDsContextProviderProps) { + const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {initialValue: CONST.PRIORITY_MODE.DEFAULT}); + const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: chatReportSelector}); + const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policySelector}); + const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: reportActionsSelector}); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [reportsDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); + const [betas] = useOnyx(ONYXKEYS.BETAS); + + const {accountID} = useCurrentUserPersonalDetails(); + const currentReportIDValue = useCurrentReportID(); + const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID; + const {activeWorkspaceID} = useActiveWorkspace(); + + const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); + + const getOrderedReportIDs = useCallback( + (currentReportID?: string) => + SidebarUtils.getOrderedReportIDs( + currentReportID ?? null, + chatReports, + betas, + policies, + priorityMode, + allReportActions, + transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + ), + // we need reports draft in deps array for reloading of list when reportsDrafts will change + // eslint-disable-next-line react-hooks/exhaustive-deps + [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, reportsDrafts], + ); + + const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); + const contextValue: ReportIDsContextValue = useMemo(() => { + // We need to make sure the current report is in the list of reports, but we do not want + // to have to re-generate the list every time the currentReportID changes. To do that + // we first generate the list as if there was no current report, then we check if + // the current report is missing from the list, which should very rarely happen. In this + // case we re-generate the list a 2nd time with the current report included. + if (derivedCurrentReportID && !orderedReportIDs.includes(derivedCurrentReportID)) { + return {orderedReportIDs: getOrderedReportIDs(derivedCurrentReportID), currentReportID: derivedCurrentReportID ?? ''}; + } + + return { + orderedReportIDs, + currentReportID: derivedCurrentReportID ?? '', + }; + }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID]); + + return <ReportIDsContext.Provider value={contextValue}>{children}</ReportIDsContext.Provider>; +} + +function useReportIDs() { + return useContext(ReportIDsContext); +} + +export {ReportIDsContext, ReportIDsContextProvider, policySelector, useReportIDs}; +export type {ChatReportSelector, PolicySelector, ReportActionsSelector}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 0d85c7a3c313..d230f58e46f9 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,14 +1,14 @@ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import type {ChatReportSelector, PolicySelector, ReportActionsSelector} from '@hooks/useReportIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, ReportActions, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; +import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; -import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import * as CollectionUtils from './CollectionUtils'; @@ -61,11 +61,11 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 { */ function getOrderedReportIDs( currentReportId: string | null, - allReports: OnyxCollection<Report>, + allReports: OnyxCollection<ChatReportSelector>, betas: OnyxEntry<Beta[]>, - policies: OnyxCollection<Policy>, - priorityMode: OnyxEntry<ValueOf<typeof CONST.PRIORITY_MODE>>, - allReportActions: OnyxCollection<ReportAction[]>, + policies: OnyxCollection<PolicySelector>, + priorityMode: OnyxEntry<PriorityMode>, + allReportActions: OnyxCollection<ReportActionsSelector>, transactionViolations: OnyxCollection<TransactionViolation[]>, currentPolicyID = '', policyMemberAccountIDs: number[] = [], @@ -87,7 +87,7 @@ function getOrderedReportIDs( const doesReportHaveViolations = !!( betas?.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && - ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction) + ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction as OnyxEntry<ReportAction>) ); const isHidden = report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; const isFocused = report.reportID === currentReportId; @@ -103,7 +103,7 @@ function getOrderedReportIDs( currentReportId: currentReportId ?? '', isInGSDMode, betas, - policies, + policies: policies as OnyxCollection<Policy>, excludeEmptyChats: true, doesReportHaveViolations, includeSelfDM: true, @@ -130,13 +130,13 @@ function getOrderedReportIDs( ); } // There are a few properties that need to be calculated for the report which are used when sorting reports. - reportsToDisplay.forEach((report) => { - // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. - // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add - // the reportDisplayName property to the report object directly. + reportsToDisplay.forEach((reportToDisplay) => { + let report = reportToDisplay as OnyxEntry<Report>; if (report) { - // eslint-disable-next-line no-param-reassign - report.displayName = ReportUtils.getReportName(report); + report = { + ...report, + displayName: ReportUtils.getReportName(report), + }; } const isPinned = report?.isPinned ?? false; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 1000ceff1a76..46f7d2410ffe 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -1,152 +1,56 @@ import {useIsFocused} from '@react-navigation/native'; -import {deepEqual} from 'fast-equals'; import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {memo, useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import type {ValueOf} from 'type-fest'; -import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; -import withCurrentReportID from '@components/withCurrentReportID'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePrevious from '@hooks/usePrevious'; +import type {PolicySelector} from '@hooks/useReportIDs'; +import {policySelector, useReportIDs} from '@hooks/useReportIDs'; import useThemeStyles from '@hooks/useThemeStyles'; import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import SidebarUtils from '@libs/SidebarUtils'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {Message} from '@src/types/onyx/ReportAction'; import SidebarLinks from './SidebarLinks'; -type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; -type PolicySelector = Pick<OnyxTypes.Policy, 'type' | 'name' | 'avatar' | 'employeeList'>; -type ReportActionsSelector = Array<Pick<OnyxTypes.ReportAction, 'reportActionID' | 'actionName' | 'errors' | 'message' | 'originalMessage'>>; - type SidebarLinksDataOnyxProps = { - /** List of reports */ - chatReports: OnyxCollection<ChatReportSelector>; - /** Whether the reports are loading. When false it means they are ready to be used. */ isLoadingApp: OnyxEntry<boolean>; /** The chat priority mode */ priorityMode: OnyxEntry<ValueOf<typeof CONST.PRIORITY_MODE>>; - /** Beta features list */ - betas: OnyxEntry<OnyxTypes.Beta[]>; - - /** All report actions for all reports */ - allReportActions: OnyxCollection<ReportActionsSelector>; - /** The policies which the user has access to */ policies: OnyxCollection<PolicySelector>; - - /** All of the transaction violations */ - transactionViolations: OnyxCollection<OnyxTypes.TransactionViolations>; - - /** Drafts of reports */ - reportsDrafts: OnyxCollection<string>; }; -type SidebarLinksDataProps = CurrentReportIDContextValue & - SidebarLinksDataOnyxProps & { - /** Toggles the navigation menu open and closed */ - onLinkClick: () => void; +type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { + /** Toggles the navigation menu open and closed */ + onLinkClick: () => void; - /** Safe area insets required for mobile devices margins */ - insets: EdgeInsets; - }; + /** Safe area insets required for mobile devices margins */ + insets: EdgeInsets; +}; -function SidebarLinksData({ - allReportActions, - betas, - chatReports, - currentReportID, - insets, - isLoadingApp = true, - onLinkClick, - policies, - priorityMode = CONST.PRIORITY_MODE.DEFAULT, - transactionViolations, - reportsDrafts, -}: SidebarLinksDataProps) { +function SidebarLinksData({insets, isLoadingApp = true, onLinkClick, priorityMode = CONST.PRIORITY_MODE.DEFAULT, policies}: SidebarLinksDataProps) { const {accountID} = useCurrentUserPersonalDetails(); - const network = useNetwork(); const isFocused = useIsFocused(); const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); - const prevPriorityMode = usePrevious(priorityMode); const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => Policy.openWorkspace(activeWorkspaceID ?? '', policyMemberAccountIDs), [activeWorkspaceID]); - const reportIDsRef = useRef<string[] | null>(null); const isLoading = isLoadingApp; - - const optionItemsMemoized: string[] = useMemo( - () => - SidebarUtils.getOrderedReportIDs( - null, - chatReports, - betas, - policies as OnyxCollection<OnyxTypes.Policy>, - priorityMode, - allReportActions as OnyxCollection<OnyxTypes.ReportAction[]>, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ), - // we need reports draft in deps array for reloading of list when reportDrafts will change - // eslint-disable-next-line react-hooks/exhaustive-deps - [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, reportsDrafts], - ); - - const optionListItems: string[] | null = useMemo(() => { - const reportIDs = optionItemsMemoized; - - if (deepEqual(reportIDsRef.current, reportIDs)) { - return reportIDsRef.current; - } - - // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 - // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. - // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !reportIDsRef.current || network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { - reportIDsRef.current = reportIDs; - } - return reportIDsRef.current ?? []; - }, [optionItemsMemoized, priorityMode, isLoading, network.isOffline, prevPriorityMode]); - // We need to make sure the current report is in the list of reports, but we do not want - // to have to re-generate the list every time the currentReportID changes. To do that - // we first generate the list as if there was no current report, then here we check if - // the current report is missing from the list, which should very rarely happen. In this - // case we re-generate the list a 2nd time with the current report included. - const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportID && !optionListItems?.includes(currentReportID)) { - return SidebarUtils.getOrderedReportIDs( - currentReportID, - chatReports as OnyxCollection<OnyxTypes.Report>, - betas, - policies as OnyxCollection<OnyxTypes.Policy>, - priorityMode, - allReportActions as OnyxCollection<OnyxTypes.ReportAction[]>, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); - } - return optionListItems ?? []; - }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs]); + const {orderedReportIDs, currentReportID} = useReportIDs(); const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; @@ -167,8 +71,8 @@ function SidebarLinksData({ // Data props: isActiveReport={isActiveReport} isLoading={isLoading ?? false} - optionListItems={optionListItemsWithCurrentReport} activeWorkspaceID={activeWorkspaceID} + optionListItems={orderedReportIDs} /> </View> ); @@ -176,105 +80,7 @@ function SidebarLinksData({ SidebarLinksData.displayName = 'SidebarLinksData'; -/** - * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering - * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - */ -const chatReportSelector = (report: OnyxEntry<OnyxTypes.Report>): ChatReportSelector => - (report && { - reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, - }, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }) as ChatReportSelector; - -const reportActionsSelector = (reportActions: OnyxEntry<OnyxTypes.ReportActions>): ReportActionsSelector => - (reportActions && - Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = reportAction.message?.[0]?.moderationDecision?.decision; - - return { - reportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ] as Message[], - originalMessage, - }; - })) as ReportActionsSelector; - -const policySelector = (policy: OnyxEntry<OnyxTypes.Policy>): PolicySelector => - (policy && { - type: policy.type, - name: policy.name, - avatar: policy.avatar, - employeeList: policy.employeeList, - }) as PolicySelector; - -const SidebarLinkDataWithCurrentReportID = withCurrentReportID( - /* - While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 2 additional run of getOrderedReportIDs. - With that we can reduce app start up time by ~2s on heavy account. - More details - https://github.com/Expensify/App/issues/35234#issuecomment-1926914534 - */ - memo( - SidebarLinksData, - (prevProps, nextProps) => - lodashIsEqual(prevProps.chatReports, nextProps.chatReports) && - lodashIsEqual(prevProps.allReportActions, nextProps.allReportActions) && - prevProps.isLoadingApp === nextProps.isLoadingApp && - prevProps.priorityMode === nextProps.priorityMode && - lodashIsEqual(prevProps.betas, nextProps.betas) && - lodashIsEqual(prevProps.policies, nextProps.policies) && - lodashIsEqual(prevProps.insets, nextProps.insets) && - prevProps.onLinkClick === nextProps.onLinkClick && - lodashIsEqual(prevProps.transactionViolations, nextProps.transactionViolations) && - prevProps.currentReportID === nextProps.currentReportID && - lodashIsEqual(prevProps.reportsDrafts, nextProps.reportsDrafts), - ), -); - -export default withOnyx<Omit<SidebarLinksDataProps, 'currentReportID' | 'updateCurrentReportID'>, SidebarLinksDataOnyxProps>({ - chatReports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, - initialValue: {}, - }, +export default withOnyx<SidebarLinksDataProps, SidebarLinksDataOnyxProps>({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, @@ -282,26 +88,24 @@ export default withOnyx<Omit<SidebarLinksDataProps, 'currentReportID' | 'updateC key: ONYXKEYS.NVP_PRIORITY_MODE, initialValue: CONST.PRIORITY_MODE.DEFAULT, }, - betas: { - key: ONYXKEYS.BETAS, - initialValue: [], - }, - allReportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - selector: reportActionsSelector, - initialValue: {}, - }, policies: { key: ONYXKEYS.COLLECTION.POLICY, selector: policySelector, initialValue: {}, }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - initialValue: {}, - }, - reportsDrafts: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - initialValue: {}, - }, -})(SidebarLinkDataWithCurrentReportID); +})( + /* +While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 2 additional run of getOrderedReportIDs. +With that we can reduce app start up time by ~2s on heavy account. +More details - https://github.com/Expensify/App/issues/35234#issuecomment-1926914534 +*/ + memo( + SidebarLinksData, + (prevProps, nextProps) => + prevProps.isLoadingApp === nextProps.isLoadingApp && + prevProps.priorityMode === nextProps.priorityMode && + lodashIsEqual(prevProps.insets, nextProps.insets) && + prevProps.onLinkClick === nextProps.onLinkClick && + lodashIsEqual(prevProps.policies, nextProps.policies), + ), +); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 6e5640d21ff9..f0cb062fb4b3 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -115,6 +115,8 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } +const stickyHeaderIndices = [0]; + function WorkspacesListPage({policies, reimbursementAccount, reports, session}: WorkspaceListPageProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -422,7 +424,7 @@ function WorkspacesListPage({policies, reimbursementAccount, reports, session}: data={workspaces} renderItem={getMenuItem} ListHeaderComponent={listHeaderComponent} - stickyHeaderIndices={[0]} + stickyHeaderIndices={stickyHeaderIndices} /> </View> <ConfirmModal diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index b19e59dbdcc0..75503e5179a4 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -2,12 +2,12 @@ import {rand} from '@ngneat/falso'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; +import type {ChatReportSelector} from '@hooks/useReportIDs'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, TransactionViolation} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; -import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; @@ -20,7 +20,7 @@ const REPORTS_COUNT = 15000; const REPORT_TRESHOLD = 5; const PERSONAL_DETAILS_LIST_COUNT = 1000; -const allReports = createCollection<Report>( +const allReports = createCollection<ChatReportSelector>( (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, (index) => ({ ...createRandomReport(index), @@ -29,6 +29,7 @@ const allReports = createCollection<Report>( // add status and state to every 5th report to mock nonarchived reports statusNum: index % REPORT_TRESHOLD ? 0 : CONST.REPORT.STATUS_NUM.CLOSED, stateNum: index % REPORT_TRESHOLD ? 0 : CONST.REPORT.STATE_NUM.APPROVED, + isUnreadWithMention: false, }), REPORTS_COUNT, ); diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 868630a8f7d2..fd39d4efef65 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -299,7 +299,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, ...reportCollectionDataSet, }), ) @@ -363,7 +363,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, ...reportCollectionDataSet, }), ) @@ -430,7 +430,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.POLICY}${fakeReport.policyID}`]: fakePolicy, ...reportCollectionDataSet, }), @@ -785,10 +785,12 @@ describe('Sidebar', () => { // When a new report is added .then(() => - Promise.all([ - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report4.reportID}`, report4), - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report4.reportID}`, 'report4 draft'), - ]), + Onyx.multiSet({ + ...reportDraftCommentCollectionDataSet, + [`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report4.reportID}`]: 'report4 draft', + ...reportCollectionDataSet, + [`${ONYXKEYS.COLLECTION.REPORT}${report4.reportID}`]: report4, + }), ) // Then they are still in alphabetical order diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 248b0f946edc..e3daa93a3179 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -8,6 +8,7 @@ import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxProvider from '@components/OnyxProvider'; import {CurrentReportIDContextProvider} from '@components/withCurrentReportID'; import {EnvironmentProvider} from '@components/withEnvironment'; +import {ReportIDsContextProvider} from '@hooks/useReportIDs'; import DateUtils from '@libs/DateUtils'; import ReportActionItemSingle from '@pages/home/report/ReportActionItemSingle'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; @@ -278,17 +279,26 @@ function getFakeAdvancedReportAction(actionName: ActionName = 'IOU', actor = 'em function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { return ( <ComposeProviders components={[OnyxProvider, LocaleContextProvider, EnvironmentProvider, CurrentReportIDContextProvider]}> - <SidebarLinksData - onLinkClick={() => {}} - insets={{ - top: 0, - left: 0, - right: 0, - bottom: 0, - }} - // @ts-expect-error - we need this prop to be able to test the component but normally its provided by HOC - currentReportID={currentReportID} - /> + {/* + * Only required to make unit tests work, since we + * explicitly pass the currentReportID in LHNTestUtils + * to SidebarLinksData, so this context doesn't have an + * access to currentReportID in that case. + * + * So this is a work around to have currentReportID available + * only in testing environment. + * */} + <ReportIDsContextProvider currentReportIDForTests={currentReportID}> + <SidebarLinksData + onLinkClick={() => {}} + insets={{ + top: 0, + left: 0, + right: 0, + bottom: 0, + }} + /> + </ReportIDsContextProvider> </ComposeProviders> ); }