From e96ce99d146a022b2d5d257b3575e0e7dc8e462d Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 17 Jan 2024 17:58:28 +0700 Subject: [PATCH 01/10] implement get all ancestor of the thread --- src/libs/ReportActionsUtils.ts | 24 +++ src/libs/ReportUtils.ts | 40 +++++ src/pages/home/report/ReportActionItem.js | 5 + .../report/ReportActionItemParentAction.js | 139 ++++++++++++------ .../report/ReportActionsListItemRenderer.js | 1 - 5 files changed, 165 insertions(+), 44 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f967cb244268..c856368cb8d6 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -810,6 +810,29 @@ function hasRequestFromCurrentAccount(reportID: string, currentAccountID: number return reportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.actorAccountID === currentAccountID); } +function isReportActionUnread(reportAction: OnyxEntry, lastReadTime: string) { + if (!lastReadTime) { + return Boolean(!isCreatedAction(reportAction)); + } + + return Boolean(reportAction && lastReadTime && reportAction.created && lastReadTime < reportAction.created); +} + +/** + * Check whether the report action of the report is unread or not + * + */ +function isCurrentActionUnread(report: Report | EmptyObject, reportAction: ReportAction): boolean { + const lastReadTime = report.lastReadTime ?? ''; + const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report.reportID))); + const currentActionIndex = sortedReportActions.findIndex((action) => action.reportActionID === reportAction.reportActionID); + if (currentActionIndex === -1) { + return false; + } + const nextReportAction = sortedReportActions[currentActionIndex + 1]; + return isReportActionUnread(reportAction, lastReadTime) && (!nextReportAction || !isReportActionUnread(nextReportAction, lastReadTime)); +} + export { extractLinksFromMessageHtml, getAllReportActions, @@ -860,6 +883,7 @@ export { getMemberChangeMessageFragment, getMemberChangeMessagePlainText, isReimbursementDeQueuedAction, + isCurrentActionUnread, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e4b82aa36c7a..7942b2105b60 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -367,6 +367,13 @@ type OnyxDataTaskAssigneeChat = { optimisticChatCreatedReportAction?: OptimisticCreatedReportAction; }; +type Ancestor = { + report: Report; + reportAction: ReportAction; + shouldDisplayNewMarker: boolean; + shouldHideThreadDividerLine: boolean; +}; + let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -4434,6 +4441,38 @@ function shouldDisableThread(reportAction: OnyxEntry, reportID: st ); } +function getAllAncestorReportActions(report: Report): Ancestor[] { + let parentReportID = report.parentReportID; + let parentReportActionID = report.parentReportActionID; + // Store the child of parent report + let currentReport = report; + let currentUnread = false; + const allAncestors: Ancestor[] = []; + while (parentReportID) { + const parentReport = getReport(parentReportID); + const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '0'); + if (!parentReportAction || ReportActionsUtils.isTransactionThread(parentReportAction) || !parentReport) { + break; + } + const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport, parentReportAction); + allAncestors.push({ + report: currentReport, + reportAction: parentReportAction, + shouldDisplayNewMarker: isParentReportActionUnread, + // We should hide the thread divider line if the previous ancestor action is unread + shouldHideThreadDividerLine: currentUnread, + }); + parentReportID = parentReport?.parentReportID; + parentReportActionID = parentReport?.parentReportActionID; + if (!isEmptyObject(parentReport)) { + currentReport = parentReport; + currentUnread = isParentReportActionUnread; + } + } + + return allAncestors.reverse(); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4613,6 +4652,7 @@ export { shouldDisplayThreadReplies, shouldDisableThread, getChildReportNotificationPreference, + getAllAncestorReportActions, }; export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport, OptimisticCreatedReportAction}; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9573d4a4ff1a..5efb6191e206 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -120,6 +120,9 @@ const propTypes = { /** All the report actions belonging to the report's parent */ parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + + /** Callback to be called on onPress */ + onPress: PropTypes.func, }; const defaultProps = { @@ -131,6 +134,7 @@ const defaultProps = { shouldHideThreadDividerLine: false, userWallet: {}, parentReportActions: {}, + onPress: undefined, }; function ReportActionItem(props) { @@ -693,6 +697,7 @@ function ReportActionItem(props) { return ( props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js index d1a294881eb9..cc50c034c554 100644 --- a/src/pages/home/report/ReportActionItemParentAction.js +++ b/src/pages/home/report/ReportActionItemParentAction.js @@ -3,19 +3,22 @@ import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import withLocalize from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; +import reportActionFragmentPropTypes from './reportActionFragmentPropTypes'; import ReportActionItem from './ReportActionItem'; -import reportActionPropTypes from './reportActionPropTypes'; const propTypes = { /** Flag to show, hide the thread divider line */ @@ -27,60 +30,70 @@ const propTypes = { /** Position index of the report parent action in the overall report FlatList view */ index: PropTypes.number.isRequired, - /** The id of the parent report */ - // eslint-disable-next-line react/no-unused-prop-types - parentReportID: PropTypes.string.isRequired, - /** ONYX PROPS */ - /** The report currently being looked at */ - report: reportPropTypes, + /** List of reports */ + /* eslint-disable-next-line react/no-unused-prop-types */ + allReports: PropTypes.objectOf(reportPropTypes), - /** The actions from the parent report */ - // TO DO: Replace with HOC https://github.com/Expensify/App/issues/18769. - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** All report actions for all reports */ + /* eslint-disable-next-line react/no-unused-prop-types */ + allReportActions: PropTypes.objectOf( + PropTypes.arrayOf( + PropTypes.shape({ + error: PropTypes.string, + message: PropTypes.arrayOf(reportActionFragmentPropTypes), + created: PropTypes.string, + pendingAction: PropTypes.oneOf(['add', 'update', 'delete']), + }), + ), + ), ...windowDimensionsPropTypes, }; const defaultProps = { - report: {}, - parentReportActions: {}, + allReports: {}, + allReportActions: {}, shouldHideThreadDividerLine: false, }; function ReportActionItemParentAction(props) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const parentReportAction = props.parentReportActions[`${props.report.parentReportActionID}`]; + const report = ReportUtils.getReport(props.reportID); + const allAncestors = ReportUtils.getAllAncestorReportActions(report); - // In case of transaction threads, we do not want to render the parent report action. - if (ReportActionsUtils.isTransactionThread(parentReportAction)) { - return null; - } return ( - Report.navigateToConciergeChatAndDeleteReport(props.report.reportID)} - > - + <> + - {parentReportAction && ( - - )} + {_.map(allAncestors, (ancestor, index) => { + const isNearestAncestor = index === allAncestors.length - 1; + const shouldHideThreadDividerLine = isNearestAncestor ? props.shouldHideThreadDividerLine : ancestor.shouldHideThreadDividerLine; + return ( + Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} + > + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))} + report={ancestor.report} + action={ancestor.reportAction} + displayAsGroup={false} + isMostRecentIOUReportAction={false} + shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} + index={props.index} + /> + {!shouldHideThreadDividerLine && } + + ); + })} - {!props.shouldHideThreadDividerLine && } - + ); } @@ -88,16 +101,56 @@ ReportActionItemParentAction.defaultProps = defaultProps; ReportActionItemParentAction.propTypes = propTypes; ReportActionItemParentAction.displayName = 'ReportActionItemParentAction'; +/** + * @param {Object} [reportActions] + * @returns {Object|undefined} + */ +const reportActionsSelector = (reportActions) => + reportActions && + _.map(reportActions, (reportAction) => ({ + errors: lodashGet(reportAction, 'errors', []), + pendingAction: lodashGet(reportAction, 'pendingAction'), + message: reportAction.message, + created: reportAction.created, + })); + +/** + * @param {Object} [report] + * @returns {Object|undefined} + */ +const reportSelector = (report) => + report && { + reportID: report.reportID, + isHidden: report.isHidden, + errorFields: { + createChat: report.errorFields && report.errorFields.createChat, + addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, + }, + pendingFields: { + createChat: report.pendingFields && report.pendingFields.createChat, + addWorkspaceRoom: report.pendingFields && report.pendingFields.addWorkspaceRoom, + }, + statusNum: report.statusNum, + stateNum: report.stateNum, + lastReadTime: report.lastReadTime, + // Other important less obivous properties for filtering: + parentReportActionID: report.parentReportActionID, + parentReportID: report.parentReportID, + isDeletedParentAction: report.isDeletedParentAction, + }; + export default compose( withWindowDimensions, withLocalize, withOnyx({ - report: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + // We should subscribe all reports and report actions here to dynamic update when any parent report action is changed + allReportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + selector: reportActionsSelector, }, - parentReportActions: { - key: ({parentReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, - canEvict: false, + allReports: { + key: ONYXKEYS.COLLECTION.REPORT, + selector: reportSelector, }, }), )(ReportActionItemParentAction); diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index a9ae2b4c73b9..13c7517e6fa6 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -59,7 +59,6 @@ function ReportActionsListItemRenderer({ From 07ccc0cde546de2970e26ec16751abaf1f1719ef Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 22 Jan 2024 12:00:49 +0700 Subject: [PATCH 02/10] get correct reportID when mark as unread --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index ea25a00ee1d3..7551e03b01ca 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -245,7 +245,8 @@ const ContextMenuActions: ContextMenuAction[] = [ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat), onPress: (closePopover, {reportAction, reportID}) => { - Report.markCommentAsUnread(reportID, reportAction?.created); + const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction) ?? ''; + Report.markCommentAsUnread(originalReportID, reportAction?.created); if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } From ce57da7b6cb799d97748490c6fde4cc00e3e2361 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 22 Jan 2024 12:44:50 +0700 Subject: [PATCH 03/10] correctly logic check unread --- src/libs/ReportActionsUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index cce9ed548eac..b32c7c2a6a84 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -829,8 +829,8 @@ function isCurrentActionUnread(report: Report | EmptyObject, reportAction: Repor if (currentActionIndex === -1) { return false; } - const nextReportAction = sortedReportActions[currentActionIndex + 1]; - return isReportActionUnread(reportAction, lastReadTime) && (!nextReportAction || !isReportActionUnread(nextReportAction, lastReadTime)); + const prevReportAction = sortedReportActions[currentActionIndex - 1]; + return isReportActionUnread(reportAction, lastReadTime) && (!prevReportAction || !isReportActionUnread(prevReportAction, lastReadTime)); } export { From 40693fb27fd5218724618a981a82a105ae854eb4 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 25 Jan 2024 18:22:30 +0700 Subject: [PATCH 04/10] remove unuse prop --- src/pages/home/report/ReportActionItemParentAction.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index d787d4075e85..dd279d5a1409 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -35,10 +35,6 @@ type ReportActionItemParentActionProps = ReportActionItemParentActionOnyxProps & /** The id of the report */ // eslint-disable-next-line react/no-unused-prop-types reportID: string; - - /** The id of the parent report */ - // eslint-disable-next-line react/no-unused-prop-types - parentReportID: string; }; function ReportActionItemParentAction({allReportActions = {}, allReports = {}, index = 0, shouldHideThreadDividerLine = false, reportID}: ReportActionItemParentActionProps) { From 850ba2429a88b0fd41c90f6ba70df8ba0c466ffc Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 29 Jan 2024 14:13:31 +0700 Subject: [PATCH 05/10] move key prop to first element --- src/pages/home/report/ReportActionItemParentAction.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 9031d4c24656..048c5bf6b7cd 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -51,6 +51,7 @@ function ReportActionItemParentAction({allReportActions = {}, allReports = {}, i {allAncestors.map((ancestor) => ( Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} > Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))} report={ancestor.report} From 56d5d2c717e0da30066fee5eda7f0da605fbf50e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 30 Jan 2024 14:29:28 +0700 Subject: [PATCH 06/10] refactor logic --- src/libs/ReportActionsUtils.ts | 5 ++++- src/libs/ReportUtils.ts | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b0f66ee6a99a..ba93d8169dab 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -806,6 +806,9 @@ function hasRequestFromCurrentAccount(reportID: string, currentAccountID: number return reportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.actorAccountID === currentAccountID); } +/** + * @private + */ function isReportActionUnread(reportAction: OnyxEntry, lastReadTime: string) { if (!lastReadTime) { return Boolean(!isCreatedAction(reportAction)); @@ -815,7 +818,7 @@ function isReportActionUnread(reportAction: OnyxEntry, lastReadTim } /** - * Check whether the report action of the report is unread or not + * Check whether the current report action of the report is unread or not * */ function isCurrentActionUnread(report: Report | EmptyObject, reportAction: ReportAction, reportActions: ReportActions): boolean { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 960ad55537ea..2646f6c36051 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12,7 +12,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; +import ONYXKEYS, { OnyxCollectionKey } from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; @@ -4613,34 +4613,38 @@ function getAllAncestorReportActions( if (!report) { return []; } - const convertReports: OnyxCollection = {}; - const convertReportActions: OnyxCollection = {}; + const convertedReports: OnyxCollection = {}; + const convertedReportActions: OnyxCollection = {}; Object.values(reports ?? {}).forEach((itemReport) => { if (!itemReport) { return; } - convertReports[itemReport.reportID] = itemReport; + convertedReports[itemReport.reportID] = itemReport; }); Object.keys(reportActions ?? {}).forEach((actionKey) => { if (!actionKey) { return; } - const reportID = CollectionUtils.extractCollectionItemID(actionKey as `reportActions_${string}`); - convertReportActions[reportID] = reportActions?.[actionKey] ?? null; + const reportID = CollectionUtils.extractCollectionItemID(actionKey as `${OnyxCollectionKey}${string}`); + convertedReportActions[reportID] = reportActions?.[actionKey] ?? null; }); const allAncestors: Ancestor[] = []; let parentReportID = report.parentReportID; let parentReportActionID = report.parentReportActionID; + // Store the child of parent report let currentReport = report; let currentUnread = shouldHideThreadDividerLine; + while (parentReportID) { - const parentReport = convertReports?.[parentReportID]; - const parentReportAction = convertReportActions?.[parentReportID]?.[parentReportActionID ?? ''] ?? null; + const parentReport = convertedReports?.[parentReportID]; + const parentReportAction = convertedReportActions?.[parentReportID]?.[parentReportActionID ?? ''] ?? null; + if (!parentReportAction || ReportActionsUtils.isTransactionThread(parentReportAction) || !parentReport) { break; } - const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport, parentReportAction, convertReportActions?.[parentReportID] ?? {}); + + const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport, parentReportAction, convertedReportActions?.[parentReportID] ?? {}); allAncestors.push({ report: currentReport, reportAction: parentReportAction, From 7df2fb79a1a458a448df1b9d708a58e8071f0b32 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 30 Jan 2024 18:12:28 +0700 Subject: [PATCH 07/10] merge main --- src/libs/ReportUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2646f6c36051..c0a8337da4b2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12,7 +12,8 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; -import ONYXKEYS, { OnyxCollectionKey } from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type { OnyxCollectionKey } from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; From d13b95469eab345d9cc5252dcb0a1d26ff7a8074 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 30 Jan 2024 18:17:25 +0700 Subject: [PATCH 08/10] fix lint --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c0a8337da4b2..e446c630b1c9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13,7 +13,7 @@ import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvata import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { OnyxCollectionKey } from '@src/ONYXKEYS'; +import type {OnyxCollectionKey} from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; @@ -4644,7 +4644,7 @@ function getAllAncestorReportActions( if (!parentReportAction || ReportActionsUtils.isTransactionThread(parentReportAction) || !parentReport) { break; } - + const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport, parentReportAction, convertedReportActions?.[parentReportID] ?? {}); allAncestors.push({ report: currentReport, From 518aec7699543bbc2988916a0be366829eb79f09 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 1 Feb 2024 16:44:52 +0700 Subject: [PATCH 09/10] implement unit test --- src/libs/ReportActionsUtils.ts | 6 +- src/libs/ReportUtils.ts | 69 ++++++++++------- src/libs/onyxSubscribe.ts | 2 +- .../report/ReportActionItemParentAction.tsx | 75 ++++++++++--------- tests/unit/ReportUtilsTest.js | 66 ++++++++++++++++ 5 files changed, 154 insertions(+), 64 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ba93d8169dab..4d7cc740fe08 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -811,7 +811,7 @@ function hasRequestFromCurrentAccount(reportID: string, currentAccountID: number */ function isReportActionUnread(reportAction: OnyxEntry, lastReadTime: string) { if (!lastReadTime) { - return Boolean(!isCreatedAction(reportAction)); + return !isCreatedAction(reportAction); } return Boolean(reportAction && lastReadTime && reportAction.created && lastReadTime < reportAction.created); @@ -821,9 +821,9 @@ function isReportActionUnread(reportAction: OnyxEntry, lastReadTim * Check whether the current report action of the report is unread or not * */ -function isCurrentActionUnread(report: Report | EmptyObject, reportAction: ReportAction, reportActions: ReportActions): boolean { +function isCurrentActionUnread(report: Report | EmptyObject, reportAction: ReportAction): boolean { const lastReadTime = report.lastReadTime ?? ''; - const sortedReportActions = getSortedReportActions(Object.values(reportActions)); + const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report.reportID))); const currentActionIndex = sortedReportActions.findIndex((action) => action.reportActionID === reportAction.reportActionID); if (currentActionIndex === -1) { return false; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7894bec20798..c90c7c6ac1d4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13,7 +13,6 @@ import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvata import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxCollectionKey} from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type { Beta, @@ -410,6 +409,11 @@ type Ancestor = { shouldHideThreadDividerLine: boolean; }; +type AncestorIDs = { + reportIDs: string[]; + reportActionsIDs: string[]; +}; + let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -4669,30 +4673,10 @@ function shouldDisableThread(reportAction: OnyxEntry, reportID: st ); } -function getAllAncestorReportActions( - report: Report | null | undefined, - shouldHideThreadDividerLine: boolean, - reports: OnyxCollection = {}, - reportActions: OnyxCollection = {}, -): Ancestor[] { +function getAllAncestorReportActions(report: Report | null | undefined, shouldHideThreadDividerLine: boolean): Ancestor[] { if (!report) { return []; } - const convertedReports: OnyxCollection = {}; - const convertedReportActions: OnyxCollection = {}; - Object.values(reports ?? {}).forEach((itemReport) => { - if (!itemReport) { - return; - } - convertedReports[itemReport.reportID] = itemReport; - }); - Object.keys(reportActions ?? {}).forEach((actionKey) => { - if (!actionKey) { - return; - } - const reportID = CollectionUtils.extractCollectionItemID(actionKey as `${OnyxCollectionKey}${string}`); - convertedReportActions[reportID] = reportActions?.[actionKey] ?? null; - }); const allAncestors: Ancestor[] = []; let parentReportID = report.parentReportID; let parentReportActionID = report.parentReportActionID; @@ -4702,14 +4686,14 @@ function getAllAncestorReportActions( let currentUnread = shouldHideThreadDividerLine; while (parentReportID) { - const parentReport = convertedReports?.[parentReportID]; - const parentReportAction = convertedReportActions?.[parentReportID]?.[parentReportActionID ?? ''] ?? null; + const parentReport = getReport(parentReportID); + const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '0'); if (!parentReportAction || ReportActionsUtils.isTransactionThread(parentReportAction) || !parentReport) { break; } - const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport, parentReportAction, convertedReportActions?.[parentReportID] ?? {}); + const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport, parentReportAction); allAncestors.push({ report: currentReport, reportAction: parentReportAction, @@ -4728,6 +4712,39 @@ function getAllAncestorReportActions( return allAncestors.reverse(); } +function getAllAncestorReportActionIDs(report: Report | null | undefined): AncestorIDs { + if (!report) { + return { + reportIDs: [], + reportActionsIDs: [], + }; + } + + const allAncestorIDs: AncestorIDs = { + reportIDs: [], + reportActionsIDs: [], + }; + let parentReportID = report.parentReportID; + let parentReportActionID = report.parentReportActionID; + + while (parentReportID) { + const parentReport = getReport(parentReportID); + const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '0'); + + if (!parentReportAction || ReportActionsUtils.isTransactionThread(parentReportAction) || !parentReport) { + break; + } + + allAncestorIDs.reportIDs.push(parentReportID ?? ''); + allAncestorIDs.reportActionsIDs.push(parentReportActionID ?? ''); + + parentReportID = parentReport?.parentReportID; + parentReportActionID = parentReport?.parentReportActionID; + } + + return allAncestorIDs; +} + function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry = null): boolean { if (!policy) { return false; @@ -4931,6 +4948,7 @@ export { isReportFieldOfTypeTitle, isReportFieldDisabled, getAvailableReportFields, + getAllAncestorReportActionIDs, }; export type { @@ -4942,4 +4960,5 @@ export type { OptimisticAddCommentReportAction, OptimisticCreatedReportAction, OptimisticClosedReportAction, + Ancestor, }; diff --git a/src/libs/onyxSubscribe.ts b/src/libs/onyxSubscribe.ts index af775842fc16..3a2daca900e4 100644 --- a/src/libs/onyxSubscribe.ts +++ b/src/libs/onyxSubscribe.ts @@ -8,7 +8,7 @@ import type {OnyxCollectionKey, OnyxKey} from '@src/ONYXKEYS'; * @param mapping Same as for Onyx.connect() * @return Unsubscribe callback */ -function onyxSubscribe(mapping: ConnectOptions) { +function onyxSubscribe(mapping: ConnectOptions) { const connectionId = Onyx.connect(mapping); return () => Onyx.disconnect(connectionId); } diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 048c5bf6b7cd..15a844ab5a72 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -1,14 +1,13 @@ -import {deepEqual} from 'fast-equals'; -import lodashIsEqual from 'lodash/isEqual'; -import React, {memo} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; +import onyxSubscribe from '@libs/onyxSubscribe'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -18,11 +17,8 @@ import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import ReportActionItem from './ReportActionItem'; type ReportActionItemParentActionOnyxProps = { - /** The report currently being looked at */ - allReportActions: OnyxCollection; - - /** The actions from the parent report */ - allReports: OnyxCollection; + /** The current report is displayed */ + report: OnyxEntry; }; type ReportActionItemParentActionProps = ReportActionItemParentActionOnyxProps & { @@ -37,12 +33,41 @@ type ReportActionItemParentActionProps = ReportActionItemParentActionOnyxProps & reportID: string; }; -function ReportActionItemParentAction({allReportActions = {}, allReports = {}, index = 0, shouldHideThreadDividerLine = false, reportID}: ReportActionItemParentActionProps) { +function ReportActionItemParentAction({report, index = 0, shouldHideThreadDividerLine = false}: ReportActionItemParentActionProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const allAncestors = ReportUtils.getAllAncestorReportActions(report, shouldHideThreadDividerLine, allReports, allReportActions); + const ancestorIDs = useRef(ReportUtils.getAllAncestorReportActionIDs(report)); + const [allAncestors, setAllAncestors] = useState([]); + + useEffect(() => { + const unsubscribeReports: Array<() => void> = []; + const unsubscribeReportActions: Array<() => void> = []; + ancestorIDs.current.reportIDs.forEach((ancestorReportID) => { + unsubscribeReports.push( + onyxSubscribe({ + key: `${ONYXKEYS.COLLECTION.REPORT}${ancestorReportID}`, + callback: () => { + setAllAncestors(ReportUtils.getAllAncestorReportActions(report, shouldHideThreadDividerLine)); + }, + }), + ); + unsubscribeReportActions.push( + onyxSubscribe({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReportID}`, + callback: () => { + setAllAncestors(ReportUtils.getAllAncestorReportActions(report, shouldHideThreadDividerLine)); + }, + }), + ); + }); + + return () => { + unsubscribeReports.forEach((unsubscribeReport) => unsubscribeReport()); + unsubscribeReportActions.forEach((unsubscribeReportAction) => unsubscribeReportAction()); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( <> @@ -79,27 +104,7 @@ function ReportActionItemParentAction({allReportActions = {}, allReports = {}, i ReportActionItemParentAction.displayName = 'ReportActionItemParentAction'; export default withOnyx({ - // We should subscribe all reports and report actions here to dynamic update when any parent report action is changed - allReportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - allReports: { - key: ONYXKEYS.COLLECTION.REPORT, + report: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, }, -})( - memo(ReportActionItemParentAction, (prevProps, nextProps) => { - const {allReportActions: prevAllReportActions, allReports: prevAllReports, ...prevPropsWithoutReportActionsAndReports} = prevProps; - const {allReportActions: nextAllReportActions, allReports: nextAllReports, ...nextPropsWithoutReportActionsAndReports} = nextProps; - - const prevReport = prevAllReports?.[`${ONYXKEYS.COLLECTION.REPORT}${prevProps.reportID}`]; - const nextReport = nextAllReports?.[`${ONYXKEYS.COLLECTION.REPORT}${nextProps.reportID}`]; - const prevAllAncestors = ReportUtils.getAllAncestorReportActions(prevReport, prevProps.shouldHideThreadDividerLine ?? false, prevAllReports, prevAllReportActions); - const nextAllAncestors = ReportUtils.getAllAncestorReportActions(nextReport, nextProps.shouldHideThreadDividerLine ?? false, nextAllReports, nextAllReportActions); - - if (prevReport !== nextReport || !deepEqual(prevAllAncestors, nextAllAncestors)) { - return false; - } - - return lodashIsEqual(prevPropsWithoutReportActionsAndReports, nextPropsWithoutReportActionsAndReports); - }), -); +})(ReportActionItemParentAction); diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index 23b748068b65..a5b0a5d3c151 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -605,4 +605,70 @@ describe('ReportUtils', () => { expect(ReportUtils.sortReportsByLastRead(reports)).toEqual(sortedReports); }); }); + + describe('getAllAncestorReportActions', () => { + const reports = [ + {reportID: '1', lastReadTime: '2024-02-01 04:56:47.233', reportName: 'Report'}, + {reportID: '2', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '1', parentReportID: '1', reportName: 'Report'}, + {reportID: '3', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '2', parentReportID: '2', reportName: 'Report'}, + {reportID: '4', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '3', parentReportID: '3', reportName: 'Report'}, + {reportID: '5', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '4', parentReportID: '4', reportName: 'Report'}, + ]; + + const reportActions = [ + {reportActionID: '1', created: '2024-02-01 04:42:22.965'}, + {reportActionID: '2', created: '2024-02-01 04:42:28.003'}, + {reportActionID: '3', created: '2024-02-01 04:42:31.742'}, + {reportActionID: '4', created: '2024-02-01 04:42:35.619'}, + ]; + + beforeAll(() => { + Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.REPORT}${reports[0].reportID}`]: reports[0], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[1].reportID}`]: reports[1], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[2].reportID}`]: reports[2], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[3].reportID}`]: reports[3], + [`${ONYXKEYS.COLLECTION.REPORT}${reports[4].reportID}`]: reports[4], + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[0].reportID}`]: {[reportActions[0].reportActionID]: reportActions[0]}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[1].reportID}`]: {[reportActions[1].reportActionID]: reportActions[1]}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[2].reportID}`]: {[reportActions[2].reportActionID]: reportActions[2]}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[3].reportID}`]: {[reportActions[3].reportActionID]: reportActions[3]}, + }); + return waitForBatchedUpdates(); + }); + + afterAll(() => Onyx.clear()); + + it('should return correctly all ancestors of a thread report', () => { + const resultAncestors = [ + {report: reports[1], reportAction: reportActions[0], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, + {report: reports[2], reportAction: reportActions[1], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, + {report: reports[3], reportAction: reportActions[2], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, + {report: reports[4], reportAction: reportActions[3], shouldDisplayNewMarker: false, shouldHideThreadDividerLine: false}, + ]; + + expect(ReportUtils.getAllAncestorReportActions(reports[4], false)).toEqual(resultAncestors); + }); + + it('should hide thread divider line of the nearest ancestor if the first action of thread report is unread', () => { + const allAncestors = ReportUtils.getAllAncestorReportActions(reports[4], true); + expect(allAncestors.reverse()[0].shouldHideThreadDividerLine).toBe(true); + }); + + it('should hide thread divider line of the previous ancestor and display unread marker of the current ancestor if the current ancestor action is unread', () => { + let allAncestors = ReportUtils.getAllAncestorReportActions(reports[4], false); + expect(allAncestors[0].shouldHideThreadDividerLine).toBe(false); + expect(allAncestors[1].shouldDisplayNewMarker).toBe(false); + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}2`, { + lastReadTime: '2024-02-01 04:42:28.001', + }) + .then(() => waitForBatchedUpdates()) + .then(() => { + allAncestors = ReportUtils.getAllAncestorReportActions(reports[4], false); + expect(allAncestors[0].shouldHideThreadDividerLine).toBe(true); + expect(allAncestors[1].shouldDisplayNewMarker).toBe(true); + }); + }); + }); }); From 9b9d8aa24f54a4543f77cdb0d61c3dfa5409b84a Mon Sep 17 00:00:00 2001 From: dukenv0307 <129500732+dukenv0307@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:22:59 +0700 Subject: [PATCH 10/10] Update src/pages/home/report/ReportActionsListItemRenderer.js Co-authored-by: Luthfi --- src/pages/home/report/ReportActionsListItemRenderer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index 9777027d022e..3fd6ddcef750 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -124,7 +124,6 @@ function ReportActionsListItemRenderer({ ) : (