diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index ae225b3db9e9..923337ba9ada 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -52,8 +52,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti return null; } - const isHidden = optionItem?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - if (isHidden && !isFocused && !optionItem?.isPinned) { + const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction); + + const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + + const shouldOverrideHidden = hasBrickError || isFocused || optionItem.isPinned; + if (isHidden && !shouldOverrideHidden) { return null; } @@ -74,8 +79,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const hoveredBackgroundColor = !!styles.sidebarLinkHover && 'backgroundColor' in styles.sidebarLinkHover ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; - const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; - const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction); /** * Show the ReportActionContextMenu modal popover. * diff --git a/src/libs/API/parameters/CreateDistanceRequestParams.ts b/src/libs/API/parameters/CreateDistanceRequestParams.ts index c1eb1003a698..62f90a64cf05 100644 --- a/src/libs/API/parameters/CreateDistanceRequestParams.ts +++ b/src/libs/API/parameters/CreateDistanceRequestParams.ts @@ -12,6 +12,8 @@ type CreateDistanceRequestParams = { category?: string; tag?: string; billable?: boolean; + transactionThreadReportID: string; + createdReportActionIDForThread: string; }; export default CreateDistanceRequestParams; diff --git a/src/libs/API/parameters/RequestMoneyParams.ts b/src/libs/API/parameters/RequestMoneyParams.ts index 983394008ba7..b55f9fd7a2a9 100644 --- a/src/libs/API/parameters/RequestMoneyParams.ts +++ b/src/libs/API/parameters/RequestMoneyParams.ts @@ -25,6 +25,8 @@ type RequestMoneyParams = { taxAmount: number; billable?: boolean; gpsPoints?: string; + transactionThreadReportID: string; + createdReportActionIDForThread: string; }; export default RequestMoneyParams; diff --git a/src/libs/API/parameters/SendMoneyParams.ts b/src/libs/API/parameters/SendMoneyParams.ts index b737ba2ea48b..ac6f42de5aa5 100644 --- a/src/libs/API/parameters/SendMoneyParams.ts +++ b/src/libs/API/parameters/SendMoneyParams.ts @@ -9,6 +9,8 @@ type SendMoneyParams = { newIOUReportDetails: string; createdReportActionID: string; reportPreviewReportActionID: string; + transactionThreadReportID: string; + createdReportActionIDForThread: string; }; export default SendMoneyParams; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index eeebbdf7dbdb..cc7560bf98e7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -6,7 +6,7 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ActionName, ChangeLog, OriginalMessageActionableMentionWhisper, OriginalMessageIOU, OriginalMessageReimbursementDequeued} from '@src/types/onyx/OriginalMessage'; +import type {ActionName, ChangeLog, IOUMessage, OriginalMessageActionableMentionWhisper, OriginalMessageIOU, OriginalMessageReimbursementDequeued} from '@src/types/onyx/OriginalMessage'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -19,6 +19,7 @@ import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import type {OptimisticIOUReportAction} from './ReportUtils'; type LastVisibleMessage = { lastMessageTranslationKey?: string; @@ -93,7 +94,7 @@ function isCreatedAction(reportAction: OnyxEntry): boolean { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; } -function isDeletedAction(reportAction: OnyxEntry): boolean { +function isDeletedAction(reportAction: OnyxEntry): boolean { // A deleted comment has either an empty array or an object with html field with empty string as value const message = reportAction?.message ?? []; return message.length === 0 || message[0]?.html === ''; @@ -103,8 +104,8 @@ function isDeletedParentAction(reportAction: OnyxEntry): boolean { return (reportAction?.message?.[0]?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isReversedTransaction(reportAction: OnyxEntry) { - return (reportAction?.message?.[0]?.isReversedTransaction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; +function isReversedTransaction(reportAction: OnyxEntry) { + return (reportAction?.message?.[0]?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0; } function isPendingRemove(reportAction: OnyxEntry | EmptyObject): boolean { @@ -184,9 +185,11 @@ function getParentReportAction(report: OnyxEntry | EmptyObject): ReportA /** * Determines if the given report action is sent money report action by checking for 'pay' type and presence of IOUDetails object. */ -function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { +function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { return ( - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!reportAction?.originalMessage?.IOUDetails + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + (reportAction?.originalMessage as IOUMessage)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && + !!(reportAction?.originalMessage as IOUMessage)?.IOUDetails ); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 453de7219315..2f78b07022a4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2248,7 +2248,7 @@ function hasMissingSmartscanFields(iouReportID: string): boolean { /** * Given a parent IOU report action get report name for the LHN. */ -function getTransactionReportName(reportAction: OnyxEntry): string { +function getTransactionReportName(reportAction: OnyxEntry): string { if (ReportActionsUtils.isReversedTransaction(reportAction)) { return Localize.translateLocal('parentReportAction.reversedTransaction'); } @@ -3750,7 +3750,7 @@ function buildOptimisticTaskReport( * * @param moneyRequestReportID - the reportID which the report action belong to */ -function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string): OptimisticChatReport { +function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string): OptimisticChatReport { const participantAccountIDs = [...new Set([currentUserAccountID, Number(reportAction?.actorAccountID)])].filter(Boolean) as number[]; return buildOptimisticChatReport( participantAccountIDs, @@ -3954,6 +3954,13 @@ function shouldReportBeInOptionList({ return true; } + const reportIsSettled = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + + // Always show IOU reports with violations unless they are reimbursed + if (isExpenseRequest(report) && doesReportHaveViolations && !reportIsSettled) { + return true; + } + // Hide only chat threads that haven't been commented on (other threads are actionable) if (isChatThread(report) && canHideReport && isEmptyChat) { return false; @@ -3965,11 +3972,6 @@ function shouldReportBeInOptionList({ return true; } - // Always show IOU reports with violations - if (isExpenseRequest(report) && doesReportHaveViolations) { - return true; - } - // All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones if (isInGSDMode) { return isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d3eafc6554db..8f337061b59c 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -5,12 +5,14 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {IOUMessage} from '@src/types/onyx/OriginalMessage'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type {Comment, Receipt, TransactionChanges, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; +import type {OptimisticIOUReportAction} from './ReportUtils'; let allTransactions: OnyxCollection = {}; @@ -460,11 +462,11 @@ function hasRoute(transaction: Transaction): boolean { * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject { +function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject { let transactionID = ''; if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - transactionID = reportAction.originalMessage?.IOUTransactionID ?? ''; + transactionID = (reportAction?.originalMessage as IOUMessage)?.IOUTransactionID ?? ''; } return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 3482dd0e30ad..cd9ff31201a2 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -76,6 +76,8 @@ type MoneyRequestInformation = { createdChatReportActionID: string; createdIOUReportActionID: string; reportPreviewAction: OnyxTypes.ReportAction; + transactionThreadReportID: string; + createdReportActionIDForThread: string; onyxData: OnyxData; }; @@ -391,6 +393,8 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedCategories: string[], optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, isNewChatReport: boolean, + transactionThreadReport: OptimisticChatReport, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction, shouldCreateNewMoneyRequestReport: boolean, policy?: OnyxEntry, policyTagList?: OnyxEntry, @@ -469,6 +473,19 @@ function buildOnyxDataForMoneyRequest( }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: transactionThreadReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, + }, + }, + // Remove the temporary transaction used during the creation flow { onyxMethod: Onyx.METHOD.SET, @@ -531,6 +548,14 @@ function buildOnyxDataForMoneyRequest( errorFields: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + pendingFields: null, + errorFields: null, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, @@ -575,6 +600,16 @@ function buildOnyxDataForMoneyRequest( }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, ); const failureData: OnyxUpdate[] = [ @@ -605,6 +640,15 @@ function buildOnyxDataForMoneyRequest( }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, @@ -671,6 +715,15 @@ function buildOnyxDataForMoneyRequest( }), }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + }, + }, + }, ]; // We don't need to compute violations unless we're on a paid policy @@ -823,7 +876,8 @@ function getMoneyRequestInformation( // 1. CREATED action for the chatReport // 2. CREATED action for the iouReport // 3. IOU action for the iouReport - // 4. REPORTPREVIEW action for the chatReport + // 4. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread + // 5. REPORTPREVIEW action for the chatReport // Note: The CREATED action for the IOU report must be optimistically generated before the IOU action so there's no chance that it appears after the IOU action in the chat const currentTime = DateUtils.getDBTime(); const optimisticCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); @@ -843,6 +897,8 @@ function getMoneyRequestInformation( false, currentTime, ); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, iouReport.reportID); + const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); let reportPreviewAction = shouldCreateNewMoneyRequestReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID); if (reportPreviewAction) { @@ -886,6 +942,8 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedCategories, optimisticPolicyRecentlyUsedTags, isNewChatReport, + optimisticTransactionThread, + optimisticCreatedActionForTransactionThread, shouldCreateNewMoneyRequestReport, policy, policyTagList, @@ -904,6 +962,8 @@ function getMoneyRequestInformation( createdChatReportActionID: isNewChatReport ? optimisticCreatedActionForChat.reportActionID : '0', createdIOUReportActionID: shouldCreateNewMoneyRequestReport ? optimisticCreatedActionForIOU.reportActionID : '0', reportPreviewAction, + transactionThreadReportID: optimisticTransactionThread.reportID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, onyxData: { optimisticData, successData, @@ -939,7 +999,18 @@ function createDistanceRequest( source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN, }; - const {iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation( + const { + iouReport, + chatReport, + transaction, + iouAction, + createdChatReportActionID, + createdIOUReportActionID, + reportPreviewAction, + transactionThreadReportID, + createdReportActionIDForThread, + onyxData, + } = getMoneyRequestInformation( currentChatReport, participant, comment, @@ -974,6 +1045,8 @@ function createDistanceRequest( category, tag, billable, + transactionThreadReportID, + createdReportActionIDForThread, }; API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); @@ -1438,27 +1511,39 @@ function requestMoney( const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; const moneyRequestReportID = isMoneyRequestReport ? report.reportID : ''; const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); - const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = - getMoneyRequestInformation( - currentChatReport, - participant, - comment, - amount, - currency, - currentCreated, - merchant, - receipt, - undefined, - category, - tag, - billable, - policy, - policyTagList, - policyCategories, - payeeAccountID, - payeeEmail, - moneyRequestReportID, - ); + const { + payerAccountID, + payerEmail, + iouReport, + chatReport, + transaction, + iouAction, + createdChatReportActionID, + createdIOUReportActionID, + reportPreviewAction, + transactionThreadReportID, + createdReportActionIDForThread, + onyxData, + } = getMoneyRequestInformation( + currentChatReport, + participant, + comment, + amount, + currency, + currentCreated, + merchant, + receipt, + undefined, + category, + tag, + billable, + policy, + policyTagList, + policyCategories, + payeeAccountID, + payeeEmail, + moneyRequestReportID, + ); const activeReportID = isMoneyRequestReport ? report.reportID : chatReport.reportID; const parameters: RequestMoneyParams = { @@ -1483,9 +1568,10 @@ function requestMoney( taxCode, taxAmount, billable, - // This needs to be a string of JSON because of limitations with the fetch() API and nested objects gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, + transactionThreadReportID, + createdReportActionIDForThread, }; API.write(WRITE_COMMANDS.REQUEST_MONEY, parameters, onyxData); @@ -1813,6 +1899,10 @@ function createSplitsAndOnyxData( // Add tag to optimistic policy recently used tags when a participant is a workspace const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag) : {}; + // Create optimistic transactionThread + const optimisticTransactionThread = ReportUtils.buildTransactionThread(oneOnOneIOUAction, oneOnOneIOUReport.reportID); + const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); + // STEP 5: Build Onyx Data const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest( oneOnOneChatReport, @@ -1826,6 +1916,8 @@ function createSplitsAndOnyxData( optimisticPolicyRecentlyUsedCategories, optimisticPolicyRecentlyUsedTags, isNewOneOnOneChatReport, + optimisticTransactionThread, + optimisticCreatedActionForTransactionThread, shouldCreateNewOneOnOneIOUReport, ); @@ -1840,6 +1932,8 @@ function createSplitsAndOnyxData( createdChatReportActionID: oneOnOneCreatedActionForChat.reportActionID, createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID, reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID, + transactionThreadReportID: optimisticTransactionThread.reportID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, }; splits.push(individualSplit); @@ -2406,6 +2500,9 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA oneOnOneReportPreviewAction = ReportUtils.buildOptimisticReportPreview(oneOnOneChatReport, oneOnOneIOUReport, '', oneOnOneTransaction); } + const optimisticTransactionThread = ReportUtils.buildTransactionThread(oneOnOneIOUAction, oneOnOneIOUReport.reportID); + const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); + const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest( oneOnOneChatReport, oneOnOneIOUReport, @@ -2418,6 +2515,8 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA [], {}, isNewOneOnOneChatReport, + optimisticTransactionThread, + optimisticCreatedActionForTransactionThread, shouldCreateNewOneOnOneIOUReport, ); @@ -2432,6 +2531,8 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA createdChatReportActionID: oneOnOneCreatedActionForChat.reportActionID, createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID, reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID, + transactionThreadReportID: optimisticTransactionThread.reportID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, }); optimisticData.push(...oneOnOneOptimisticData); @@ -3118,6 +3219,9 @@ function getSendMoneyParams( const reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, optimisticIOUReport); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(optimisticIOUReportAction, optimisticIOUReport.reportID); + const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(recipientEmail); + // Change the method to set for new reports because it doesn't exist yet, is faster, // and we need the data to be available when we navigate to the chat page const optimisticChatReportData: OnyxUpdate = isNewChat @@ -3150,6 +3254,11 @@ function getSendMoneyParams( lastMessageHtml: optimisticIOUReportAction.message?.[0].html, }, }; + const optimisticTransactionThreadData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: optimisticTransactionThread, + }; const optimisticIOUReportActionsData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`, @@ -3167,6 +3276,13 @@ function getSendMoneyParams( [reportPreviewAction.reportActionID]: reportPreviewAction, }, }; + const optimisticTransactionThreadReportActionsData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: { + [optimisticCreatedActionForTransactionThread.reportActionID]: optimisticCreatedActionForTransactionThread, + }, + }; const successData: OnyxUpdate[] = [ { @@ -3192,6 +3308,15 @@ function getSendMoneyParams( }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: { + [optimisticCreatedActionForTransactionThread.reportActionID]: { + pendingAction: null, + }, + }, + }, ]; const failureData: OnyxUpdate[] = [ @@ -3202,6 +3327,24 @@ function getSendMoneyParams( errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: { + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: { + [optimisticCreatedActionForTransactionThread.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + }, + }, + }, ]; let optimisticPersonalDetailListData: OnyxUpdate | EmptyObject = {}; @@ -3266,7 +3409,16 @@ function getSendMoneyParams( }); } - const optimisticData: OnyxUpdate[] = [optimisticChatReportData, optimisticIOUReportData, optimisticChatReportActionsData, optimisticIOUReportActionsData, optimisticTransactionData]; + const optimisticData: OnyxUpdate[] = [ + optimisticChatReportData, + optimisticIOUReportData, + optimisticChatReportActionsData, + optimisticIOUReportActionsData, + optimisticTransactionData, + optimisticTransactionThreadData, + optimisticTransactionThreadReportActionsData, + ]; + if (!isEmptyObject(optimisticPersonalDetailListData)) { optimisticData.push(optimisticPersonalDetailListData); } @@ -3281,6 +3433,8 @@ function getSendMoneyParams( newIOUReportDetails, createdReportActionID: isNewChat ? optimisticCreatedAction.reportActionID : '0', reportPreviewReportActionID: reportPreviewAction.reportActionID, + transactionThreadReportID: optimisticTransactionThread.reportID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, }, optimisticData, successData, diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 3bd538e8beab..9c73fc0897d7 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -271,7 +271,7 @@ const chatReportSelector = (report) => const reportActionsSelector = (reportActions) => reportActions && lodashMap(reportActions, (reportAction) => { - const {reportActionID, parentReportActionID, actionName, errors = []} = reportAction; + const {reportActionID, parentReportActionID, actionName, errors = [], originalMessage} = reportAction; const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); return { @@ -284,6 +284,7 @@ const reportActionsSelector = (reportActions) => moderationDecision: {decision}, }, ], + originalMessage, }; }); diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index b04011978d73..6bbcb174a617 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -35,6 +35,8 @@ type Split = { createdChatReportActionID?: string; createdIOUReportActionID?: string; reportPreviewReportActionID?: string; + transactionThreadReportID?: string; + createdReportActionIDForThread?: string; }; type IOU = { diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index cb31afbf8f8f..0d20adbbc1ca 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -58,6 +58,8 @@ describe('actions/IOU', () => { let createdAction; let iouAction; let transactionID; + let transactionThread; + let transactionThreadCreatedAction; fetch.pause(); IOU.requestMoney({}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment); return waitForBatchedUpdates() @@ -70,14 +72,16 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // A chat report and an iou report should be created + // A chat report, a transaction thread, and an iou report should be created const chatReports = _.filter(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT); const iouReports = _.filter(allReports, (report) => report.type === CONST.REPORT.TYPE.IOU); - expect(_.size(chatReports)).toBe(1); + expect(_.size(chatReports)).toBe(2); expect(_.size(iouReports)).toBe(1); const chatReport = chatReports[0]; + const transactionThreadReport = chatReports[1]; const iouReport = iouReports[0]; iouReportID = iouReport.reportID; + transactionThread = transactionThreadReport; expect(iouReport.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); @@ -132,6 +136,27 @@ describe('actions/IOU', () => { }); }), ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`, + waitForCollectionCallback: true, + callback: (reportActionsForTransactionThread) => { + Onyx.disconnect(connectionID); + + // The transaction thread should have a CREATED action + expect(_.size(reportActionsForTransactionThread)).toBe(1); + const createdActions = _.filter(reportActionsForTransactionThread, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); + expect(_.size(createdActions)).toBe(1); + transactionThreadCreatedAction = createdActions[0]; + + expect(transactionThreadCreatedAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) .then( () => new Promise((resolve) => { @@ -237,8 +262,8 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // The same chat report should be reused, and an IOU report should be created - expect(_.size(allReports)).toBe(2); + // The same chat report should be reused, a transaction thread and an IOU report should be created + expect(_.size(allReports)).toBe(3); expect(_.find(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT).reportID).toBe(chatReport.reportID); chatReport = _.find(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT); const iouReport = _.find(allReports, (report) => report.type === CONST.REPORT.TYPE.IOU); @@ -431,7 +456,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); // No new reports should be created - expect(_.size(allReports)).toBe(2); + expect(_.size(allReports)).toBe(3); expect(_.find(allReports, (report) => report.reportID === chatReportID)).toBeTruthy(); expect(_.find(allReports, (report) => report.reportID === iouReportID)).toBeTruthy(); @@ -550,6 +575,8 @@ describe('actions/IOU', () => { let createdAction; let iouAction; let transactionID; + let transactionThreadReport; + let transactionThreadAction; fetch.pause(); IOU.requestMoney({}, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment); return ( @@ -563,13 +590,15 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // A chat report and an iou report should be created + // A chat report, transaction thread and an iou report should be created const chatReports = _.filter(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT); const iouReports = _.filter(allReports, (report) => report.type === CONST.REPORT.TYPE.IOU); - expect(_.size(chatReports)).toBe(1); + expect(_.size(chatReports)).toBe(2); expect(_.size(iouReports)).toBe(1); const chatReport = chatReports[0]; chatReportID = chatReport.reportID; + transactionThreadReport = chatReports[1]; + const iouReport = iouReports[0]; iouReportID = iouReport.reportID; @@ -674,6 +703,25 @@ describe('actions/IOU', () => { }); }), ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, + waitForCollectionCallback: true, + callback: (reportActionsForTransactionThread) => { + Onyx.disconnect(connectionID); + expect(_.size(reportActionsForTransactionThread)).toBe(3); + transactionThreadAction = _.find( + reportActionsForTransactionThread[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`], + (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + expect(transactionThreadAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) .then( () => new Promise((resolve) => { @@ -696,6 +744,7 @@ describe('actions/IOU', () => { () => new Promise((resolve) => { ReportActions.clearReportActionErrors(iouReportID, iouAction); + ReportActions.clearReportActionErrors(transactionThreadReport.reportID, transactionThreadAction); resolve(); }), ) @@ -738,6 +787,7 @@ describe('actions/IOU', () => { () => new Promise((resolve) => { Report.deleteReport(chatReportID); + Report.deleteReport(transactionThreadReport.reportID); resolve(); }), ) @@ -938,8 +988,8 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // There should now be 7 reports - expect(_.size(allReports)).toBe(7); + // There should now be 10 reports + expect(_.size(allReports)).toBe(10); // 1. The chat report with Rory + Carlos carlosChatReport = _.find(allReports, (report) => report.reportID === carlosChatReport.reportID); @@ -1006,8 +1056,8 @@ describe('actions/IOU', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); - // There should be reportActions on all 4 chat reports + 3 IOU reports in each 1:1 chat - expect(_.size(allReportActions)).toBe(7); + // There should be reportActions on all 7 chat reports + 3 IOU reports in each 1:1 chat + expect(_.size(allReportActions)).toBe(10); const carlosReportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${carlosChatReport.iouReportID}`]; const julesReportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${julesChatReport.iouReportID}`]; @@ -1220,9 +1270,10 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - expect(_.size(allReports)).toBe(2); + expect(_.size(allReports)).toBe(3); - chatReport = _.find(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT); + const chatReports = _.filter(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT); + chatReport = chatReports[0]; expect(chatReport).toBeTruthy(); expect(chatReport).toHaveProperty('reportID'); expect(chatReport).toHaveProperty('iouReportID'); @@ -1299,7 +1350,7 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - expect(_.size(allReports)).toBe(2); + expect(_.size(allReports)).toBe(3); chatReport = _.find(allReports, (r) => r.type === CONST.REPORT.TYPE.CHAT); iouReport = _.find(allReports, (r) => r.type === CONST.REPORT.TYPE.IOU); @@ -1348,7 +1399,7 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - expect(_.size(allReports)).toBe(2); + expect(_.size(allReports)).toBe(3); chatReport = _.find(allReports, (r) => r.type === CONST.REPORT.TYPE.CHAT); iouReport = _.find(allReports, (r) => r.type === CONST.REPORT.TYPE.IOU); @@ -1903,8 +1954,8 @@ describe('actions/IOU', () => { }); }); - // Then we should have exactly 2 reports - expect(_.size(allReports)).toBe(2); + // Then we should have exactly 3 reports + expect(_.size(allReports)).toBe(3); // Then one of them should be a chat report with relevant properties chatReport = _.find(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT);