diff --git a/src/CONST.ts b/src/CONST.ts index 33eae01ed9c8..7d0cd3c4cf54 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -171,7 +171,7 @@ const CONST = { }, // Note: Group and Self-DM excluded as these are not tied to a Workspace - WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], + WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE], ANDROID_PACKAGE_NAME, WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f584f694edd0..a268b41eea0a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -994,20 +994,20 @@ const iouRequestTypes = new Set>([ CONST.IOU.REPORT_ACTION_TYPE.TRACK, ]); -/** - * Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions. - * Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. - */ -function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined { - // If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report. +function getMoneyRequestActions( + reportID: string, + reportActions: OnyxEntry | ReportAction[], + isOffline: boolean | undefined = undefined, +): Array> { + // If the report is not an IOU, Expense report, or Invoice, it shouldn't have money request actions. const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) { - return; + return []; } const reportActionsArray = Array.isArray(reportActions) ? reportActions : Object.values(reportActions ?? {}); if (!reportActionsArray.length) { - return; + return []; } const iouRequestActions = []; @@ -1035,6 +1035,15 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn iouRequestActions.push(action); } } + return iouRequestActions; +} + +/** + * Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions. + * Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. + */ +function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined { + const iouRequestActions = getMoneyRequestActions(reportID, reportActions, isOffline); // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report if (!iouRequestActions.length || iouRequestActions.length > 1) { @@ -1054,6 +1063,27 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn return singleAction.childReportID; } +/** + * Returns true if all transactions on the report have the same ownerID + */ +function hasSameActorForAllTransactions(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): boolean { + const iouRequestActions = getMoneyRequestActions(reportID, reportActions, isOffline); + if (!iouRequestActions.length) { + return true; + } + + let actorID: number | undefined; + + for (const action of iouRequestActions) { + if (actorID !== undefined && actorID !== action?.actorAccountID) { + return false; + } + actorID = action?.actorAccountID; + } + + return true; +} + /** * When we delete certain reports, we want to check whether there are any visible actions left to display. * If there are no visible actions left (including system messages), we can hide the report from view entirely @@ -1834,6 +1864,7 @@ export { getRenamedAction, isCardIssuedAction, getCardIssuedMessage, + hasSameActorForAllTransactions, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f4391a1171ac..158c56e54a02 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1580,12 +1580,20 @@ function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): bo return transactions.every((transaction) => !TransactionUtils.getReimbursable(transaction)); } +/** + * Checks if a report has only transactions with same ownerID + */ +function isSingleActorMoneyReport(reportID: string): boolean { + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]); + return !!ReportActionsUtils.hasSameActorForAllTransactions(reportID, reportActions); +} + /** * Checks if a report has only one transaction associated with it */ function isOneTransactionReport(reportID: string): boolean { const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]); - return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null; + return !!ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions); } /* @@ -2210,7 +2218,7 @@ function getIcons( if (isChatThread(report)) { const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; - const actorAccountID = getReportActionActorAccountID(parentReportAction, report); + const actorAccountID = getReportActionActorAccountID(parentReportAction); const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false); const actorIcon = { id: actorAccountID, @@ -2305,7 +2313,7 @@ function getIcons( const isManager = currentUserAccountID === report?.managerID; // For one transaction IOUs, display a simplified report icon - if (isOneTransactionReport(report?.reportID ?? '-1')) { + if (isOneTransactionReport(report?.reportID ?? '-1') || isSingleActorMoneyReport(report?.reportID ?? '-1')) { return [ownerIcon]; } @@ -4819,9 +4827,9 @@ function buildOptimisticReportPreview( }, ], created, - accountID: iouReport?.managerID ?? -1, + accountID: iouReport?.ownerAccountID ?? -1, // The preview is initially whispered if created with a receipt, so the actor is the current user as well - actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? -1, + actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.ownerAccountID ?? -1, childReportID: childReportID ?? iouReport?.reportID, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, @@ -6644,7 +6652,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { return true; } - if (isExpenseReport(report) && isOneTransactionReport(report?.reportID ?? '-1')) { + if (isExpenseReport(report)) { return true; } @@ -6656,7 +6664,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { return true; } - if (isInvoiceRoom(report)) { + if (isInvoiceRoom(report) || isInvoiceReport(report)) { return true; } @@ -7538,10 +7546,10 @@ function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boo return (isChatThread(report) && !!getReportNotificationPreference(report)) || isUserCreatedPolicyRoom(report) || isNonAdminOrOwnerOfPolicyExpenseChat(report, policy); } -function getReportActionActorAccountID(reportAction: OnyxInputOrEntry, iouReport: OnyxInputOrEntry | undefined): number | undefined { +function getReportActionActorAccountID(reportAction: OnyxInputOrEntry): number | undefined { switch (reportAction?.actionName) { case CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW: - return !isEmptyObject(iouReport) ? iouReport.managerID : reportAction?.childManagerAccountID; + return reportAction?.childOwnerAccountID ?? reportAction?.actorAccountID; case CONST.REPORT.ACTIONS.TYPE.SUBMITTED: return reportAction?.adminAccountID ?? reportAction?.actorAccountID; @@ -8122,6 +8130,7 @@ export { isIndividualInvoiceRoom, isAuditor, hasMissingInvoiceBankAccount, + isSingleActorMoneyReport, }; export type { diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 57ac3a0c0bf4..ac40889e6c6e 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -81,19 +81,20 @@ function ReportActionItemSingle({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const actorAccountID = ReportUtils.getReportActionActorAccountID(action, iouReport); + const actorAccountID = ReportUtils.getReportActionActorAccountID(action); const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : -1}`); - let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); + const icons = ReportUtils.getIcons(iouReport ?? null, personalDetails); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); const isTripRoom = ReportUtils.isTripRoom(report); const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW; - const displayAllActors = isReportPreviewAction && !isTripRoom; + const displayAllActors = isReportPreviewAction && !isTripRoom && ReportUtils.isIOUReport(iouReport ?? null) && icons.length > 1; const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? null); const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID; + const managerID = iouReport?.managerID ?? action?.childManagerAccountID; let avatarSource = avatar; let avatarId: number | string | undefined = actorAccountID; @@ -130,10 +131,9 @@ function ReportActionItemSingle({ }; } else { // The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice - const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID; + const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? managerID : ownerAccountID; const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); - secondaryAvatar = { source: secondaryUserAvatar, type: CONST.ICON_TYPE_AVATAR, @@ -150,24 +150,41 @@ function ReportActionItemSingle({ } else { secondaryAvatar = {name: '', source: '', type: 'avatar'}; } - const icon = { - source: avatarSource ?? FallbackAvatar, - type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, - name: primaryDisplayName ?? '', - id: avatarId, - }; + + const icon = useMemo( + () => ({ + source: avatarSource ?? FallbackAvatar, + type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, + name: primaryDisplayName ?? '', + id: avatarId, + }), + [avatarSource, isWorkspaceActor, primaryDisplayName, avatarId], + ); // Since the display name for a report action message is delivered with the report history as an array of fragments // we'll need to take the displayName from personal details and have it be in the same format for now. Eventually, // we should stop referring to the report history items entirely for this information. - const personArray = displayName - ? [ - { - type: 'TEXT', - text: displayName, - }, - ] - : action?.person; + const personArray = useMemo(() => { + const baseArray = displayName + ? [ + { + type: 'TEXT', + text: displayName, + }, + ] + : action?.person ?? []; + + if (displayAllActors) { + return [ + ...baseArray, + { + type: 'TEXT', + text: secondaryAvatar.name ?? '', + }, + ]; + } + return baseArray; + }, [displayName, action?.person, displayAllActors, secondaryAvatar?.name]); const reportID = report?.reportID; const iouReportID = iouReport?.reportID; @@ -192,45 +209,130 @@ function ReportActionItemSingle({ [action, isWorkspaceActor, actorAccountID], ); - const getAvatar = () => { - if (displayAllActors) { + const getAvatar = useMemo(() => { + return () => { + if (displayAllActors) { + return ( + + ); + } + if (shouldShowSubscriptAvatar) { + return ( + + ); + } return ( - + + + + + ); - } - if (shouldShowSubscriptAvatar) { + }; + }, [ + displayAllActors, + shouldShowSubscriptAvatar, + actorAccountID, + action?.delegateAccountID, + icon, + styles.actionAvatar, + fallbackIcon, + icons, + StyleUtils, + theme.appBG, + theme.hoverComponentBG, + theme.componentBG, + isHovered, + secondaryAvatar, + ]); + + const getHeading = useMemo(() => { + return () => { + if (displayAllActors && secondaryAvatar.name && isReportPreviewAction) { + return ( + + + + {` & `} + + + + ); + } return ( - - ); - } - return ( - - + {personArray?.map((fragment) => ( + + ))} - - ); - }; + ); + }; + }, [ + displayAllActors, + secondaryAvatar, + isReportPreviewAction, + personArray, + styles.flexRow, + styles.flex1, + styles.chatItemMessageHeaderSender, + styles.pre, + action, + actorAccountID, + displayName, + icon, + ]); + const hasEmojiStatus = !displayAllActors && status?.emojiCode; const formattedDate = DateUtils.getStatusUntilDate(status?.clearAfter ?? ''); const statusText = status?.text ?? ''; @@ -261,18 +363,7 @@ function ReportActionItemSingle({ accessibilityLabel={actorHint} role={CONST.ROLE.BUTTON} > - {personArray?.map((fragment, index) => ( - - ))} + {getHeading()} {!!hasEmojiStatus && ( @@ -290,7 +381,5 @@ function ReportActionItemSingle({ ); } - ReportActionItemSingle.displayName = 'ReportActionItemSingle'; - export default ReportActionItemSingle; diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index ff1c2431ca8b..63b2cb43d836 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -171,7 +171,7 @@ function ReportActionsListItemRenderer({ displayAsGroup={displayAsGroup} shouldDisplayNewMarker={shouldDisplayNewMarker} shouldShowSubscriptAvatar={ - ReportUtils.isPolicyExpenseChat(report) && + (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isInvoiceRoom(report)) && [ CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,