diff --git a/src/hooks/useOriginalReportID.ts b/src/hooks/useOriginalReportID.ts new file mode 100644 index 000000000000..798cb03a8063 --- /dev/null +++ b/src/hooks/useOriginalReportID.ts @@ -0,0 +1,41 @@ +import {useMemo} from 'react'; +import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; +import {getOneTransactionThreadReportID} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx'; +import useNetwork from './useNetwork'; +import useOnyx from './useOnyx'; +import useTransactionsAndViolationsForReport from './useTransactionsAndViolationsForReport'; + +function useOriginalReportID(reportID: string | undefined, reportAction: OnyxInputOrEntry): string | undefined { + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canBeMissing: true}); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true}); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`, {canBeMissing: true}); + const {isOffline} = useNetwork(); + const {transactions: allReportTransactions} = useTransactionsAndViolationsForReport(reportID); + + const visibleTransactionsIDs = useMemo( + () => + getAllNonDeletedTransactions(allReportTransactions, Object.values(reportActions ?? {})) + .filter((transaction) => isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .map((transaction) => transaction.transactionID), + [allReportTransactions, reportActions, isOffline], + ); + if (!reportID) { + return undefined; + } + const currentReportAction = reportAction?.reportActionID ? reportActions?.[reportAction.reportActionID] : undefined; + + if (Object.keys(currentReportAction ?? {}).length === 0) { + const isThreadReportParentAction = reportAction?.childReportID?.toString() === reportID; + if (isThreadReportParentAction) { + return report?.parentReportID; + } + const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? ([] as ReportAction[]), isOffline, visibleTransactionsIDs); + return transactionThreadReportID ?? reportID; + } + return reportID; +} + +export default useOriginalReportID; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 5e47526932c5..0919d9c96edd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -663,8 +663,11 @@ function notifyNewAction(reportID: string | undefined, accountID: number | undef * - Adding one comment * - Adding one attachment * - Add both a comment and attachment simultaneously + * + * @param reportID - The report ID where the comment should be added + * @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report. */ -function addActions(reportID: string, text = '', file?: FileObject) { +function addActions(reportID: string, notifyReportID: string, text = '', file?: FileObject) { let reportCommentText = ''; let reportCommentAction: OptimisticAddCommentReportAction | undefined; let attachmentAction: OptimisticAddCommentReportAction | undefined; @@ -830,23 +833,23 @@ function addActions(reportID: string, text = '', file?: FileObject) { successData, failureData, }); - notifyNewAction(reportID, lastAction?.actorAccountID, lastAction); + notifyNewAction(notifyReportID, lastAction?.actorAccountID, lastAction); } /** Add an attachment and optional comment. */ -function addAttachment(reportID: string, file: FileObject, text = '', shouldPlaySound?: boolean) { +function addAttachment(reportID: string, notifyReportID: string, file: FileObject, text = '', shouldPlaySound?: boolean) { if (shouldPlaySound) { playSound(SOUNDS.DONE); } - addActions(reportID, text, file); + addActions(reportID, notifyReportID, text, file); } /** Add a single comment to a report */ -function addComment(reportID: string, text: string, shouldPlaySound?: boolean) { +function addComment(reportID: string, notifyReportID: string, text: string, shouldPlaySound?: boolean) { if (shouldPlaySound) { playSound(SOUNDS.DONE); } - addActions(reportID, text); + addActions(reportID, notifyReportID, text); } function reportActionsExist(reportID: string): boolean { @@ -2058,11 +2061,10 @@ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMar /** Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. */ function editReportComment(reportID: string | undefined, originalReportAction: OnyxEntry, textForNewComment: string, videoAttributeCache?: Record) { - const originalReportID = getOriginalReportID(reportID, originalReportAction); - if (!originalReportID || !originalReportAction) { + if (!reportID || !originalReportAction) { return; } - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report); // Do not autolink if someone explicitly tries to remove a link from message. @@ -2089,7 +2091,7 @@ function editReportComment(reportID: string | undefined, originalReportAction: O // Delete the comment if it's empty if (!htmlForNewComment) { - deleteReportComment(originalReportID, originalReportAction); + deleteReportComment(reportID, originalReportAction); return; } @@ -2120,12 +2122,12 @@ function editReportComment(reportID: string | undefined, originalReportAction: O const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: optimisticReportActions, }, ]; - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction, optimisticReportActions as ReportActions); if (reportActionID === lastVisibleAction?.reportActionID) { const lastMessageText = formatReportLastMessageText(reportComment); const optimisticReport = { @@ -2133,7 +2135,7 @@ function editReportComment(reportID: string | undefined, originalReportAction: O }; optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: optimisticReport, }); } @@ -2141,7 +2143,7 @@ function editReportComment(reportID: string | undefined, originalReportAction: O const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { [reportActionID]: { ...originalReportAction, @@ -2154,7 +2156,7 @@ function editReportComment(reportID: string | undefined, originalReportAction: O const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { [reportActionID]: { pendingAction: null, @@ -2164,7 +2166,7 @@ function editReportComment(reportID: string | undefined, originalReportAction: O ]; const parameters: UpdateCommentParams = { - reportID: originalReportID, + reportID, reportComment: htmlForNewComment, reportActionID, }; @@ -5931,19 +5933,19 @@ function changeReportPolicyAndInviteSubmitter(report: Report, policy: Policy, em /** * Resolves Concierge category options by adding a comment and updating the report action - * @param reportID - The report ID where the comment should be added - * @param actionReportID - The report ID where the report action should be updated (may be different for threads) + * @param reportID - The report ID where the comment should be added and the report action should be updated + * @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report. * @param reportActionID - The specific report action ID to update * @param selectedCategory - The category selected by the user */ -function resolveConciergeCategoryOptions(reportID: string | undefined, actionReportID: string | undefined, reportActionID: string | undefined, selectedCategory: string) { - if (!reportID || !actionReportID || !reportActionID) { +function resolveConciergeCategoryOptions(reportID: string | undefined, notifyReportID: string | undefined, reportActionID: string | undefined, selectedCategory: string) { + if (!reportID || !reportActionID) { return; } - addComment(reportID, selectedCategory); + addComment(reportID, notifyReportID ?? reportID, selectedCategory); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${actionReportID}`, { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [reportActionID]: { originalMessage: { selectedCategory, diff --git a/src/pages/Share/ShareDetailsPage.tsx b/src/pages/Share/ShareDetailsPage.tsx index 9781b4ea3ac7..82eb281edab1 100644 --- a/src/pages/Share/ShareDetailsPage.tsx +++ b/src/pages/Share/ShareDetailsPage.tsx @@ -97,7 +97,7 @@ function ShareDetailsPage({ } if (isTextShared) { - addComment(report.reportID, message); + addComment(report.reportID, report.reportID, message); const routeToNavigate = ROUTES.REPORT_WITH_ID.getRoute(reportOrAccountID); Navigation.navigate(routeToNavigate, {forceReplace: true}); return; @@ -119,7 +119,7 @@ function ShareDetailsPage({ ); } if (report.reportID) { - addAttachment(report.reportID, file, message); + addAttachment(report.reportID, report.reportID, file, message); } const routeToNavigate = ROUTES.REPORT_WITH_ID.getRoute(reportOrAccountID); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b566b1538b6d..53a5f3782376 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -48,6 +48,7 @@ import { isCreatedAction, isDeletedParentAction, isMoneyRequestAction, + isSentMoneyReportAction, isWhisperAction, shouldReportActionBeVisible, } from '@libs/ReportActionsUtils'; @@ -316,6 +317,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { canBeMissing: true, }); const combinedReportActions = getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions)); + const isSentMoneyReport = useMemo(() => reportActions.some((action) => isSentMoneyReportAction(action)), [reportActions]); const lastReportAction = [...combinedReportActions, parentReportAction].find((action) => canEditReportAction(action) && !isMoneyRequestAction(action)); // wrapping in useMemo to stabilize children re-rendering const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -888,6 +890,8 @@ function ReportScreen({route, navigation}: ReportScreenProps) { isComposerFullSize={!!isComposerFullSize} lastReportAction={lastReportAction} reportTransactions={reportTransactions} + // If the report is from the 'Send Money' flow, we add the comment to the `iou` report because for these we don't combine reportActions even if there is a single transaction (they always have a single transaction) + transactionThreadReportID={isSentMoneyReport ? undefined : transactionThreadReportID} /> ) : null} diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 3d7c450064f2..ef7ad2951009 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -714,6 +714,7 @@ function PureReportActionItem({ ]; } + const reportActionReportID = originalReportID ?? reportID; if (isConciergeCategoryOptions(action)) { const options = getOriginalMessage(action)?.options; if (!options) { @@ -724,7 +725,7 @@ function PureReportActionItem({ return []; } - if (!reportID) { + if (!reportActionReportID) { return []; } @@ -732,7 +733,7 @@ function PureReportActionItem({ text: `${i + 1} - ${option}`, key: `${action.reportActionID}-conciergeCategoryOptions-${option}`, onPress: () => { - resolveConciergeCategoryOptions(reportID, originalReportID, action.reportActionID, option); + resolveConciergeCategoryOptions(reportActionReportID, reportID, action.reportActionID, option); }, })); } @@ -748,7 +749,7 @@ function PureReportActionItem({ text: 'actionableMentionTrackExpense.submit', key: `${action.reportActionID}-actionableMentionTrackExpense-submit`, onPress: () => { - createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportID, CONST.IOU.ACTION.SUBMIT, action.reportActionID); + createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportActionReportID, CONST.IOU.ACTION.SUBMIT, action.reportActionID); }, }, ]; @@ -759,14 +760,14 @@ function PureReportActionItem({ text: 'actionableMentionTrackExpense.categorize', key: `${action.reportActionID}-actionableMentionTrackExpense-categorize`, onPress: () => { - createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportID, CONST.IOU.ACTION.CATEGORIZE, action.reportActionID); + createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportActionReportID, CONST.IOU.ACTION.CATEGORIZE, action.reportActionID); }, }, { text: 'actionableMentionTrackExpense.share', key: `${action.reportActionID}-actionableMentionTrackExpense-share`, onPress: () => { - createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportID, CONST.IOU.ACTION.SHARE, action.reportActionID); + createDraftTransactionAndNavigateToParticipantSelector(transactionID, reportActionReportID, CONST.IOU.ACTION.SHARE, action.reportActionID); }, }, ); @@ -775,7 +776,7 @@ function PureReportActionItem({ text: 'actionableMentionTrackExpense.nothing', key: `${action.reportActionID}-actionableMentionTrackExpense-nothing`, onPress: () => { - dismissTrackExpenseActionableWhisper(reportID, action); + dismissTrackExpenseActionableWhisper(reportActionReportID, action); }, }); return options; @@ -786,13 +787,13 @@ function PureReportActionItem({ { text: 'actionableMentionJoinWorkspaceOptions.accept', key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT}`, - onPress: () => acceptJoinRequest(reportID, action), + onPress: () => acceptJoinRequest(reportActionReportID, action), isPrimary: true, }, { text: 'actionableMentionJoinWorkspaceOptions.decline', key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE}`, - onPress: () => declineJoinRequest(reportID, action), + onPress: () => declineJoinRequest(reportActionReportID, action), }, ]; } @@ -802,13 +803,13 @@ function PureReportActionItem({ { text: 'common.yes', key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE}`, - onPress: () => resolveActionableReportMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE), + onPress: () => resolveActionableReportMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE), isPrimary: true, }, { text: 'common.no', key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING}`, - onPress: () => resolveActionableReportMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING), + onPress: () => resolveActionableReportMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING), }, ]; } @@ -818,7 +819,13 @@ function PureReportActionItem({ { text: 'common.buttonConfirm', key: `${action.reportActionID}-actionableReportMentionConfirmWhisper-${CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE}`, - onPress: () => resolveActionableMentionConfirmWhisper(reportID, action, CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE, formatPhoneNumber), + onPress: () => + resolveActionableMentionConfirmWhisper( + reportActionReportID, + action, + CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE, + formatPhoneNumber, + ), isPrimary: true, }, ]; @@ -831,7 +838,8 @@ function PureReportActionItem({ actionableMentionWhisperOptions.push({ text: 'actionableMentionWhisperOptions.inviteToSubmitExpense', key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE}`, - onPress: () => resolveActionableMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, formatPhoneNumber, policy), + onPress: () => + resolveActionableMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, formatPhoneNumber, policy), isMediumSized: true, }); } @@ -840,13 +848,13 @@ function PureReportActionItem({ { text: 'actionableMentionWhisperOptions.inviteToChat', key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`, - onPress: () => resolveActionableMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, formatPhoneNumber, policy), + onPress: () => resolveActionableMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, formatPhoneNumber, policy), isMediumSized: true, }, { text: 'actionableMentionWhisperOptions.nothing', key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`, - onPress: () => resolveActionableMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, formatPhoneNumber, policy), + onPress: () => resolveActionableMentionWhisper(reportActionReportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, formatPhoneNumber, policy), isMediumSized: true, }, ); @@ -1307,6 +1315,7 @@ function PureReportActionItem({ action={action} draftMessage={draftMessage} reportID={reportID} + originalReportID={originalReportID} policyID={report?.policyID} index={index} ref={composerTextInputRef} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 53049408caab..0a096a8440b2 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -93,6 +93,9 @@ type ReportActionComposeProps = Pick; + /** The ID of the transaction thread report if there is a single transaction */ + transactionThreadReportID?: string; + /** Report transactions */ reportTransactions?: OnyxEntry; @@ -129,6 +132,7 @@ function ReportActionCompose({ onComposerBlur, didHideComposerInput, reportTransactions, + transactionThreadReportID, }: ReportActionComposeProps) { const actionSheetAwareScrollViewContext = useContext(ActionSheetAwareScrollView.ActionSheetAwareScrollViewContext); const styles = useThemeStyles(); @@ -319,11 +323,11 @@ function ReportActionCompose({ if (Array.isArray(attachmentFileRef.current)) { // Handle multiple files attachmentFileRef.current.forEach((file) => { - addAttachmentReportActions(reportID, file, newCommentTrimmed, true); + addAttachmentReportActions(transactionThreadReportID ?? reportID, reportID, file, newCommentTrimmed, true); }); } else { // Handle single file - addAttachmentReportActions(reportID, attachmentFileRef.current, newCommentTrimmed, true); + addAttachmentReportActions(transactionThreadReportID ?? reportID, reportID, attachmentFileRef.current, newCommentTrimmed, true); } attachmentFileRef.current = null; } else { @@ -332,7 +336,7 @@ function ReportActionCompose({ onSubmit(newCommentTrimmed); } }, - [onSubmit, reportID], + [onSubmit, reportID, transactionThreadReportID], ); const onTriggerAttachmentPicker = useCallback(() => { diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 3382c481cda8..a0e30c9c89f3 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,7 +1,8 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useBlockedFromConcierge} from '@components/OnyxListItemProvider'; import useOnyx from '@hooks/useOnyx'; +import useOriginalReportID from '@hooks/useOriginalReportID'; import useReportIsArchived from '@hooks/useReportIsArchived'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils'; @@ -9,7 +10,6 @@ import { chatIncludesChronosWithID, createDraftTransactionAndNavigateToParticipantSelector, getIndicatedMissingPaymentMethod, - getOriginalReportID, getReimbursementDeQueuedOrCanceledActionMessage, getTransactionsWithReceipts, isArchivedNonExpenseReport, @@ -85,8 +85,7 @@ function ReportActionItem({ }: ReportActionItemProps) { const reportID = report?.reportID; const originalMessage = getOriginalMessage(action); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const originalReportID = useMemo(() => getOriginalReportID(reportID, action), [reportID, action]); + const originalReportID = useOriginalReportID(reportID, action); const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; const isOriginalReportArchived = useReportIsArchived(originalReportID); const [currentUserAccountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (session) => session?.accountID}); diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index c1761947670e..0c3ff3883cf4 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -65,6 +65,9 @@ type ReportActionItemMessageEditProps = { /** ReportID that holds the comment we're editing */ reportID: string | undefined; + /** ID of the original report from which the given reportAction is first created */ + originalReportID: string; + /** PolicyID of the policy the report belongs to */ policyID?: string; @@ -89,7 +92,7 @@ const DEFAULT_MODAL_VALUE = { }; function ReportActionItemMessageEdit( - {action, draftMessage, reportID, policyID, index, isGroupPolicyReport, shouldDisableEmojiPicker = false}: ReportActionItemMessageEditProps, + {action, draftMessage, reportID, originalReportID, policyID, index, isGroupPolicyReport, shouldDisableEmojiPicker = false}: ReportActionItemMessageEditProps, forwardedRef: ForwardedRef, ) { const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true}); @@ -280,12 +283,12 @@ function ReportActionItemMessageEdit( // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting. if (!trimmedNewDraft) { textInputRef.current?.blur(); - ReportActionContextMenu.showDeleteModal(reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current)); + ReportActionContextMenu.showDeleteModal(originalReportID ?? reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current)); return; } - editReportComment(reportID, action, trimmedNewDraft, Object.fromEntries(draftMessageVideoAttributeCache)); + editReportComment(originalReportID ?? reportID, action, trimmedNewDraft, Object.fromEntries(draftMessageVideoAttributeCache)); deleteDraft(); - }, [action, deleteDraft, draft, reportID]); + }, [action, deleteDraft, draft, originalReportID, reportID]); /** * @param emoji diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 50219904d0ab..6b7337760206 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -53,6 +53,9 @@ type ReportFooterProps = { /** Report transactions */ reportTransactions?: OnyxEntry; + /** The ID of the transaction thread report if there is a single transaction */ + transactionThreadReportID?: string; + /** The policy of the report */ policy: OnyxEntry; @@ -82,6 +85,7 @@ function ReportFooter({ onComposerBlur, onComposerFocus, reportTransactions, + transactionThreadReportID, }: ReportFooterProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -171,10 +175,13 @@ function ReportFooter({ if (isTaskCreated) { return; } - addComment(report.reportID, text, true); + // If we are adding an action on an expense report that only has a single transaction thread child report, we need to add the action to the transaction thread instead. + // This is because we need it to be associated with the transaction thread and not the expense report in order for conversational corrections to work as expected. + const targetReportID = transactionThreadReportID ?? report.reportID; + addComment(targetReportID, report.reportID, text, true); }, // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [report.reportID, handleCreateTask], + [report.reportID, handleCreateTask, transactionThreadReportID], ); const [didHideComposerInput, setDidHideComposerInput] = useState(!shouldShowComposeInput); @@ -232,6 +239,7 @@ function ReportFooter({ isComposerFullSize={isComposerFullSize} didHideComposerInput={didHideComposerInput} reportTransactions={reportTransactions} + transactionThreadReportID={transactionThreadReportID} /> diff --git a/src/pages/settings/AboutPage/ShareLogList/index.native.tsx b/src/pages/settings/AboutPage/ShareLogList/index.native.tsx index 9150f4655f95..4272ba749426 100644 --- a/src/pages/settings/AboutPage/ShareLogList/index.native.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/index.native.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Navigation from '@libs/Navigation/Navigation'; -import * as Report from '@userActions/Report'; +import {addAttachment} from '@userActions/Report'; import ROUTES from '@src/ROUTES'; import BaseShareLogList from './BaseShareLogList'; import type {ShareLogListProps} from './types'; @@ -11,7 +11,7 @@ function ShareLogList({logSource}: ShareLogListProps) { return; } const src = `file://${logSource}`; - Report.addAttachment(reportID, {name: filename, source: src, uri: src, type: 'text/plain'} as File); + addAttachment(reportID, reportID, {name: filename, source: src, uri: src, type: 'text/plain'} as File); const routeToNavigate = ROUTES.REPORT_WITH_ID.getRoute(reportID); Navigation.navigate(routeToNavigate); diff --git a/src/pages/settings/AboutPage/ShareLogList/index.tsx b/src/pages/settings/AboutPage/ShareLogList/index.tsx index 555e44e53747..6bfcbacf6295 100644 --- a/src/pages/settings/AboutPage/ShareLogList/index.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/index.tsx @@ -1,18 +1,18 @@ import React from 'react'; -import * as FileUtils from '@libs/fileDownload/FileUtils'; +import {readFileAsync} from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as Report from '@userActions/Report'; +import {addAttachment} from '@userActions/Report'; import ROUTES from '@src/ROUTES'; import BaseShareLogList from './BaseShareLogList'; import type {ShareLogListProps} from './types'; function ShareLogList({logSource}: ShareLogListProps) { const onAttachLogToReport = (reportID: string, filename: string) => { - FileUtils.readFileAsync( + readFileAsync( logSource, filename, (file) => { - Report.addAttachment(reportID, file); + addAttachment(reportID, reportID, file); const routeToNavigate = ROUTES.REPORT_WITH_ID.getRoute(reportID); Navigation.navigate(routeToNavigate); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 0dd4d3e813e0..e3fe75c1b523 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3723,7 +3723,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); // When a comment is added - addComment(thread.reportID, 'Testing a comment'); + addComment(thread.reportID, thread.reportID, 'Testing a comment'); await waitForBatchedUpdates(); // Then comment details should match the expected report action @@ -3821,7 +3821,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); - addComment(thread.reportID, 'Testing a comment'); + addComment(thread.reportID, thread.reportID, 'Testing a comment'); await waitForBatchedUpdates(); // Fetch the updated IOU Action from Onyx due to addition of comment to transaction thread. @@ -3870,7 +3870,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); if (IOU_REPORT_ID) { - addComment(IOU_REPORT_ID, 'Testing a comment'); + addComment(IOU_REPORT_ID, IOU_REPORT_ID, 'Testing a comment'); } await waitForBatchedUpdates(); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 0c537f5a9686..adedfb0077d8 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -139,7 +139,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); return waitForBatchedUpdates(); }) .then(() => { @@ -269,7 +269,7 @@ describe('actions/Report', () => { } // And leave a comment on a report - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); // Then we should expect that there is on persisted request expect(PersistedRequests.getAll().length).toBe(1); @@ -390,7 +390,7 @@ describe('actions/Report', () => { // When a new comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, 'Current User Comment 1'); + Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 1'); return waitForBatchedUpdates(); }) .then(() => { @@ -402,7 +402,7 @@ describe('actions/Report', () => { // When another comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, 'Current User Comment 2'); + Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 2'); return waitForBatchedUpdates(); }) .then(() => { @@ -413,7 +413,7 @@ describe('actions/Report', () => { // When another comment is added by the current user currentTime = DateUtils.getDBTime(); - Report.addComment(REPORT_ID, 'Current User Comment 3'); + Report.addComment(REPORT_ID, REPORT_ID, 'Current User Comment 3'); return waitForBatchedUpdates(); }) .then(() => { @@ -678,7 +678,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); return waitForBatchedUpdates(); }) .then(() => { @@ -806,7 +806,7 @@ describe('actions/Report', () => { .then(() => { // This is a fire and forget response, but once it completes we should be able to verify that we // have an "optimistic" report action in Onyx. - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); return waitForBatchedUpdates(); }) .then(() => { @@ -896,7 +896,7 @@ describe('actions/Report', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; @@ -968,7 +968,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(1); @@ -1019,7 +1019,7 @@ describe('actions/Report', () => { const TEN_MINUTES_AGO = subMinutes(new Date(), 10); const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); await waitForNetworkPromises(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1068,7 +1068,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); const file = new File([''], 'test.txt', {type: 'text/plain'}); - Report.addAttachment(REPORT_ID, file); + Report.addAttachment(REPORT_ID, REPORT_ID, file); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1137,7 +1137,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addAttachment(REPORT_ID, file, 'Attachment with comment'); + Report.addAttachment(REPORT_ID, REPORT_ID, file, 'Attachment with comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1210,7 +1210,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); await Promise.resolve(); - Report.addComment(REPORT_ID, 'reactions with comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'reactions with comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; @@ -1306,7 +1306,7 @@ describe('actions/Report', () => { const TEN_MINUTES_AGO = subMinutes(new Date(), 10); const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); - Report.addComment(REPORT_ID, 'Attachment with comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Attachment with comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); @@ -1378,7 +1378,7 @@ describe('actions/Report', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); await waitForBatchedUpdates(); - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; @@ -1436,7 +1436,7 @@ describe('actions/Report', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - Report.addComment(REPORT_ID, 'Testing a comment'); + Report.addComment(REPORT_ID, REPORT_ID, 'Testing a comment'); // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 8a461d9cacb6..44a0575a71fc 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -448,7 +448,7 @@ describe('Unread Indicators', () => { expect(unreadIndicator).toHaveLength(1); // Leave a comment as the current user and verify the indicator is removed - addComment(REPORT_ID, 'Current User Comment 1'); + addComment(REPORT_ID, REPORT_ID, 'Current User Comment 1'); return waitForBatchedUpdates(); }) .then(() => { @@ -511,7 +511,7 @@ describe('Unread Indicators', () => { .then(() => navigateToSidebarOption(0)) .then(() => { // Leave a comment as the current user - addComment(REPORT_ID, 'Current User Comment 1'); + addComment(REPORT_ID, REPORT_ID, 'Current User Comment 1'); return waitForBatchedUpdates(); }) .then(() => { @@ -556,7 +556,7 @@ describe('Unread Indicators', () => { await signInAndGetAppWithUnreadChat(); await navigateToSidebarOption(0); - addComment(REPORT_ID, 'Comment 1'); + addComment(REPORT_ID, REPORT_ID, 'Comment 1'); await waitForBatchedUpdates(); @@ -567,7 +567,7 @@ describe('Unread Indicators', () => { await waitForBatchedUpdates(); - addComment(REPORT_ID, 'Comment 2'); + addComment(REPORT_ID, REPORT_ID, 'Comment 2'); await waitForBatchedUpdates(); diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 0efe35d485a5..c979ed5a5896 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -118,9 +118,9 @@ describe('Sidebar', () => { const report3 = LHNTestUtils.getFakeReport([1, 4], 1); // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); const reportCollectionDataSet: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, @@ -166,9 +166,9 @@ describe('Sidebar', () => { const report3 = LHNTestUtils.getFakeReport([1, 4], 1); // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); const currentReportId = report1.reportID; const reportCollectionDataSet: ReportCollectionDataSet = { @@ -215,9 +215,9 @@ describe('Sidebar', () => { const report3 = LHNTestUtils.getFakeReport([1, 4], 1); // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); const reportCollectionDataSet: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, @@ -276,9 +276,9 @@ describe('Sidebar', () => { }; // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); const reportCollectionDataSet: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, @@ -350,9 +350,9 @@ describe('Sidebar', () => { report3.iouReportID = iouReport.reportID; // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); const reportCollectionDataSet: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, @@ -429,9 +429,9 @@ describe('Sidebar', () => { report3.iouReportID = expenseReport.reportID; // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); const reportCollectionDataSet: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, @@ -479,9 +479,9 @@ describe('Sidebar', () => { const report3 = LHNTestUtils.getFakeReport([1, 4], 1); // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); const currentReportId = report2.reportID; @@ -628,7 +628,7 @@ describe('Sidebar', () => { iouReportID: undefined, }; const report4 = LHNTestUtils.getFakeReport([1, 5], 1); - addComment(report4.reportID, 'Hi, this is a comment'); + addComment(report4.reportID, report4.reportID, 'Hi, this is a comment'); const iouReport: OnyxTypes.Report = { ...LHNTestUtils.getFakeReport([1, 4]), @@ -850,9 +850,9 @@ describe('Sidebar', () => { const report3 = LHNTestUtils.getFakeReport([1, 4]); // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); // Given the user is in all betas const betas = [CONST.BETAS.DEFAULT_ROOMS]; @@ -904,9 +904,9 @@ describe('Sidebar', () => { const report3: OnyxTypes.Report = LHNTestUtils.getFakeReport([1, 4]); // Each report has at least one ADD_COMMENT action so should be rendered in the LNH - addComment(report1.reportID, 'Hi, this is a comment'); - addComment(report2.reportID, 'Hi, this is a comment'); - addComment(report3.reportID, 'Hi, this is a comment'); + addComment(report1.reportID, report1.reportID, 'Hi, this is a comment'); + addComment(report2.reportID, report2.reportID, 'Hi, this is a comment'); + addComment(report3.reportID, report3.reportID, 'Hi, this is a comment'); const reportCollectionDataSet: ReportCollectionDataSet = { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1,