diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ad9859dae448..a86dbc147e85 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -86,8 +86,7 @@ Onyx.connect({ callback: (val) => (isNetworkOffline = val?.isOffline ?? false), }); -let currentUserAccountID: number | undefined; -let currentEmail = ''; +let deprecatedCurrentUserAccountID: number | undefined; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (value) => { @@ -96,8 +95,7 @@ Onyx.connect({ return; } - currentUserAccountID = value.accountID; - currentEmail = value?.email ?? ''; + deprecatedCurrentUserAccountID = value.accountID; }, }); @@ -443,7 +441,7 @@ function isWhisperActionTargetedToOthers(reportAction: OnyxInputOrEntry): reportAction is ReportAction { @@ -2216,18 +2214,18 @@ function getMentionedEmailsFromMessage(message: string) { return matches.map((match) => Str.removeSMSDomain(match[1].substring(1))); } -function didMessageMentionCurrentUser(reportAction: OnyxInputOrEntry) { +function didMessageMentionCurrentUser(reportAction: OnyxInputOrEntry, currentUserEmail: string) { const accountIDsFromMessage = getMentionedAccountIDsFromAction(reportAction); const message = getReportActionMessage(reportAction)?.html ?? ''; const emailsFromMessage = getMentionedEmailsFromMessage(message); - return accountIDsFromMessage.includes(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID) || emailsFromMessage.includes(currentEmail) || message.includes(''); + return accountIDsFromMessage.includes(deprecatedCurrentUserAccountID ?? CONST.DEFAULT_NUMBER_ID) || emailsFromMessage.includes(currentUserEmail) || message.includes(''); } /** * Check if the current user is the requestor of the action */ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry): boolean { - return currentUserAccountID === reportAction?.actorAccountID; + return deprecatedCurrentUserAccountID === reportAction?.actorAccountID; } /** @@ -3227,7 +3225,7 @@ function getRemovedFromApprovalChainMessage(translate: LocalizedTranslate, repor const originalMessage = getOriginalMessage(reportAction); const submittersNames = getPersonalDetailsByIDs({ accountIDs: originalMessage?.submittersAccountIDs ?? [], - currentUserAccountID: currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, + currentUserAccountID: deprecatedCurrentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, }).map(({displayName, login}) => displayName ?? login ?? 'Unknown Submitter'); return translate('workspaceActions.removedFromApprovalWorkflow', {submittersNames, count: submittersNames.length}); } @@ -3393,7 +3391,7 @@ function getCardIssuedMessage({ const isExpensifyCardActive = isCardActive(expensifyCard); const expensifyCardLink = (expensifyCardLinkText: string) => shouldRenderHTML && isExpensifyCardActive ? `${expensifyCardLinkText}` : expensifyCardLinkText; - const isAssigneeCurrentUser = currentUserAccountID === assigneeAccountID; + const isAssigneeCurrentUser = deprecatedCurrentUserAccountID === assigneeAccountID; const companyCardLink = shouldRenderHTML && isAssigneeCurrentUser && companyCard ? `${translate('workspace.companyCards.companyCard')}` diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 05d4935f0fe7..5c4ffc9e74b1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1992,6 +1992,7 @@ function deleteReportComment( ancestors: Ancestor[], isReportArchived: boolean | undefined, isOriginalReportArchived: boolean | undefined, + currentEmail: string, ) { const originalReportID = getOriginalReportID(reportID, reportAction); const reportActionID = reportAction.reportActionID; @@ -2031,13 +2032,13 @@ function deleteReportComment( ...optimisticLastReportData, }; - const didCommentMentionCurrentUser = ReportActionsUtils.didMessageMentionCurrentUser(reportAction); + const didCommentMentionCurrentUser = ReportActionsUtils.didMessageMentionCurrentUser(reportAction, currentEmail); if (didCommentMentionCurrentUser && reportAction.created === report?.lastMentionedTime) { const reportActionsForReport = allReportActions?.[reportID]; const latestMentionedReportAction = Object.values(reportActionsForReport ?? {}).find( (action) => action.reportActionID !== reportAction.reportActionID && - ReportActionsUtils.didMessageMentionCurrentUser(action) && + ReportActionsUtils.didMessageMentionCurrentUser(action, currentEmail) && ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID), ); optimisticReport.lastMentionedTime = latestMentionedReportAction?.created ?? null; @@ -2168,6 +2169,7 @@ function editReportComment( textForNewComment: string, isOriginalReportArchived: boolean | undefined, isOriginalParentReportArchived: boolean | undefined, + currentEmail: string, videoAttributeCache?: Record, ) { const originalReportID = originalReport?.reportID; @@ -2201,7 +2203,7 @@ function editReportComment( // Delete the comment if it's empty if (!htmlForNewComment) { - deleteReportComment(originalReportID, originalReportAction, ancestors, isOriginalReportArchived, isOriginalParentReportArchived); + deleteReportComment(originalReportID, originalReportAction, ancestors, isOriginalReportArchived, isOriginalParentReportArchived, currentEmail); return; } diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 422166b4373c..913fd583b523 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -370,7 +370,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived); + deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? ''); }); } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 1a3a10679ad3..7a5f00e6ce76 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -16,6 +16,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip'; import useAncestors from '@hooks/useAncestors'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered'; import useKeyboardState from '@hooks/useKeyboardState'; @@ -110,6 +111,7 @@ function ReportActionItemMessageEdit({ ref, }: ReportActionItemMessageEditProps) { const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true}); + const {email} = useCurrentUserPersonalDetails(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -314,9 +316,30 @@ function ReportActionItemMessageEdit({ ReportActionContextMenu.showDeleteModal(originalReportID ?? reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current)); return; } - editReportComment(originalReport, action, ancestors, trimmedNewDraft, isOriginalReportArchived, isOriginalParentReportArchived, Object.fromEntries(draftMessageVideoAttributeCache)); + editReportComment( + originalReport, + action, + ancestors, + trimmedNewDraft, + isOriginalReportArchived, + isOriginalParentReportArchived, + email ?? '', + Object.fromEntries(draftMessageVideoAttributeCache), + ); deleteDraft(); - }, [reportID, action, ancestors, deleteDraft, draft, originalReportID, isOriginalReportArchived, originalReport, isOriginalParentReportArchived, debouncedValidateCommentMaxLength]); + }, [ + reportID, + action, + ancestors, + deleteDraft, + draft, + originalReportID, + isOriginalReportArchived, + originalReport, + isOriginalParentReportArchived, + debouncedValidateCommentMaxLength, + email, + ]); /** * @param emoji diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index f321feb450ae..2071112bc4a2 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -511,7 +511,7 @@ describe('actions/Report', () => { .then(() => { rerender(report); // If the user deletes a comment that is before the last read - Report.deleteReportComment(REPORT_ID, {...reportActions[200]}, ancestors.current, undefined, undefined); + Report.deleteReportComment(REPORT_ID, {...reportActions[200]}, ancestors.current, undefined, undefined, USER_1_LOGIN); return waitForBatchedUpdates(); }) .then(() => { @@ -530,7 +530,7 @@ describe('actions/Report', () => { rerender(report); // If the user deletes the last comment after the lastReadTime the lastMessageText will reflect the new last comment - Report.deleteReportComment(REPORT_ID, {...reportActions[400]}, ancestors.current, undefined, undefined); + Report.deleteReportComment(REPORT_ID, {...reportActions[400]}, ancestors.current, undefined, undefined, USER_1_LOGIN); return waitForBatchedUpdates(); }) .then(() => { @@ -1003,7 +1003,7 @@ describe('actions/Report', () => { }; const {result: ancestors, rerender} = renderHook(() => useAncestors(originalReport)); - Report.editReportComment(originalReport, newReportAction, ancestors.current, 'Testing an edited comment', undefined, undefined); + Report.editReportComment(originalReport, newReportAction, ancestors.current, 'Testing an edited comment', undefined, undefined, ''); await waitForBatchedUpdates(); @@ -1037,7 +1037,7 @@ describe('actions/Report', () => { }); rerender(originalReport); - Report.deleteReportComment(REPORT_ID, newReportAction, ancestors.current, undefined, undefined); + Report.deleteReportComment(REPORT_ID, newReportAction, ancestors.current, undefined, undefined, ''); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1061,6 +1061,41 @@ describe('actions/Report', () => { TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 0); }); + it('should remove AddComment and UpdateComment without sending any request when DeleteComment is set with currentUserEmail', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const REPORT: OnyxTypes.Report = createRandomReport(1, undefined); + const created = format(addSeconds(subMinutes(new Date(), 10), 10), CONST.DATE.FNS_DB_FORMAT_STRING); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + Report.addComment(REPORT, REPORT_ID, [], 'Testing a comment', CONST.DEFAULT_TIME_ZONE); + + const reportActionID = PersistedRequests.getAll().at(0)?.data?.reportActionID as string | undefined; + const newReportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + const originalReport = {reportID: REPORT_ID}; + const {result: ancestors, rerender} = renderHook(() => useAncestors(originalReport)); + + const currentUserEmail = 'test@test.com'; + Report.editReportComment(originalReport, newReportAction, ancestors.current, 'Testing an edited comment', undefined, undefined, currentUserEmail); + await waitForBatchedUpdates(); + + const persistedRequests = await getOnyxValue(ONYXKEYS.PERSISTED_REQUESTS); + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); + + rerender(originalReport); + Report.deleteReportComment(REPORT_ID, newReportAction, ancestors.current, undefined, undefined, currentUserEmail); + await waitForBatchedUpdates(); + + expect(PersistedRequests.getAll().length).toBe(0); + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 0); + }); + it('should send DeleteComment request and remove UpdateComment accordingly', async () => { global.fetch = TestHelper.getGlobalFetchMock(); @@ -1086,7 +1121,7 @@ describe('actions/Report', () => { }; const {result: ancestors, rerender} = renderHook(() => useAncestors(originalReport)); - Report.editReportComment(originalReport, reportAction, ancestors.current, 'Testing an edited comment', undefined, undefined); + Report.editReportComment(originalReport, reportAction, ancestors.current, 'Testing an edited comment', undefined, undefined, ''); await waitForBatchedUpdates(); @@ -1102,7 +1137,7 @@ describe('actions/Report', () => { }); rerender(originalReport); - Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current, undefined, undefined); + Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current, undefined, undefined, ''); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1152,7 +1187,7 @@ describe('actions/Report', () => { }), ); - Report.deleteReportComment(REPORT_ID, reportAction, [], undefined, undefined); + Report.deleteReportComment(REPORT_ID, reportAction, [], undefined, undefined, ''); jest.runOnlyPendingTimers(); await waitForBatchedUpdates(); @@ -1215,7 +1250,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined); + Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined, ''); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1285,7 +1320,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined); + Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined, ''); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1485,7 +1520,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined); + Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined, ''); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1570,7 +1605,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, reportAction, [], undefined, undefined); + Report.deleteReportComment(REPORT_ID, reportAction, [], undefined, undefined, ''); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1619,7 +1654,7 @@ describe('actions/Report', () => { const {result: ancestors} = renderHook(() => useAncestors({reportID: REPORT_ID})); - Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current, undefined, undefined); + Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current, undefined, undefined, ''); expect(PersistedRequests.getAll().length).toBe(3); @@ -1658,7 +1693,7 @@ describe('actions/Report', () => { const originalReport = { reportID: REPORT_ID, }; - Report.editReportComment(originalReport, reportAction, [], 'Testing an edited comment', undefined, undefined); + Report.editReportComment(originalReport, reportAction, [], 'Testing an edited comment', undefined, undefined, ''); await waitForBatchedUpdates(); @@ -1696,9 +1731,9 @@ describe('actions/Report', () => { const {result: ancestors} = renderHook(() => useAncestors(originalReport)); - Report.editReportComment(originalReport, action, ancestors.current, 'value1', undefined, undefined); - Report.editReportComment(originalReport, action, ancestors.current, 'value2', undefined, undefined); - Report.editReportComment(originalReport, action, ancestors.current, 'value3', undefined, undefined); + Report.editReportComment(originalReport, action, ancestors.current, 'value1', undefined, undefined, ''); + Report.editReportComment(originalReport, action, ancestors.current, 'value2', undefined, undefined, ''); + Report.editReportComment(originalReport, action, ancestors.current, 'value3', undefined, undefined, ''); const requests = PersistedRequests?.getAll(); @@ -1711,6 +1746,33 @@ describe('actions/Report', () => { TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 1); }); + it('it should only send the last sequential UpdateComment request to BE with currentUserEmail', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + const action: OnyxEntry = { + reportID: '123', + reportActionID: '722', + actionName: 'ADDCOMMENT', + created: '2024-10-21 10:37:59.881', + }; + const originalReport = {reportID: '123'}; + const {result: ancestors} = renderHook(() => useAncestors(originalReport)); + const currentUserEmail = 'user@test.com'; + + Report.editReportComment(originalReport, action, ancestors.current, 'value1', undefined, undefined, currentUserEmail); + Report.editReportComment(originalReport, action, ancestors.current, 'value2', undefined, undefined, currentUserEmail); + Report.editReportComment(originalReport, action, ancestors.current, 'value3', undefined, undefined, currentUserEmail); + + const requests = PersistedRequests?.getAll(); + expect(requests.length).toBe(1); + expect(requests?.at(0)?.command).toBe(WRITE_COMMANDS.UPDATE_COMMENT); + expect(requests?.at(0)?.data?.reportComment).toBe('value3'); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 1); + }); + it('should clears lastMentionedTime when all mentions to the current user are deleted', async () => { const reportID = '1'; const mentionActionID = '1'; @@ -1749,8 +1811,8 @@ describe('actions/Report', () => { const {result: ancestors} = renderHook(() => useAncestors(report)); - Report.deleteReportComment(reportID, mentionAction, ancestors.current, undefined, undefined); - Report.deleteReportComment(reportID, mentionAction2, ancestors.current, undefined, undefined); + Report.deleteReportComment(reportID, mentionAction, ancestors.current, undefined, undefined, ''); + Report.deleteReportComment(reportID, mentionAction2, ancestors.current, undefined, undefined, ''); await waitForBatchedUpdates(); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 849046531699..6407045cd712 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -582,7 +582,7 @@ describe('Unread Indicators', () => { expect(screen.getAllByText('Current User Comment 1').at(0)).toBeOnTheScreen(); if (lastReportAction) { - deleteReportComment(REPORT_ID, lastReportAction, [], undefined, undefined); + deleteReportComment(REPORT_ID, lastReportAction, [], undefined, undefined, ''); } return waitForBatchedUpdates(); }) @@ -620,7 +620,7 @@ describe('Unread Indicators', () => { await waitForBatchedUpdates(); - deleteReportComment(REPORT_ID, firstNewReportAction, [], undefined, undefined); + deleteReportComment(REPORT_ID, firstNewReportAction, [], undefined, undefined, ''); await waitForBatchedUpdates(); } diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 8ed77eb62931..233b01a9278c 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -2260,6 +2260,78 @@ describe('ReportActionsUtils', () => { }); }); + describe('didMessageMentionCurrentUser', () => { + const currentUserEmail = 'currentuser@example.com'; + const otherUserEmail = 'otheruser@example.com'; + const otherUserAccountID = 456; + + it('should return true when email matches', () => { + const reportAction: ReportAction = { + ...createRandomReportAction(0), + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + message: [ + { + html: `@${currentUserEmail}`, + type: 'COMMENT', + text: `@${currentUserEmail}`, + }, + ], + originalMessage: { + html: `@${currentUserEmail}`, + whisperedTo: [], + mentionedAccountIDs: [], + }, + }; + + const result = ReportActionsUtils.didMessageMentionCurrentUser(reportAction, currentUserEmail); + expect(result).toBe(true); + }); + + it('should return true when message includes mention-here', () => { + const reportAction: ReportAction = { + ...createRandomReportAction(0), + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + message: [ + { + html: 'Hello ', + type: 'COMMENT', + text: 'Hello @here', + }, + ], + originalMessage: { + html: 'Hello ', + whisperedTo: [], + mentionedAccountIDs: [], + }, + }; + + const result = ReportActionsUtils.didMessageMentionCurrentUser(reportAction, currentUserEmail); + expect(result).toBe(true); + }); + + it('should return false when user is not mentioned', () => { + const reportAction: ReportAction = { + ...createRandomReportAction(0), + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + message: [ + { + html: `@${otherUserEmail}`, + type: 'COMMENT', + text: `@${otherUserEmail}`, + }, + ], + originalMessage: { + html: `@${otherUserEmail}`, + whisperedTo: [], + mentionedAccountIDs: [otherUserAccountID], + }, + }; + + const result = ReportActionsUtils.didMessageMentionCurrentUser(reportAction, currentUserEmail); + expect(result).toBe(false); + }); + }); + describe('getReportActionActorAccountID', () => { it('should return report owner account id if action is REPORTPREVIEW and report is a policy expense chat', () => { const reportAction: ReportAction = {