Skip to content

Commit

Permalink
Merge pull request Expensify#36420 from callstack-internal/hur/perf/i…
Browse files Browse the repository at this point in the history
…ssue-35704

perf: make switch between chat list and workspaces smoother
  • Loading branch information
roryabraham authored Apr 25, 2024
2 parents 6906090 + 821e4e0 commit 6fb4c78
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 262 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -78,6 +79,7 @@ function App({url}: AppProps) {
CustomStatusBarAndBackgroundContextProvider,
ActiveElementRoleProvider,
ActiveWorkspaceContextProvider,
ReportIDsContextProvider,
PlaybackContextProvider,
FullScreenContextProvider,
VolumeContextProvider,
Expand Down
12 changes: 11 additions & 1 deletion src/components/withCurrentReportID.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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],
);
Expand Down
174 changes: 174 additions & 0 deletions src/hooks/useReportIDs.tsx
Original file line number Diff line number Diff line change
@@ -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};
30 changes: 15 additions & 15 deletions src/libs/SidebarUtils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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[] = [],
Expand All @@ -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;
Expand All @@ -103,7 +103,7 @@ function getOrderedReportIDs(
currentReportId: currentReportId ?? '',
isInGSDMode,
betas,
policies,
policies: policies as OnyxCollection<Policy>,
excludeEmptyChats: true,
doesReportHaveViolations,
includeSelfDM: true,
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 6fb4c78

Please sign in to comment.