diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index df55ae49faee..7c60e65eda3c 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -41,8 +41,19 @@ function isDeletedAction(reportAction) { } /** - * Sort an array of reportActions by their created timestamp first, and reportActionID second - * This gives us a stable order even in the case of multiple reportActions created on the same millisecond + * @param {Object} reportAction + * @returns {Boolean} + */ +function isOptimisticAction(reportAction) { + return lodashGet(reportAction, 'pendingAction') === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; +} + +/** + * Sort an array of reportActions by: + * + * - Finalized actions always are "later" than optimistic actions + * - then sort by created timestamp + * - then sort by reportActionID. This gives us a stable order even in the case of multiple reportActions created on the same millisecond * * @param {Array} reportActions * @param {Boolean} shouldSortInDescendingOrder @@ -57,6 +68,11 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal return _.chain(reportActions) .compact() .sort((first, second) => { + // First, make sure that optimistic reportActions appear at the end + if (isOptimisticAction(second) && !isOptimisticAction(first)) { + return -1 * invertedMultiplier; + } + // First sort by timestamp if (first.created !== second.created) { return (first.created < second.created ? -1 : 1) * invertedMultiplier; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 15000bbb87e8..f33dd0e243b4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -21,6 +21,7 @@ import linkingConfig from './Navigation/linkingConfig'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; +import * as CollectionUtils from './CollectionUtils'; let sessionEmail; Onyx.connect({ @@ -71,6 +72,21 @@ Onyx.connect({ callback: val => allReports = val, }); +const lastReportActions = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + callback: (actions, key) => { + if (!key || !actions) { + return; + } + const reportID = CollectionUtils.extractCollectionItemID(key); + lastReportActions[reportID] = _.find( + ReportActionsUtils.getSortedReportActionsForDisplay(_.toArray(actions)), + reportAction => reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); + }, +}); + let doesDomainHaveApprovedAccountant; Onyx.connect({ key: ONYXKEYS.ACCOUNT, @@ -434,6 +450,31 @@ function canShowReportRecipientLocalTime(personalDetails, report) { && isReportParticipantValidated); } +/** + * Gets the last message text from the report. + * Looks at reportActions data as the "best source" for information, because the front-end may have optimistic reportActions that the server is not yet aware of. + * If reportActions are not loaded for the report, then there can't be any optimistic reportActions, and the lastMessageText rNVP will be accurate as a fallback. + * + * @param {Object} report + * @returns {String} + */ +function getLastMessageText(report) { + if (!report) { + return ''; + } + + const lastReportAction = lastReportActions[report.reportID]; + let lastReportActionText = report.lastMessageText; + let lastReportActionHtml = report.lastMessageHtml; + if (lastReportAction && lastReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { + lastReportActionText = lodashGet(lastReportAction, 'message[0].text', report.lastMessageText); + lastReportActionHtml = lodashGet(lastReportAction, 'message[0].html', report.lastMessageHtml); + } + return isReportMessageAttachment({text: lastReportActionText, html: lastReportActionHtml}) + ? `[${Localize.translateLocal('common.attachment')}]` + : Str.htmlDecode(lastReportActionText); +} + /** * Trim the last message text to a fixed limit. * @param {String} lastMessageText @@ -1669,6 +1710,7 @@ export { isIOUOwnedByCurrentUser, getIOUTotal, canShowReportRecipientLocalTime, + getLastMessageText, formatReportLastMessageText, chatIncludesConcierge, isPolicyExpenseChat, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 3e6fd500c98e..eaa6d3929c5d 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -5,6 +5,7 @@ import lodashOrderBy from 'lodash/orderBy'; import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../ONYXKEYS'; import * as ReportUtils from './ReportUtils'; +import * as ReportActionsUtils from './ReportActionsUtils'; import * as Localize from './Localize'; import CONST from '../CONST'; import * as OptionsListUtils from './OptionsListUtils'; @@ -61,7 +62,7 @@ Onyx.connect({ return; } const reportID = CollectionUtils.extractCollectionItemID(key); - lastReportActions[reportID] = _.last(_.toArray(actions)); + lastReportActions[reportID] = _.first(ReportActionsUtils.getSortedReportActionsForDisplay(_.toArray(actions))); reportActions[key] = actions; }, }); @@ -243,12 +244,7 @@ function getOptionData(reportID) { // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); - let lastMessageTextFromReport = ''; - if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml})) { - lastMessageTextFromReport = `[${Localize.translateLocal('common.attachment')}]`; - } else { - lastMessageTextFromReport = Str.htmlDecode(report ? report.lastMessageText : ''); - } + const lastMessageTextFromReport = ReportUtils.getLastMessageText(report); // If the last actor's details are not currently saved in Onyx Collection, // then try to get that from the last report action. @@ -263,7 +259,7 @@ function getOptionData(reportID) { let lastMessageText = hasMultipleParticipants && lastActorDetails && (lastActorDetails.login !== currentUserLogin.email) ? `${lastActorDetails.displayName}: ` : ''; - lastMessageText += report ? lastMessageTextFromReport : ''; + lastMessageText += lastMessageTextFromReport; if (result.isPolicyExpenseChat && result.isArchivedRoom) { const archiveReason = (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index 20bf389969b7..ad473647dc60 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -6,7 +6,15 @@ describe('ReportActionsUtils', () => { const cases = [ [ [ - // This is the highest created timestamp, so should appear last + // This is the lowest created timestamp, but because it's an optimistic action it should appear last + { + created: '2022-11-09 20:00:00.000', + reportActionID: '395268342', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + + // This is the highest created timestamp, so should appear 2nd-to-last { created: '2022-11-09 22:27:01.825', reportActionID: '8401445780099176', @@ -61,6 +69,12 @@ describe('ReportActionsUtils', () => { reportActionID: '8401445780099176', actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, }, + { + created: '2022-11-09 20:00:00.000', + reportActionID: '395268342', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, ], ], [