diff --git a/src/components/ReportActionAvatars/ReportActionAvatar.tsx b/src/components/ReportActionAvatars/ReportActionAvatar.tsx index 806548c7b982..214ed95d61a0 100644 --- a/src/components/ReportActionAvatars/ReportActionAvatar.tsx +++ b/src/components/ReportActionAvatars/ReportActionAvatar.tsx @@ -330,7 +330,7 @@ function ReportActionAvatarMultipleHorizontal({ const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {localeCompare, formatPhoneNumber} = useLocalize(); + const {localeCompare, formatPhoneNumber, translate} = useLocalize(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); @@ -370,8 +370,8 @@ function ReportActionAvatarMultipleHorizontal({ }, [icons, maxAvatarsInRow, shouldDisplayAvatarsInRows]); const tooltipTexts = useMemo( - () => (shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)) : ['']), - [shouldShowTooltip, icons, formatPhoneNumber], + () => (shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, translate, icon.name)) : ['']), + [shouldShowTooltip, icons, formatPhoneNumber, translate], ); return avatarRows.map((avatars, rowIndex) => ( @@ -482,11 +482,11 @@ function ReportActionAvatarMultipleDiagonal({ const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {formatPhoneNumber} = useLocalize(); + const {formatPhoneNumber, translate} = useLocalize(); const tooltipTexts = useMemo( - () => (shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)) : ['']), - [shouldShowTooltip, icons, formatPhoneNumber], + () => (shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, translate, icon.name)) : ['']), + [shouldShowTooltip, icons, formatPhoneNumber, translate], ); const removeRightMargin = icons.length === 2 && size === CONST.AVATAR_SIZE.X_LARGE; const avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); diff --git a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx index fb2bd5fb719d..92c160e1b2c6 100644 --- a/src/components/Search/SearchList/ListItem/AttendeesCell.tsx +++ b/src/components/Search/SearchList/ListItem/AttendeesCell.tsx @@ -46,7 +46,7 @@ function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {localeCompare, formatPhoneNumber} = useLocalize(); + const {localeCompare, formatPhoneNumber, translate} = useLocalize(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); @@ -59,7 +59,7 @@ function AttendeesCell({attendees, isHovered, isPressed}: AttendeesCellProps) { const avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height), styles.overflowHidden]); const icons = sortIconsByName(attendeeIcons, personalDetails, localeCompare); - const tooltipTexts = icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, icon.name)); + const tooltipTexts = icons.map((icon) => getUserDetailTooltipText(Number(icon.id), formatPhoneNumber, translate, icon.name)); return ( 0) { const delegateUserDetails = personalDetails?.[delegateAccountID]; - const delegateUserDisplayName = getUserDetailTooltipText(delegateAccountID, formatPhoneNumber); + const delegateUserDisplayName = getUserDetailTooltipText(delegateAccountID, formatPhoneNumber, translate); userDisplayName = `${delegateUserDisplayName} (${translate('reportAction.asCopilot')} ${userDisplayName})`; userLogin = delegateUserDetails?.login ?? ''; userAvatar = delegateUserDetails?.avatar; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index adc6b0755786..946093c3b5ba 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4154,8 +4154,8 @@ function getDisplayNamesWithTooltips( /** * Returns the the display names of the given user accountIDs */ -function getUserDetailTooltipText(accountID: number, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], fallbackUserDisplayName = ''): string { - const displayNameForParticipant = getDisplayNameForParticipant({accountID, formatPhoneNumber}); +function getUserDetailTooltipText(accountID: number, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], translate: LocalizedTranslate, fallbackUserDisplayName = ''): string { + const displayNameForParticipant = getDisplayNameForParticipant({accountID, formatPhoneNumber, translate}); return displayNameForParticipant || fallbackUserDisplayName; } @@ -5782,7 +5782,7 @@ function getReportPreviewMessage(translate: LocalizedTranslate, params: GetRepor const policyName = getPolicyName({report: parentReport ?? report, policy}); const payerName = isExpenseReport(report) ? policyName - : getDisplayNameForParticipant({accountID: report.managerID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils}); + : getDisplayNameForParticipant({accountID: report.managerID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}); const formattedAmount = convertToDisplayString(totalAmount, report.currency); @@ -5839,7 +5839,7 @@ function getReportPreviewMessage(translate: LocalizedTranslate, params: GetRepor let actualPayerName = report.managerID === deprecatedCurrentUserAccountID && !isForListPreview ? '' - : getDisplayNameForParticipant({accountID: payerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils}); + : getDisplayNameForParticipant({accountID: payerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}); actualPayerName = actualPayerName && isForListPreview && !isPreviewMessageForParentChatReport ? `${actualPayerName}:` : actualPayerName; const payerDisplayName = isPreviewMessageForParentChatReport ? payerName : actualPayerName; @@ -5858,7 +5858,8 @@ function getReportPreviewMessage(translate: LocalizedTranslate, params: GetRepor } if (report.isWaitingOnBankAccount) { - const submitterDisplayName = getDisplayNameForParticipant({accountID: report.ownerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils}) ?? ''; + const submitterDisplayName = + getDisplayNameForParticipant({accountID: report.ownerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}) ?? ''; return translate('iou.waitingOnBankAccount', submitterDisplayName); } @@ -5887,13 +5888,17 @@ function getReportPreviewMessage(translate: LocalizedTranslate, params: GetRepor // We only want to show the actor name in the preview if it's not the current user who took the action const requestorName = lastActorID && lastActorID !== deprecatedCurrentUserAccountID - ? getDisplayNameForParticipant({accountID: lastActorID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils}) + ? getDisplayNameForParticipant({accountID: lastActorID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}) : ''; return `${requestorName ? `${requestorName}: ` : ''}${translate('iou.expenseAmount', amountToDisplay, comment)}`; } if (containsNonReimbursable) { - return translate('iou.payerSpentAmount', formattedAmount, getDisplayNameForParticipant({accountID: report.ownerAccountID, formatPhoneNumber: formatPhoneNumberPhoneUtils}) ?? ''); + return translate( + 'iou.payerSpentAmount', + formattedAmount, + getDisplayNameForParticipant({accountID: report.ownerAccountID, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}) ?? '', + ); } return translate('iou.payerOwesAmount', formattedAmount, payerName ?? '', comment); } @@ -6227,7 +6232,7 @@ function getModifiedExpenseOriginalMessage( /** * Get the payee name given a report. */ -function getPayeeName(report: OnyxEntry): string | undefined { +function getPayeeName(report: OnyxEntry, translate: LocalizedTranslate): string | undefined { if (isEmptyObject(report)) { return undefined; } @@ -6239,7 +6244,7 @@ function getPayeeName(report: OnyxEntry): string | undefined { if (participantsWithoutCurrentUser.length === 0) { return undefined; } - return getDisplayNameForParticipant({accountID: participantsWithoutCurrentUser.at(0), shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils}); + return getDisplayNameForParticipant({accountID: participantsWithoutCurrentUser.at(0), shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}); } // TODO: currentUserEmail will be required eventually so this becomes a pure function. Subscribe the data via useOnyx and pass it from the component. Refactor issue: https://github.com/Expensify/App/issues/66412 diff --git a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index db60fb351493..466aadb383ff 100644 --- a/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -257,7 +257,7 @@ function AttachmentPickerWithMenuItems({ [CONST.IOU.TYPE.PAY]: [ { icon: getIconForAction(CONST.IOU.TYPE.SEND, icons), - text: translate('iou.paySomeone', getPayeeName(report)), + text: translate('iou.paySomeone', getPayeeName(report, translate)), shouldCallAfterModalHide: shouldUseNarrowLayout, sentryLabel: CONST.SENTRY_LABEL.REPORT.ATTACHMENT_PICKER_MENU_PAY_SOMEONE, onSelected: () => { diff --git a/src/pages/iou/request/DistanceRequestStartPage.tsx b/src/pages/iou/request/DistanceRequestStartPage.tsx index 1f2202d02864..6b5fe6bdc525 100644 --- a/src/pages/iou/request/DistanceRequestStartPage.tsx +++ b/src/pages/iou/request/DistanceRequestStartPage.tsx @@ -60,8 +60,8 @@ function DistanceRequestStartPage({ const tabTitles = { [CONST.IOU.TYPE.REQUEST]: translate('iou.trackDistance'), [CONST.IOU.TYPE.SUBMIT]: translate('iou.trackDistance'), - [CONST.IOU.TYPE.SEND]: translate('iou.paySomeone', getPayeeName(report)), - [CONST.IOU.TYPE.PAY]: translate('iou.paySomeone', getPayeeName(report)), + [CONST.IOU.TYPE.SEND]: translate('iou.paySomeone', getPayeeName(report, translate)), + [CONST.IOU.TYPE.PAY]: translate('iou.paySomeone', getPayeeName(report, translate)), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitExpense'), [CONST.IOU.TYPE.SPLIT_EXPENSE]: translate('iou.splitExpense'), [CONST.IOU.TYPE.TRACK]: translate('iou.trackDistance'), diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 459b737efd7e..88608acbc3c4 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -92,8 +92,8 @@ function IOURequestStartPage({ const tabTitles = { [CONST.IOU.TYPE.REQUEST]: translate('iou.createExpense'), [CONST.IOU.TYPE.SUBMIT]: translate('iou.createExpense'), - [CONST.IOU.TYPE.SEND]: translate('iou.paySomeone', getPayeeName(report)), - [CONST.IOU.TYPE.PAY]: translate('iou.paySomeone', getPayeeName(report)), + [CONST.IOU.TYPE.SEND]: translate('iou.paySomeone', getPayeeName(report, translate)), + [CONST.IOU.TYPE.PAY]: translate('iou.paySomeone', getPayeeName(report, translate)), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitExpense'), [CONST.IOU.TYPE.SPLIT_EXPENSE]: translate('iou.splitExpense'), [CONST.IOU.TYPE.TRACK]: translate('iou.createExpense'), diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index f0f1f6234e1b..37018372bc6d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -109,6 +109,7 @@ import { getParentNavigationSubtitle, getParsedComment, getParticipantsList, + getPayeeName, getPolicyChangeLogCopyMessage, getPolicyExpenseChat, getPolicyIDsWithEmptyReportsForAccount, @@ -134,6 +135,7 @@ import { getTransactionsWithReceipts, getUnheldReimbursableTotal, getUnreportedTransactionMessage, + getUserDetailTooltipText, getViolatingReportIDForRBRInLHN, getWorkspaceIcon, getWhisperDisplayNames, @@ -16185,6 +16187,33 @@ describe('ReportUtils', () => { // ...but the report-action-message variant is always the English text, regardless of the loaded locale expect(getReportPreviewReportActionMessage(params)).toBe(getReportPreviewMessage(englishTranslate, params)); }); + + it('routes the participant display name through the injected translate function', async () => { + const hiddenManagerAccountID = 987654; + const iouReport: Report = { + ...LHNTestUtils.getFakeReport(), + reportID: 'preview-marker-report', + type: CONST.REPORT.TYPE.IOU, + currency: CONST.CURRENCY.USD, + managerID: hiddenManagerAccountID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); + // A participant with no name resolves to the "hidden" copy, which is produced by the injected translate + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [hiddenManagerAccountID]: {accountID: hiddenManagerAccountID, login: '', displayName: ''}, + }); + + // A translate that tags the hidden-participant fallback so we can prove the preview used it + const translateWithMarker: LocalizedTranslate = (path, ...parameters) => + path === 'common.hidden' ? 'HiddenParticipantMarker' : translate(CONST.LOCALES.EN, path, ...parameters); + + const result = getReportPreviewMessage(translateWithMarker, {reportOrID: iouReport}); + + // The manager's name resolves to the marker, proving getDisplayNameForParticipant received the injected translate + expect(result).toContain('HiddenParticipantMarker'); + }); }); describe('getReportPreviewReportActionMessage (hardcoded English)', () => { @@ -16209,6 +16238,50 @@ describe('ReportUtils', () => { }); }); + describe('getUserDetailTooltipText', () => { + it('routes the display name through the injected translate function', async () => { + const hiddenAccountID = 778899; + // A participant with no name resolves to the "hidden" copy, which is produced by the injected translate + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [hiddenAccountID]: {accountID: hiddenAccountID, login: '', displayName: ''}, + }); + await waitForBatchedUpdates(); + + const translateWithMarker: LocalizedTranslate = (path, ...parameters) => (path === 'common.hidden' ? 'HiddenTooltipMarker' : translate(CONST.LOCALES.EN, path, ...parameters)); + + // The nameless participant resolves to the marker, proving getDisplayNameForParticipant received the injected translate + expect(getUserDetailTooltipText(hiddenAccountID, formatPhoneNumber, translateWithMarker)).toBe('HiddenTooltipMarker'); + }); + + it('falls back to the provided display name when the participant has no resolvable name', () => { + // accountID 0 short-circuits getDisplayNameForParticipant to '', so the fallback name is returned + expect(getUserDetailTooltipText(0, formatPhoneNumber, translateLocal, 'Fallback Name')).toBe('Fallback Name'); + }); + }); + + describe('getPayeeName', () => { + it('routes the payee display name through the injected translate function', async () => { + const hiddenPayeeAccountID = 665544; + const report: Report = { + ...LHNTestUtils.getFakeReport(), + reportID: 'payee-marker-report', + participants: { + [hiddenPayeeAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + }, + }; + // A payee with no name resolves to the "hidden" copy, which is produced by the injected translate + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [hiddenPayeeAccountID]: {accountID: hiddenPayeeAccountID, login: '', displayName: ''}, + }); + await waitForBatchedUpdates(); + + const translateWithMarker: LocalizedTranslate = (path, ...parameters) => (path === 'common.hidden' ? 'HiddenPayeeMarker' : translate(CONST.LOCALES.EN, path, ...parameters)); + + // The nameless payee resolves to the marker, proving getDisplayNameForParticipant received the injected translate + expect(getPayeeName(report, translateWithMarker)).toBe('HiddenPayeeMarker'); + }); + }); + describe('isConciergeChatReport', () => { it('should return false for null/undefined report', () => { expect(isConciergeChatReport(null, '12345')).toBe(false);