diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8c43ae542932..469f17258a7f 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -116,6 +116,8 @@ function LHNOptionsList({ const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); const lastReportAction = sortedReportActions[0]; + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, itemReportActions); + const transactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; // Get the transaction for the last report action let lastReportActionTransactionID = ''; @@ -129,6 +131,7 @@ function LHNOptionsList({ {}, opti false, optionItem.isPinned, !!optionItem.isUnread, + [], + false, + () => {}, + false, + optionItem.transactionThreadReportID, ); }; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index c80017c39a3d..1d6cc636f939 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -27,6 +27,7 @@ function OptionRowLHNData({ lastReportActionTransaction = {}, transactionViolations, canUseViolations, + transactionThreadReport, ...propsToForward }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; @@ -47,6 +48,7 @@ function OptionRowLHNData({ policy, parentReportAction, hasViolations: !!shouldDisplayViolations, + transactionThreadReport, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; @@ -70,6 +72,7 @@ function OptionRowLHNData({ transactionViolations, canUseViolations, receiptTransactions, + transactionThreadReport, ]); return ( diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 0f0c921747b4..79b5f5ad3889 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -74,6 +74,9 @@ type OptionRowLHNDataProps = { /** The full data of the report */ fullReport: OnyxEntry; + /** The transaction thread report associated with the current report – applicable only for one-transaction money reports */ + transactionThreadReport: OnyxEntry; + /** The policy which the user has access to and which the report could be tied to */ policy?: OnyxEntry; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c6d70a9da71c..4faafab2950f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -440,6 +440,7 @@ type OptionData = { reportID?: string; enabled?: boolean; data?: Partial; + transactionThreadReportID?: string | null; } & Report; type OnyxDataTaskAssigneeChat = { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index a218534e6b16..c86d91c44ff9 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -190,6 +190,7 @@ function getOptionData({ policy, parentReportAction, hasViolations, + transactionThreadReport, }: { report: OnyxEntry; reportActions: OnyxEntry; @@ -198,6 +199,7 @@ function getOptionData({ policy: OnyxEntry | undefined; parentReportAction: OnyxEntry | undefined; hasViolations: boolean; + transactionThreadReport: OnyxEntry; }): ReportUtils.OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do @@ -234,6 +236,7 @@ function getOptionData({ isWaitingOnBankAccount: false, isAllowedToComment: true, isDeletedParentAction: false, + transactionThreadReportID: transactionThreadReport?.reportID, }; let participantAccountIDs = report.participantAccountIDs ?? []; @@ -266,7 +269,7 @@ function getOptionData({ result.statusNum = report.statusNum; // When the only message of a report is deleted lastVisibileActionCreated is not reset leading to wrongly // setting it Unread so we add additional condition here to avoid empty chat LHN from being bold. - result.isUnread = ReportUtils.isUnread(report) && !!report.lastActorAccountID; + result.isUnread = (ReportUtils.isUnread(report) && !!report.lastActorAccountID) || (ReportUtils.isUnread(transactionThreadReport) && !!transactionThreadReport?.lastActorAccountID); result.isUnreadWithMention = ReportUtils.isUnreadWithMention(report); result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 46ebdd751762..8e2c9d6f09b3 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -49,6 +49,9 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types originalReportID: string; + /** The ID of transaction thread report associated with the current report, if any */ + transactionThreadReportID: string; + /** * If true, this component will be a small, row-oriented menu that displays icons but not text. * If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. @@ -117,6 +120,7 @@ function BaseReportActionContextMenu({ checkIfContextMenuActive, disabledActions = [], setIsEmojiPickerActive, + transactionThreadReportID, }: BaseReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -245,6 +249,7 @@ function BaseReportActionContextMenu({ interceptAnonymousUser, openOverflowMenu, setIsEmojiPickerActive, + transactionThreadReportID, }; if ('renderContent' in contextAction) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 438d09e778b4..a09f0355b996 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -79,6 +79,7 @@ type ContextMenuActionPayload = { event?: GestureResponderEvent | MouseEvent | KeyboardEvent; setIsEmojiPickerActive?: (state: boolean) => void; anchorRef?: MutableRefObject; + transactionThreadReportID?: string; }; type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; @@ -213,8 +214,11 @@ const ContextMenuActions: ContextMenuAction[] = [ successIcon: Expensicons.Checkmark, shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, - onPress: (closePopover, {reportID}) => { + onPress: (closePopover, {reportID, transactionThreadReportID}) => { Report.readNewestAction(reportID); + if (transactionThreadReportID && transactionThreadReportID !== '0') { + Report.readNewestAction(transactionThreadReportID); + } if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 6cb688ff2558..38fd37ee4d26 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -37,6 +37,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef>(null); const reportActionIDRef = useRef('0'); const originalReportIDRef = useRef('0'); + const transactionThreadReportIDRef = useRef('0'); const selectionRef = useRef(''); const reportActionDraftMessageRef = useRef(); @@ -171,6 +172,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {}, isOverflowMenu = false, + transactionThreadReportID = undefined, ) => { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -212,6 +214,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef void, isOverflowMenu?: boolean, + transactionThreadReportID?: string, ) => void; type ReportActionContextMenu = { @@ -119,6 +120,7 @@ function showContextMenu( shouldCloseOnTarget = false, setIsEmojiPickerActive = () => {}, isOverflowMenu = false, + transactionThreadReportID = '0', ) { if (!contextMenuRef.current) { return; @@ -149,6 +151,7 @@ function showContextMenu( shouldCloseOnTarget, setIsEmojiPickerActive, isOverflowMenu, + transactionThreadReportID, ); } diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index efb2d8ba73fb..57139dda6d50 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -920,6 +920,7 @@ function ReportActionItem({ isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} + transactionThreadReportID={transactionThreadReport?.reportID ?? '0'} /> { + resetUnreadMarker(newLastReadTime); + setMessageManuallyMarkedUnread(new Date().getTime()); + }); + + readNewestActionSubscriptionForTransactionThread = DeviceEventEmitter.addListener(`readNewestAction_${transactionThreadReport?.reportID}`, (newLastReadTime) => { + resetUnreadMarker(newLastReadTime); + setMessageManuallyMarkedUnread(0); + }); + } + return () => { unreadActionSubscription.remove(); readNewestActionSubscription.remove(); deletedReportActionSubscription.remove(); + unreadActionSubscriptionForTransactionThread?.remove(); + readNewestActionSubscriptionForTransactionThread?.remove(); }; - }, [report.reportID]); + }, [report.reportID, transactionThreadReport?.reportID]); useEffect(() => { if (linkedReportActionID) { @@ -401,6 +418,9 @@ function ReportActionsList({ reportScrollManager.scrollToBottom(); readActionSkipped.current = false; Report.readNewestAction(report.reportID); + if (transactionThreadReport?.reportID) { + Report.readNewestAction(transactionThreadReport?.reportID); + } }; /** diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 8566abb97c7f..b19e59dbdcc0 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -106,6 +106,7 @@ describe('SidebarUtils', () => { policy, parentReportAction, hasViolations: false, + transactionThreadReport: null, }), ); });