diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 9b20ee0168aa..cda1f39d3087 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -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'; @@ -332,11 +332,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) - ) { + if (!Object.values(CONST.REPORT.ACTIONS.TYPE).includes(reportAction.actionName) && !Object.values(CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG).includes(reportAction.actionName)) { return false; } @@ -351,7 +347,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); } @@ -383,14 +379,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, + }); + 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; } /** diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 17dd97277150..8e9cea908f74 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -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'; @@ -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}`] || {}; } /** @@ -153,7 +154,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; } /** @@ -163,7 +164,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; } /** @@ -173,7 +174,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; } /** @@ -183,7 +184,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; } /** @@ -237,7 +238,7 @@ function isCompletedTaskReport(report) { * @returns {Boolean} */ function isReportManager(report) { - return lodashGet(report, 'managerID') === currentUserAccountID; + return report && report.managerID === currentUserAccountID; } /** @@ -247,7 +248,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; } /** @@ -271,9 +272,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; } @@ -287,7 +288,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; } @@ -394,8 +398,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); } /** @@ -405,8 +408,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; } /** @@ -576,7 +578,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; } /** @@ -744,7 +746,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); } @@ -2836,7 +2838,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; } @@ -2856,7 +2864,6 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, if (report.hasDraft || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { return true; } - const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); const isEmptyChat = !report.lastMessageText && !report.lastMessageTranslationKey && !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey; const canHideReport = shouldHideReport(report, currentReportId); @@ -2873,7 +2880,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; } diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index a3e57a9dc252..f645697690e6 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -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'; @@ -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 @@ -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. @@ -121,52 +161,45 @@ 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()), + ); + // For archived reports ensure that most recent reports are at the top by reversing the order + 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; } /**