diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8efd70c5be2b..5628a8796450 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4567,21 +4567,34 @@ function getInvoicesChatName({ } /** - * Get the title for a report using only participant names. This may be used for 1:1 DMs and other non-categorized chats. - */ -function buildReportNameFromParticipantNames({report, personalDetails}: {report: OnyxEntry; personalDetails?: Partial}) { - const participantsWithoutCurrentUser: number[] = []; - Object.keys(report?.participants ?? {}).forEach((accountID) => { - const accID = Number(accountID); - if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) { - participantsWithoutCurrentUser.push(accID); - } - }); - const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; - return participantsWithoutCurrentUser - .map((accountID) => getDisplayNameForParticipant({accountID, shouldUseShortForm: isMultipleParticipantReport, personalDetailsData: personalDetails})) - .join(', '); -} + * Generates a report title using the names of participants, excluding the current user. + * This function is useful in contexts such as 1:1 direct messages (DMs) or other group chats. + * It limits to a maximum of 5 participants for the title and uses short names unless there is only one participant. + */ +const buildReportNameFromParticipantNames = ({report, personalDetails: personalDetailsData}: {report: OnyxEntry; personalDetails?: Partial}) => + Object.keys(report?.participants ?? {}) + .map(Number) + .filter((id) => id !== currentUserAccountID) + .slice(0, 5) + .map((accountID) => ({ + accountID, + name: getDisplayNameForParticipant({ + accountID, + shouldUseShortForm: true, + personalDetailsData, + }), + })) + .filter((participant) => participant.name) + .reduce((formattedNames, {name, accountID}, _, array) => { + // If there is only one participant (if it is 0 or less the function will return empty string), return their full name + if (array.length < 2) { + return getDisplayNameForParticipant({ + accountID, + personalDetailsData, + }); + } + return formattedNames ? `${formattedNames}, ${name}` : name; + }, ''); function generateReportName(report: OnyxEntry): string { if (!report) { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index aa95baf25374..7391cbfe6ec7 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import {beforeAll} from '@jest/globals'; import {renderHook} from '@testing-library/react-native'; import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -13,6 +14,7 @@ import { buildOptimisticExpenseReport, buildOptimisticIOUReportAction, buildParticipantsFromAccountIDs, + buildReportNameFromParticipantNames, buildTransactionThread, canDeleteReportAction, canEditWriteCapability, @@ -49,7 +51,9 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, OnyxInputOrEntry, PersonalDetailsList, Policy, PolicyEmployeeList, Report, ReportAction, Transaction} from '@src/types/onyx'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; +import type {Participant} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; +import {chatReportR14932 as mockedChatReport} from '../../__mocks__/reportData/reports'; import * as NumberUtils from '../../src/libs/NumberUtils'; import {convertedInvoiceChat} from '../data/Invoice'; import createRandomPolicy from '../utils/collections/policies'; @@ -2456,4 +2460,67 @@ describe('ReportUtils', () => { expect(isArchivedNonExpenseReportWithID(archivedChatReport, isReportArchived.current)).toBe(true); }); }); + + describe('buildReportNameFromParticipantNames', () => { + /** + * Generates a fake report and matching personal details for specified number of participants. + * Participants in the report are directly linked with their personal details. + */ + const generateFakeReportAndParticipantsPersonalDetails = ({count, start = 0}: {count: number; start?: number}): {report: Report; personalDetails: PersonalDetailsList} => { + const data = { + report: { + ...mockedChatReport, + participants: Object.keys(fakePersonalDetails) + .slice(start, count) + .reduce>((acc, cur) => { + acc[cur] = {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}; + return acc; + }, {}), + }, + personalDetails: Object.fromEntries(Object.entries(fakePersonalDetails).slice(start, count)), + }; + + data.personalDetails[currentUserAccountID] = { + accountID: currentUserAccountID, + displayName: 'CURRENT USER', + firstName: 'CURRENT', + }; + + return data; + }; + + it('excludes the current user from the report title', () => { + const result = buildReportNameFromParticipantNames(generateFakeReportAndParticipantsPersonalDetails({count: currentUserAccountID + 2})); + expect(result).not.toContain('CURRENT'); + }); + + it('limits to a maximum of 5 participants in the title', () => { + const result = buildReportNameFromParticipantNames(generateFakeReportAndParticipantsPersonalDetails({count: 10})); + expect(result.split(',').length).toBeLessThanOrEqual(5); + }); + + it('returns full name if only one participant is present (excluding current user)', () => { + const result = buildReportNameFromParticipantNames(generateFakeReportAndParticipantsPersonalDetails({count: 1})); + const {displayName} = fakePersonalDetails[1] ?? {}; + expect(result).toEqual(displayName); + }); + + it('returns an empty string if there are no participants or all are excluded', () => { + const result = buildReportNameFromParticipantNames(generateFakeReportAndParticipantsPersonalDetails({start: currentUserAccountID - 1, count: 1})); + expect(result).toEqual(''); + }); + + it('handles partial or missing personal details correctly', () => { + const {report} = generateFakeReportAndParticipantsPersonalDetails({count: 6}); + + const secondUser = fakePersonalDetails[2]; + const fourthUser = fakePersonalDetails[4]; + + const incompleteDetails = {2: secondUser, 4: fourthUser}; + const result = buildReportNameFromParticipantNames({report, personalDetails: incompleteDetails}); + const expectedNames = [secondUser?.firstName, fourthUser?.firstName].sort(); + const resultNames = result.split(', ').sort(); + expect(resultNames).toEqual(expect.arrayContaining(expectedNames)); + }); + }); });