Skip to content

Improvement: Sidebar Links Performance #26447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions src/libs/ReportActionsUtils.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable rulesdir/prefer-underscore-method */
import lodashGet from 'lodash/get';
import _ from 'underscore';
import lodashMerge from 'lodash/merge';
import {max, parseISO, isEqual} from 'date-fns';
import lodashFindLast from 'lodash/findLast';
import Onyx from 'react-native-onyx';
import moment from 'moment';
import * as CollectionUtils from './CollectionUtils';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
Expand Down Expand Up @@ -331,11 +331,7 @@ function shouldReportActionBeVisible(reportAction, key) {
}

// Filter out any unsupported reportAction types
if (
!_.has(CONST.REPORT.ACTIONS.TYPE, reportAction.actionName) &&
!_.contains(_.values(CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG), reportAction.actionName) &&
!_.contains(_.values(CONST.REPORT.ACTIONS.TYPE.TASK), reportAction.actionName)
Copy link
Member

@parasharrajat parasharrajat Sep 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we have accidentally removed this last condition as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "TYPE.TASK" doesn't exist anymore, thats why I removed it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, interesting. @mountiny We should double-check this. It may be possible that we forget to refactor this condition which could lead to further issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, I think it just go split up to TASKEDITED, TASKCANCELLED etc so this should work

) {
if (!Object.values(CONST.REPORT.ACTIONS.TYPE).includes(reportAction.actionName) && !Object.values(CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG).includes(reportAction.actionName)) {
return false;
}

Expand All @@ -350,7 +346,7 @@ function shouldReportActionBeVisible(reportAction, key) {

// All other actions are displayed except thread parents, deleted, or non-pending actions
const isDeleted = isDeletedAction(reportAction);
const isPending = !_.isEmpty(reportAction.pendingAction);
const isPending = !!reportAction.pendingAction;
return !isDeleted || isPending || isDeletedParentAction(reportAction);
}

Expand Down Expand Up @@ -379,14 +375,24 @@ function shouldReportActionBeVisibleAsLastAction(reportAction) {
* @return {Object}
*/
function getLastVisibleAction(reportID, actionsToMerge = {}) {
const actions = _.toArray(lodashMerge({}, allReportActions[reportID], actionsToMerge));
const visibleActions = _.filter(actions, (action) => shouldReportActionBeVisibleAsLastAction(action));
const updatedActionsToMerge = {};
if (actionsToMerge && Object.keys(actionsToMerge).length !== 0) {
Object.keys(actionsToMerge).forEach(
(actionToMergeID) => (updatedActionsToMerge[actionToMergeID] = {...allReportActions[reportID][actionToMergeID], ...actionsToMerge[actionToMergeID]}),
);
}
const actions = Object.values({
...allReportActions[reportID],
...updatedActionsToMerge,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this change introduced this issue: #28941 fixed in this PR: #29939

const visibleActions = actions.filter((action) => shouldReportActionBeVisibleAsLastAction(action));

if (_.isEmpty(visibleActions)) {
if (visibleActions.length === 0) {
return {};
}

return _.max(visibleActions, (action) => moment.utc(action.created).valueOf());
const maxDate = max(visibleActions.map((action) => parseISO(action.created)));
const maxAction = visibleActions.find((action) => isEqual(parseISO(action.created), maxDate));
return maxAction;
}

/**
Expand Down
50 changes: 28 additions & 22 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable rulesdir/prefer-underscore-method */
import _ from 'underscore';
import {format, parseISO} from 'date-fns';
import Str from 'expensify-common/lib/str';
Expand Down Expand Up @@ -86,8 +87,8 @@ function getChatType(report) {
* @returns {Object}
*/
function getPolicy(policyID) {
const policy = lodashGet(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${policyID}`) || {};
return policy;
if (!allPolicies || !policyID) return {};
return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] || {};
}

/**
Expand Down Expand Up @@ -159,7 +160,7 @@ function getReportParticipantsTitle(accountIDs) {
* @returns {Boolean}
*/
function isChatReport(report) {
return lodashGet(report, 'type') === CONST.REPORT.TYPE.CHAT;
return report && report.type === CONST.REPORT.TYPE.CHAT;
}

/**
Expand All @@ -169,7 +170,7 @@ function isChatReport(report) {
* @returns {Boolean}
*/
function isExpenseReport(report) {
return lodashGet(report, 'type') === CONST.REPORT.TYPE.EXPENSE;
return report && report.type === CONST.REPORT.TYPE.EXPENSE;
}

/**
Expand All @@ -179,7 +180,7 @@ function isExpenseReport(report) {
* @returns {Boolean}
*/
function isIOUReport(report) {
return lodashGet(report, 'type') === CONST.REPORT.TYPE.IOU;
return report && report.type === CONST.REPORT.TYPE.IOU;
}

/**
Expand All @@ -189,7 +190,7 @@ function isIOUReport(report) {
* @returns {Boolean}
*/
function isTaskReport(report) {
return lodashGet(report, 'type') === CONST.REPORT.TYPE.TASK;
return report && report.type === CONST.REPORT.TYPE.TASK;
}

/**
Expand Down Expand Up @@ -229,7 +230,7 @@ function isCompletedTaskReport(report) {
* @returns {Boolean}
*/
function isReportManager(report) {
return lodashGet(report, 'managerID') === currentUserAccountID;
return report && report.managerID === currentUserAccountID;
}

/**
Expand All @@ -239,7 +240,7 @@ function isReportManager(report) {
* @returns {Boolean}
*/
function isReportApproved(report) {
return lodashGet(report, 'stateNum') === CONST.REPORT.STATE_NUM.SUBMITTED && lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.APPROVED;
return report && report.statusNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED;
}

/**
Expand All @@ -263,9 +264,9 @@ function sortReportsByLastRead(reports) {
* @returns {Boolean}
*/
function isSettled(reportID) {
const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {});

if (_.isEmpty(report) || report.isWaitingOnBankAccount) {
if (!allReports) return false;
const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {};
if ((typeof report === 'object' && Object.keys(report).length === 0) || report.isWaitingOnBankAccount) {
return false;
}

Expand All @@ -279,7 +280,10 @@ function isSettled(reportID) {
* @returns {Boolean}
*/
function isCurrentUserSubmitter(reportID) {
const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {});
if (!allReports) {
return false;
}
const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {};
return report && report.ownerEmail === currentUserEmail;
}

Expand Down Expand Up @@ -386,8 +390,7 @@ function isChatRoom(report) {
* @returns {Boolean}
*/
function isPublicRoom(report) {
const visibility = lodashGet(report, 'visibility', '');
return visibility === CONST.REPORT.VISIBILITY.PUBLIC || visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE;
return (report && report.visibility === CONST.REPORT.VISIBILITY.PUBLIC) || report.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE;
}

/**
Expand All @@ -397,8 +400,7 @@ function isPublicRoom(report) {
* @returns {Boolean}
*/
function isPublicAnnounceRoom(report) {
const visibility = lodashGet(report, 'visibility', '');
return visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE;
return report && report.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE;
}

/**
Expand Down Expand Up @@ -568,7 +570,7 @@ function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTim
* @returns {Boolean}
*/
function isArchivedRoom(report) {
return lodashGet(report, ['statusNum']) === CONST.REPORT.STATUS.CLOSED && lodashGet(report, ['stateNum']) === CONST.REPORT.STATE_NUM.SUBMITTED;
return report && report.statusNum === CONST.REPORT.STATUS.CLOSED && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED;
}

/**
Expand Down Expand Up @@ -736,7 +738,7 @@ function isMoneyRequest(reportOrID) {
* @returns {Boolean}
*/
function isMoneyRequestReport(reportOrID) {
const report = _.isObject(reportOrID) ? reportOrID : allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`];
const report = typeof reportOrID === 'object' ? reportOrID : allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`];
return isIOUReport(report) || isExpenseReport(report);
}

Expand Down Expand Up @@ -2794,7 +2796,13 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas,
!report ||
!report.reportID ||
report.isHidden ||
(_.isEmpty(report.participantAccountIDs) && !isChatThread(report) && !isPublicRoom(report) && !isArchivedRoom(report) && !isMoneyRequestReport(report) && !isTaskReport(report))
(report.participantAccountIDs &&
report.participantAccountIDs.length === 0 &&
!isChatThread(report) &&
!isPublicRoom(report) &&
!isArchivedRoom(report) &&
!isMoneyRequestReport(report) &&
!isTaskReport(report))
) {
return false;
}
Expand All @@ -2814,10 +2822,8 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas,
if (report.hasDraft || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) {
return true;
}

const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID);
const isEmptyChat = !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey;

// Hide only chat threads that haven't been commented on (other threads are actionable)
if (isChatThread(report) && isEmptyChat) {
return false;
Expand All @@ -2830,7 +2836,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas,

// Include reports that have errors from trying to add a workspace
// If we excluded it, then the red-brock-road pattern wouldn't work for the user to resolve the error
if (report.errorFields && !_.isEmpty(report.errorFields.addWorkspaceRoom)) {
if (report.errorFields && report.errorFields.addWorkspaceRoom) {
return true;
}

Expand Down
108 changes: 70 additions & 38 deletions src/libs/SidebarUtils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable rulesdir/prefer-underscore-method */
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import lodashOrderBy from 'lodash/orderBy';
import Str from 'expensify-common/lib/str';
import ONYXKEYS from '../ONYXKEYS';
import * as ReportUtils from './ReportUtils';
Expand Down Expand Up @@ -75,6 +75,22 @@ function setIsSidebarLoadedReady() {
resolveSidebarIsReadyPromise();
}

// Define a cache object to store the memoized results
const reportIDsCache = new Map();

// Function to set a key-value pair while maintaining the maximum key limit
function setWithLimit(map, key, value) {
if (map.size >= 5) {
// If the map has reached its limit, remove the first (oldest) key-value pair
const firstKey = map.keys().next().value;
map.delete(firstKey);
}
map.set(key, value);
}

// Variable to verify if ONYX actions are loaded
let hasInitialReportActions = false;

/**
* @param {String} currentReportId
* @param {Object} allReportsDict
Expand All @@ -85,22 +101,46 @@ function setIsSidebarLoadedReady() {
* @returns {String[]} An array of reportIDs sorted in the proper order
*/
function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions) {
// Generate a unique cache key based on the function arguments
const cachedReportsKey = JSON.stringify(
// eslint-disable-next-line es/no-optional-chaining
[currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1],
(key, value) => {
/**
* Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data,
* which we don't need to store in a cacheKey
*/
if (key === 'participantAccountIDs' || key === 'participants' || key === 'lastMessageText') {
return undefined;
}
return value;
},
);

// Check if the result is already in the cache
if (reportIDsCache.has(cachedReportsKey) && hasInitialReportActions) {
return reportIDsCache.get(cachedReportsKey);
}

// This is needed to prevent caching when Onyx is empty for a second render
hasInitialReportActions = Object.values(lastReportActions).length > 0;

const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD;
const isInDefaultMode = !isInGSDMode;

const allReportsDictValues = Object.values(allReportsDict);
// Filter out all the reports that shouldn't be displayed
const reportsToDisplay = _.filter(allReportsDict, (report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, true));
const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, true));

if (_.isEmpty(reportsToDisplay)) {
if (reportsToDisplay.length === 0) {
// Display Concierge chat report when there is no report to be displayed
const conciergeChatReport = _.find(allReportsDict, ReportUtils.isConciergeChatReport);
const conciergeChatReport = allReportsDictValues.find(ReportUtils.isConciergeChatReport);
if (conciergeChatReport) {
reportsToDisplay.push(conciergeChatReport);
}
}

// There are a few properties that need to be calculated for the report which are used when sorting reports.
_.each(reportsToDisplay, (report) => {
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.
Expand All @@ -121,52 +161,44 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p
// 5. Archived reports
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
// - Sorted by reportDisplayName in GSD (focus) view mode
let pinnedReports = [];
let outstandingIOUReports = [];
let draftReports = [];
let nonArchivedReports = [];
let archivedReports = [];
_.each(reportsToDisplay, (report) => {
const pinnedReports = [];
const outstandingIOUReports = [];
const draftReports = [];
const nonArchivedReports = [];
const archivedReports = [];
reportsToDisplay.forEach((report) => {
if (report.isPinned) {
pinnedReports.push(report);
return;
}

if (ReportUtils.isWaitingForIOUActionFromCurrentUser(report)) {
} else if (ReportUtils.isWaitingForIOUActionFromCurrentUser(report)) {
outstandingIOUReports.push(report);
return;
}

if (report.hasDraft) {
} else if (report.hasDraft) {
draftReports.push(report);
return;
}

if (ReportUtils.isArchivedRoom(report)) {
} else if (ReportUtils.isArchivedRoom(report)) {
archivedReports.push(report);
return;
} else {
nonArchivedReports.push(report);
}

nonArchivedReports.push(report);
});

// Sort each group of reports accordingly
pinnedReports = _.sortBy(pinnedReports, (report) => report.displayName.toLowerCase());
outstandingIOUReports = lodashOrderBy(outstandingIOUReports, ['iouReportAmount', (report) => report.displayName.toLowerCase()], ['desc', 'asc']);
draftReports = _.sortBy(draftReports, (report) => report.displayName.toLowerCase());
nonArchivedReports = isInDefaultMode
? lodashOrderBy(nonArchivedReports, ['lastVisibleActionCreated', (report) => report.displayName.toLowerCase()], ['desc', 'asc'])
: lodashOrderBy(nonArchivedReports, [(report) => report.displayName.toLowerCase()], ['asc']);
archivedReports = _.sortBy(archivedReports, (report) => (isInDefaultMode ? report.lastVisibleActionCreated : report.displayName.toLowerCase()));

// For archived reports ensure that most recent reports are at the top by reversing the order of the arrays because underscore will only sort them in ascending order
pinnedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
outstandingIOUReports.sort((a, b) => b.iouReportAmount - a.iouReportAmount || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
draftReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
if (isInDefaultMode) {
archivedReports.reverse();
nonArchivedReports.sort(
(a, b) => new Date(b.lastVisibleActionCreated) - new Date(a.lastVisibleActionCreated) || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()),
);
archivedReports.sort((a, b) => new Date(a.lastVisibleActionCreated) - new Date(b.lastVisibleActionCreated));
} else {
nonArchivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
archivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
}

// Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID.
// The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar.
return _.pluck([].concat(pinnedReports).concat(outstandingIOUReports).concat(draftReports).concat(nonArchivedReports).concat(archivedReports), 'reportID');
const LHNReports = [].concat(pinnedReports, outstandingIOUReports, draftReports, nonArchivedReports, archivedReports).map((report) => report.reportID);
setWithLimit(reportIDsCache, cachedReportsKey, LHNReports);
return LHNReports;
}

/**
Expand Down